Raspberry Pi · Lesson 1
Raspberry Pi Web Server
Turn a computer the size of a deck of cards into a live website
- Grades 7 to 12
- Time 3 class periods
- Difficulty Beginner
Right now the Raspberry Pi in front of you is just a small computer. By the end of today it will be a server: it will have a web address, and anyone on the school network can type that address into another computer and see a page you built. By the end of the week that page will be showing the Pi's own CPU, memory, disk and temperature, updating live, and it will look good.
What you'll learn
- What a web server actually is, and how to run one in six lines of code
- How a page asks a server for data using an API, and gets JSON back
- How to reach one computer from another using SSH
- How HTML, CSS and JavaScript split the work of a web page between them
- How to change something on the server and watch it appear in the browser
What you need
- A Raspberry Pi at a monitor station, already set up and on the school network
- The same Pi, every day. Your folder is saved on that Pi, not in the cloud, so sit at the same one on Days 2 and 3
- A second computer in the lab, not a Pi, on the school network, for Day 2. You will load your dashboard on it and use it for the SSH step
- A sticky note and a pen
- No phone. No accounts. No downloads. Nothing to sign up for.
Before you touch the Pi
- Power comes out last. Shut the Pi down from the menu before unplugging it. Yanking the power while it is writing to the SD card is the one reliable way to destroy your work.
- The SD card stays in. It is the Pi's whole hard drive. Do not pop it out to see what happens.
- Nothing goes in the ports except what belongs there. The exposed pins along the edge are live.
- Never type a password into a file. Not your Pi password, not the Wi-Fi password, not anything. You will be asked for a password in the terminal on Day 2. That is the only place it goes.
How this lesson works
Every grey box is something you type or paste. The bar on top of each box tells you where it goes.
| The bar says | What to do |
|---|---|
| Type in the Terminal | A command. Click Copy, click inside the Terminal window, paste, then tap the Enter key. |
| server.py or static/index.html | The inside of a file. Open the file with nano, click Copy, paste into nano, then save. |
| Gold bar, dashed box, no Copy button | Look, do not paste. It is an example or what you should see. |
If the Copy button says Press Ctrl+C, your browser blocked it. The code is already selected, so press Ctrl+C in the browser and carry on.
Day 1
Start here
- Pick a Pi and keep it. Use the same Pi on Days 2 and 3. Your work is saved on that Pi, so a different Pi will not have it.
- Open the Terminal. It is the black rectangle icon on the top bar.
- Today: write down your username, your IP address and your folder name, build your Python workspace, then run your first web server.
- Finish line: a Chromium tab that says My Pi is a web server.
Step 1: Find out who you are and where you are
Open the Terminal on the Pi. It is the black rectangle icon on the top bar.
Type this, then tap the Enter key. The Terminal does nothing until you do:
whoami
That prints your username on this Pi. Write it down. You will need it on Day 2. Now type:
hostname -I
That prints your Pi's IP address, something like 10.0.4.71. Write it on a sticky note and put it on your monitor. Everything on Day 2 depends on it.
What an IP address is
Your network is a neighborhood and every device on it gets a house number. That is the IP address. When someone types it into a browser they are saying "take me to that house."
The computer next to you has one. So does every Chromebook in the school. Your Pi has one, and for the next three days it is the only thing standing between your project and everybody else being able to see it.
Claim your own folder
Everyone in both classes shares these Pis. If two people build a folder with the same name, the second person lands inside the first person's project, sees work they did not do, and overwrites it when they save. So before you build anything, you pick a name that is only yours.
Your folder name is your first name plus the first letter of your last name, all lowercase, no spaces. Jordan Baptiste is jordanb. If two of you would get the same name, one of you adds a number: jordanb2.
Type it in the box and every command on this page changes to use it. If the box is not there, type your folder name everywhere you see YOURNAME.
What changed, with an example
This lesson used to have everyone use the same two names, pihealth and pi-dashboard. Same names on a shared Pi means the second person walks into the first person's work. The names stay the same. They just move inside your own folder.
| Old way | New way, for jordanb | |
|---|---|---|
| Your folder | none | ~/jordanb |
| Python toolbox | ~/pihealth | ~/jordanb/pihealth |
| Project | ~/pi-dashboard | ~/jordanb/pi-dashboard |
| Prompt shows | (pihealth) | (jordanb-pihealth) |
~/pihealth or ~/pi-dashboard the old way? Leave them where they are. You cannot tell whose work is in there. Start fresh in your own folder.Sharing a Pi without wrecking anyone's work
See whose folders are already on this Pi:
ls ~
You will see Desktop, Documents and the rest, plus one folder per student who has used this Pi. Four rules:
- Only work inside your own folder. Every command in this lesson starts with
~/YOURNAME. If a command does not, stop and check. - Never open, edit or delete a folder that is not yours. Treat it like someone's locker.
- Your name already there, and it is not your work? Someone else has your name. Add a number and use that instead.
- Before you leave, stop your server with Ctrl+C. Only one server can use port 5000 at a time, and the next person needs it.
Step 2: Set up your Python workspace
This project needs two Python add-ons that are not installed by default. They live in a little workspace of their own called pihealth, inside your folder. First, check whether you already built it last time:
ls ~/YOURNAME/pihealth
| What you see | What it means | What to do |
|---|---|---|
A short list: bin, include, lib, pyvenv.cfg | Already built | Skip to Open it |
No such file or directory | Not built yet | Do Build it first |
Build it. Two commands, one at a time. Wait for the first to finish.
mkdir -p ~/YOURNAME && python3 -m venv --prompt YOURNAME-pihealth ~/YOURNAME/pihealth
~/YOURNAME/pihealth/bin/pip install flask psutil
The second one downloads for a minute and prints a lot of text. Wait for your prompt to come back.
Open it. Everyone runs this, whether you just built the workspace or you built it last time:
source ~/YOURNAME/pihealth/bin/activate
(YOURNAME-pihealth), your own name. Someone else's name there means you opened their box. Type deactivate and run your own source line.What did you just build?
A virtual environment: a box that holds one project's extra Python tools and nothing else. Two tools went in the box. Flask lets Python run a website. psutil lets Python read the computer's own CPU, memory and temperature.
Think of it like a toolbox for one job. The rest of the Pi is not affected, and if you ever wreck it you delete the folder and build a new one in thirty seconds.
(YOURNAME-pihealth) missing from your prompt, and the symptom is ModuleNotFoundError: No module named 'flask'. The fix is always the same: run the source line again.Step 3: Make your project folder
mkdir -p ~/YOURNAME/pi-dashboard/static && cd ~/YOURNAME/pi-dashboard
You just made this:
YOURNAME/ ← your folder, nobody else's
├── pihealth/ ← your Python toolbox (Step 2)
└── pi-dashboard/ ← this project
├── server.py ← the Python that runs the server (next step)
└── static/ ← the files your server hands out
└── index.html ← your actual web page (Step 7)
The ~ means the home folder of the account you are logged in as. Everyone on this Pi shares that account, which is exactly why your work lives one level down, in ~/YOURNAME.
Meet nano, the editor you will use all week
nano opens inside the Terminal. There are no menus to click. Mouse clicks will not move the cursor, so use the arrow keys.
The bottom of the screen lists the commands. The little ^ means the Ctrl key, so ^O means hold Ctrl and tap O.
The two in gold are the two you need today.
Save and exit nano, one key at a time
Save (do this often):
- Hold the Ctrl key and tap O. A line appears at the bottom:
File Name to Write: server.py - Tap the Enter key on your keyboard. That says yes to the name. The bottom now says
[ Wrote 9 lines ]
Exit: hold Ctrl and tap X. You are back at the Terminal prompt.
Save modified buffer? Tap the Y key for yes, then the Enter key. Tap N to throw your changes away.[ Wrote ... lines ] before you exited. No message means nothing was saved.Step 4: Write the smallest web server that works
nano server.py
Type or paste exactly this. The easy way: click Copy above the code, click inside the nano window, then press Ctrl+Shift+V.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "<h1>My Pi is a web server.</h1>"
app.run(host="0.0.0.0", port=5000)
Save: hold Ctrl and tap O, then tap the Enter key. Exit: hold Ctrl and tap X.
Step 5: Run it
python3 server.py
The Terminal will print a few lines and then sit there looking frozen. It has not crashed, it is listening.
Open Chromium on the Pi and go to:
http://localhost:5000
To stop it: click the Terminal window and press Ctrl+C. Remember this. You will need it constantly.
What the Terminal should print
When your server starts, it prints lines like these. Read them. The last Running on line is your Pi's IP address and port, which is the address Day 2 uses.
* Serving Flask app 'server'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://10.0.4.71:5000
Press CTRL+C to quit
ModuleNotFoundError instead? You are outside your toolbox. Your prompt is missing (YOURNAME-pihealth). Run the source line from Step 2 again, then run this again.What a server actually does
A server does one thing, over and over, forever:
- It waits.
- Something asks it for a thing. This is a request.
- It sends that thing back. This is a response.
- Go to 1.
Nothing else. When you typed http://localhost:5000 into Chromium, the browser sent a request to your Pi. Your six lines of Python caught it, and the return line was the response.
Netflix does the same thing. So does Instagram, so does your school's grade portal. They have more lines of code and more computers, but the loop is identical: wait, request, response, repeat.
The @app.route("/") line is the part that decides which request. / means the front page. Next you will add a second route, and the server will start answering two different questions.
Step 6: Build the real thing
Stop your server with Ctrl+C. You are replacing the whole file, so throw the old one away and open a blank one. Run these two, one at a time:
rm ~/YOURNAME/pi-dashboard/server.py
nano ~/YOURNAME/pi-dashboard/server.py
The nano screen is empty. Click Copy on the code below, click inside the Terminal, and paste with Ctrl+Shift+V:
from flask import Flask, jsonify, send_from_directory
import psutil
import subprocess
import os
STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
app = Flask(__name__)
@app.route("/")
def home():
return send_from_directory(STATIC, "index.html")
@app.route("/<path:filename>")
def other_files(filename):
return send_from_directory(STATIC, filename)
def temperature_c():
try:
for group in psutil.sensors_temperatures().values():
for reading in group:
if getattr(reading, "current", None) is not None:
return round(float(reading.current), 1)
except Exception:
pass
try:
output = subprocess.check_output(["vcgencmd", "measure_temp"], text=True)
return round(float(output.split("=")[1].split("'")[0]), 1)
except Exception:
return None
@app.route("/api/stats")
def stats():
return jsonify({
"cpu": psutil.cpu_percent(interval=0.2),
"memory": psutil.virtual_memory().percent,
"disk": psutil.disk_usage("/").percent,
"temperature": temperature_c()
})
app.run(host="0.0.0.0", port=5000)
Save: hold Ctrl and tap O, then tap the Enter key. Exit: hold Ctrl and tap X.
/ hands over the file static/index.html. /api/stats hands over four numbers, freshly measured, as text. Anything else gets looked up in your static folder, which is how images and CSS files will work later.Step 7: Write the page
nano static/index.html
Same moves as before: Copy, click inside nano, Ctrl+Shift+V.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pi Health</title>
</head>
<body>
<h1>Raspberry Pi Health</h1>
<p>CPU: <span id="cpu">--</span>%</p>
<p>Memory: <span id="memory">--</span>%</p>
<p>Disk: <span id="disk">--</span>%</p>
<p>Temperature: <span id="temperature">--</span> C</p>
<script>
async function refresh() {
const response = await fetch("/api/stats");
const data = await response.json();
for (const name of ["cpu", "memory", "disk", "temperature"]) {
document.getElementById(name).textContent =
data[name] === null ? "--" : Math.round(data[name]);
}
}
refresh();
setInterval(refresh, 2000);
</script>
</body>
</html>
Save and exit the same way (Ctrl+O, Enter, Ctrl+X), then run it:
python3 server.py
Reload http://localhost:5000 in Chromium.
--? Some Pi models do not report it. Everything else still works. Not your fault, not a bug.Meet Thonny, a second editor
nano lives inside the Terminal. Thonny is a Python editor with windows and buttons, and it is already on your Pi. It colors your code, runs your program with one click, and shows errors in red with the line number. Many programmers learn Python in it first.
Today you learn it. On Day 3 you will use it for the Python edit.
- Stop your server. Click the Terminal and press Ctrl+C. Thonny will run its own copy, and two servers cannot share port 5000.
- Open Thonny: Menu → Programming → Thonny.
- Open your server file. Click File, then Open. Click Home, then YOURNAME, then pi-dashboard, then server.py, then Open.
Point Thonny at your toolbox
Thonny starts with its own Python, and that Python does not have Flask. It is the same problem as ModuleNotFoundError in the Terminal: you are outside your toolbox. Tell Thonny to use the one you built.
- Click Tools, then Options, then the Interpreter tab.
- Choose Alternative Python 3 interpreter or virtual environment.
- In the box, type this path. Thonny does not understand
~, so write out your username. It is on your sticky note.
/home/USERNAME/YOURNAME/pihealth/bin/python3
- Click OK. The Shell restarts.
pihealth in it. If it still shows another path, go back to step 2.Run your server from Thonny
Click the green Run button on the toolbar. The Shell prints the same Running on lines the Terminal did. Open http://localhost:5000 in Chromium.
To stop it, click the red Stop button. Thonny's Stop does what Ctrl+C did.
| Terminal and nano | Thonny | |
|---|---|---|
| Start the server | python3 server.py | Green Run button |
| Stop the server | Ctrl+C | Red Stop button |
| Save | Ctrl+O, Enter | Ctrl+S |
| Paste | Ctrl+Shift+V | Ctrl+V |
| Find | Ctrl+W | Ctrl+F |
http://localhost:5000 while the server runs from Thonny, and stops loading after you click Stop.Address already in use, your server is still running in the Terminal. Stop it there first.Break it on purpose
Programmers spend a lot of time reading errors, and Thonny makes them easy to read. Break your file once while nothing depends on it.
- Scroll to the last line,
app.run(host="0.0.0.0", port=5000). Delete the final). - Click Run.
- Read the red text in the Shell. It names the kind of error and the line number. Click the line link to jump to it.
- Type the
)back, then click Run again.
SyntaxError, fixed it, and the server ran again. Click Stop when you are done.server.py. Thonny can open index.html too, but it will not color it, so the page stays in nano.Before you leave today
- Stop your server. Click the Terminal and press Ctrl+C. If you ran it from Thonny, click the red Stop button instead. Only one server can use port 5000, and the next class needs it.
- Check your sticky note. It should have three things on it: your username, your Pi's IP address and your folder name. Stick it on the monitor.
- Remember where you sat. Day 2 starts at this same Pi. Open the Day 2 tab and begin with Start here.
The other way to do this
Optional
Your page lives in its own file and your Python hands that file over. There is a second way worth seeing once. HTML is just text, and Python is very good at holding text, so you can put the entire page inside the Python file as one long string.
from flask import Flask, render_template_string
app = Flask(__name__)
PAGE = """
<!doctype html>
<html>
<head><title>Pi Health</title></head>
<body>
<h1>My page lives inside the Python file.</h1>
</body>
</html>
"""
@app.route("/")
def home():
return render_template_string(PAGE)
app.run(host="0.0.0.0", port=5000)
render_template_string means "here is a page, as text, send it." Compare that to send_from_directory, which means "here is a filename, go find it and send it."
| HTML in the string | HTML on its own | |
|---|---|---|
| Files to track | 1 | 2 |
| Editing 200 lines of HTML | Painful | Normal |
| Adding a stylesheet or a photo | Awkward | Already works |
| Your editor coloring the HTML | No | Yes |
We used two files because the next project is a whole website with images and a stylesheet, and that is the shape it needs. A one-file server is a real and reasonable thing to build when the page is four lines long.
Day 2
Start here: get back to where you stopped
A new Terminal window knows nothing about yesterday, so every day starts the same way.
- Sit at the same Pi as Day 1. Open the Terminal and check your folder is here:
ls ~Your folder name is not in the list? Wrong Pi. Go back to yours. Do not rebuild. - Use a fresh Terminal window. A new window has forgotten your toolbox and does not know which folder you are in. The next step fixes both.
- Step back into your toolbox and your project. Click Copy, paste, tap Enter:
source ~/YOURNAME/pihealth/bin/activate cd ~/YOURNAME/pi-dashboard
Check you are really back
Look for these three signs before you do anything else. This takes ten seconds and saves most of the problems from last time.
| Check | You should see | If not |
|---|---|---|
| The start of your prompt | (YOURNAME-pihealth) | Run the source line again |
Type pwd, tap Enter | A path ending in YOURNAME/pi-dashboard | Run the cd line again. Check the spelling |
Type ls, tap Enter | server.py and static | Wrong folder or wrong Pi. Run ls ~ |
Then check your IP address. It can change overnight, and Day 2 depends on it:
hostname -I
Different from your sticky note? Cross the old one out and write the new one.
Now start your server and leave this window alone:
python3 server.py
Step 1: Load your dashboard on a different computer
Your server is running. Now walk to a different computer in the lab that is not a Pi. The one you will use for SSH later works fine. Open its web browser, click the address bar, and type your Pi's IP address and port:
http://YOUR-PI-IP:5000
So if your sticky note says 10.0.4.71, you type http://10.0.4.71:5000 and tap the Enter key.
:5000 after the IP address? Still stuck, tell Mr. Krigger.Why 0.0.0.0 was the important part
Look at the last line of server.py:
app.run(host="0.0.0.0", port=5000)
0.0.0.0 means answer requests from anywhere on the network, not just from me. If it said 127.0.0.1 instead, the page would work in Chromium on the Pi and every other computer would get nothing.
| Address | What it means | Works from |
|---|---|---|
http://localhost:5000 | this computer, port 5000 | only the Pi itself |
http://10.0.4.71:5000 | the device at that house number, port 5000 | any device on the network |
Port 5000 is the door number. One computer can run many servers at once as long as each uses a different door.
Step 2: Look at the raw data
Stay on the other computer, in the same browser. You are going to add one thing to the end of the address.
- Click the address bar.
- Type your Pi's address again, with
/api/statson the end. - Tap the Enter key.
http://YOUR-PI-IP:5000/api/stats
You will see one short line of text and nothing else. No colors, no cards:
{"cpu":12.5,"disk":31.2,"memory":24.8,"temperature":47.1}
Refresh the page. The numbers change. That is your server measuring itself, fresh, every time something asks.
Step 2b: Take the screenshot now
You need a screenshot of this page for your hand-in. Take it while it is on screen, so you are not hunting for it on Day 3.
- Take a whole-screen screenshot. The shortcuts for each kind of computer are in the Turn in tab.
- Save it as
api-stats.png. - Open it and check: can you read your Pi's IP address in the address bar and the line of data underneath?
/api/stats, and the page is one line of data.api-stats.png is saved, and you can read the address bar in it.API and JSON
An API is a door on a server that hands back data instead of a web page.
Think about ordering food. You do not walk into the kitchen. You tell a waiter what you want, the waiter goes to the kitchen, and the waiter brings back a plate. You never see how the kitchen works and you do not need to.
/api/stats is the waiter. Your web page is you. The Python function stats() is the kitchen.
JSON is what is on the plate. It is just text, arranged so a program can read it:
{"cpu": 12.5, "memory": 24.8}
A name, a colon, a value, commas between them, curly braces around the whole thing. The whole format, start to finish. Almost every app on your phone is passing JSON back and forth all day.
Now look at your page again. These three lines are the whole conversation:
const response = await fetch("/api/stats"); // ask the waiter
const data = await response.json(); // take the plate
setInterval(refresh, 2000); // do it again in 2 seconds
Step 3: Connect to your Pi from a different computer
SSH lets you type commands on your Pi while sitting at a different machine. No monitor, no keyboard on the Pi at all.
Go to one of the lab computers. Open its terminal. Type this, using your username and your Pi's IP:
ssh YOUR-USERNAME@YOUR-PI-IP
For example:
ssh krigger@10.0.4.71
The first time, it will ask something like Are you sure you want to continue connecting? Type yes and tap the Enter key. Then it asks for your Pi password.
Steps 4 and 5: Prove it, then start the server remotely
Still in the SSH window, run:
hostname
uptime
The first prints your Pi's name. The second prints how long it has been switched on. Neither of those is the computer you are sitting at. To leave:
exit
Address already in use, because only one server can use port 5000.Now SSH back in and start your server without touching the Pi:
source ~/YOURNAME/pihealth/bin/activate
cd ~/YOURNAME/pi-dashboard
python3 server.py
Then open a browser on the lab computer and go to your Pi's address. You just started a server on a computer across the room and loaded its page. Plenty of people do exactly this for a living.
What SSH is
SSH stands for Secure Shell. Two halves:
- Shell is the thing that reads your typed commands. The Terminal is a shell.
- Secure means everything you type is scrambled on the way over, so anyone watching the network sees noise.
That second half is the whole reason it exists. Before SSH, people used a tool called Telnet that sent passwords across the network in plain readable text. If you were on the same network, you could just read them.
This is how IT staff fix servers they will never physically see, how your school's tech department reaches every machine in the building from one desk, and how anyone who runs a game server does anything at all.
Before you leave today
- Check you have
api-stats.png. If you do not, load/api/statsonce more and take it now. - Stop your server with Ctrl+C, then close the SSH window by typing
exitand tapping Enter. - Update your sticky note if your IP address changed today.
- Tomorrow: same Pi. Open the Day 3 tab and begin with Start here.
Day 3
Start here: get back to where you stopped
Same start as Day 2. Only the page changes today.
- Same Pi, fresh Terminal. Your folder should be on it.
- Step back in. Click Copy, paste, tap Enter:
source ~/YOURNAME/pihealth/bin/activate cd ~/YOURNAME/pi-dashboard - Check the three signs. Prompt starts with
(YOURNAME-pihealth).pwdends inYOURNAME/pi-dashboard.lsshowsserver.pyandstatic. - Prove yesterday's work runs. Start it, open
http://localhost:5000in Chromium, see four numbers, then Ctrl+C:python3 server.py
Step 1: Start the page over
Nothing about the Python changes today. The server already works. You are only replacing the page it hands out. Real web work usually goes this way, with the thing underneath staying put while the thing people see gets redesigned.
Make sure your server is stopped (Ctrl+C). First keep a copy of yesterday's page, just in case. Then throw the old one away and open a blank one:
cp ~/YOURNAME/pi-dashboard/static/index.html ~/YOURNAME/pi-dashboard/static/index-day2.html
rm ~/YOURNAME/pi-dashboard/static/index.html
nano ~/YOURNAME/pi-dashboard/static/index.html
You now have an empty file. Nothing to select, nothing to accidentally delete half of.
Step 2: Paste the new page
Click Copy to grab the whole page, click inside the empty nano window, and press Ctrl+Shift+V. Then save and exit (Ctrl+O, tap Enter, Ctrl+X).
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pi Health</title>
<style>
/* =====================================================================
THEME BLOCK. Everything you need to restyle this whole page lives
between these two lines. Change a value, save, refresh.
===================================================================== */
:root {
--bg-top: #0f2027;
--bg-bottom: #2c5364;
--text: #ffffff;
--muted: rgba(255, 255, 255, 0.70);
--card: rgba(255, 255, 255, 0.12);
--card-edge: rgba(255, 255, 255, 0.18);
--track: rgba(255, 255, 255, 0.15);
--ok: #4ade80;
--warn: #facc15;
--hot: #f87171;
--round: 16px;
--font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
/* ================================================================== */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
padding: 32px 20px;
color: var(--text);
font-family: var(--font);
background: linear-gradient(135deg, var(--bg-top), var(--bg-bottom));
}
h1 { margin: 0 0 4px; font-size: 28px; }
.sub { margin: 0 0 28px; color: var(--muted); font-size: 14px; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 18px;
max-width: 960px;
}
.card {
padding: 20px;
border-radius: var(--round);
border: 1px solid var(--card-edge);
background: var(--card);
backdrop-filter: blur(10px);
transition: transform 0.15s ease;
}
.card:hover { transform: translateY(-4px); }
.card h2 { margin: 0 0 12px; font-size: 15px; font-weight: 600; color: var(--muted); }
.value { font-size: 42px; font-weight: 700; line-height: 1; }
.unit { font-size: 16px; font-weight: 400; color: var(--muted); margin-left: 2px; }
.bar {
margin-top: 14px;
height: 8px;
border-radius: 999px;
background: var(--track);
overflow: hidden;
}
.fill {
width: 0%;
height: 100%;
border-radius: 999px;
background: var(--ok);
transition: width 0.4s ease, background 0.4s ease;
}
.warn .fill { background: var(--warn); }
.warn .value { color: var(--warn); }
.hot .fill { background: var(--hot); }
.hot .value { color: var(--hot); }
</style>
</head>
<body>
<h1>🖥️ Raspberry Pi Health</h1>
<p class="sub">Live from my Pi. Updates every 2 seconds.</p>
<div class="grid">
<div class="card" id="card-cpu">
<h2>⚡ CPU Usage</h2>
<div><span class="value" id="cpu">--</span><span class="unit">%</span></div>
<div class="bar"><div class="fill" id="bar-cpu"></div></div>
</div>
<div class="card" id="card-memory">
<h2>🧠 Memory Used</h2>
<div><span class="value" id="memory">--</span><span class="unit">%</span></div>
<div class="bar"><div class="fill" id="bar-memory"></div></div>
</div>
<div class="card" id="card-disk">
<h2>💽 Disk Used</h2>
<div><span class="value" id="disk">--</span><span class="unit">%</span></div>
<div class="bar"><div class="fill" id="bar-disk"></div></div>
</div>
<div class="card" id="card-temperature">
<h2>🔥 Temperature</h2>
<div><span class="value" id="temperature">--</span><span class="unit">°C</span></div>
<div class="bar"><div class="fill" id="bar-temperature"></div></div>
</div>
</div>
<script>
const CARDS = [
{ key: "cpu", max: 100, warn: 50, hot: 80 },
{ key: "memory", max: 100, warn: 60, hot: 85 },
{ key: "disk", max: 100, warn: 70, hot: 90 },
{ key: "temperature", max: 90, warn: 60, hot: 75 }
];
async function refresh() {
const response = await fetch("/api/stats");
const data = await response.json();
for (const card of CARDS) {
const reading = data[card.key];
const box = document.getElementById("card-" + card.key);
const value = document.getElementById(card.key);
const fill = document.getElementById("bar-" + card.key);
if (reading === null || reading === undefined) {
value.textContent = "--";
fill.style.width = "0%";
continue;
}
value.textContent = Math.round(reading);
fill.style.width = Math.min(100, (reading / card.max) * 100) + "%";
box.classList.remove("warn", "hot");
if (reading >= card.hot) {
box.classList.add("hot");
} else if (reading >= card.warn) {
box.classList.add("warn");
}
}
}
refresh();
setInterval(refresh, 2000);
</script>
</body>
</html>
Save and exit (Ctrl+O, tap Enter, Ctrl+X), then run it:
source ~/YOURNAME/pihealth/bin/activate
cd ~/YOURNAME/pi-dashboard
python3 server.py
The three new things you just used
Gradient. Instead of one flat color, a smooth blend between two:
background: linear-gradient(135deg, #0f2027, #2c5364);
135deg is the angle. The two # codes are hex colors, six characters that mix red, green and blue.
Grid. Four cards that rearrange themselves depending on screen width:
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
This says "make as many columns as fit, each at least 220px wide." Drag your browser window narrower and watch four columns become two and then one. Nobody wrote a rule for phones. That one line did it. This is called responsive design.
Classes as switches. The color change is not JavaScript drawing anything. The CSS already knows what .hot looks like. JavaScript just adds and removes the label:
box.classList.add("hot");
That split matters. CSS handles how things look, JavaScript handles when, and keeping the two apart is what lets a big site stay workable once there are thousands of lines of it.
Step 3: Make it yours
Now the page is yours to change. A good way to work, so you never lose a dashboard that works:
Use two Terminal windows
| Window | Its job |
|---|---|
| Terminal 1 | Runs your server. Start it, then leave it alone. Do not close it and do not type in it. |
| Terminal 2 | Opens nano so you can edit the page. Click the black rectangle icon again to get a second window. It does not need your toolbox. |
Start Terminal 1 now with python3 server.py if it is not already running.
The loop: change, save, refresh
- Terminal 2: open the page. The
-lshows line numbers down the side. - Change one thing. Arrow keys move the cursor. One change at a time, so you know what did what.
- Save and exit: Ctrl+O, tap Enter, Ctrl+X
- Browser: press Ctrl+Shift+R to reload without the old copy.
nano -l ~/YOURNAME/pi-dashboard/static/index.html
server.py: click Terminal 1, press Ctrl+C, run python3 server.py again.Your undo button
Before each big change, save a copy that works. If it goes wrong, put the copy back. You cannot lose your dashboard this way.
cp ~/YOURNAME/pi-dashboard/static/index.html ~/YOURNAME/pi-dashboard/static/index-works.html
cp ~/YOURNAME/pi-dashboard/static/index-works.html ~/YOURNAME/pi-dashboard/static/index.html
The map of your page
Your page is one file with four zones. Every change in Step 3 happens in one of them, so this is the map. The line numbers are about right. They drift as you add things, which is why the next slide teaches you to search instead of counting.
| About line | What is there | You edit it for |
|---|---|---|
| 12 to 25 | Zone 1: the theme block. The :root { ... } lines with all the colors | Level 1, Level 2, and the --font line |
| 26 to 82 | Zone 2: the rest of the CSS. How cards, bars and text look | Mostly leave alone |
| 83 | </style>, the end of the CSS | New Level 3 CSS goes on the line above this one |
| 87 to 116 | Zone 3: the HTML. The title, then the four cards | Clock, the big card, a fifth card |
| 118 to 156 | Zone 4: the JavaScript. const CARDS first, then refresh() | Warning levels, the last-updated stamp, the clock |
Find things fast in nano
Scrolling for a line is slow. nano can jump straight to it.
- Hold Ctrl and tap W. The bottom says
Search: - Type the word from the table, then tap the Enter key. The cursor jumps there.
- Wrong one? Ctrl+W then Enter jumps to the next match.
| To get to | Search for |
|---|---|
| The colors and the corner roundness | --bg-top or --round |
| The font | --font |
| The end of the CSS | </style> |
| The title at the top of the page | <h1> |
| The start of the JavaScript | <script> |
| The warning levels | CARDS |
| The end of the JavaScript | </script> |
Level 1: The theme block
--bg-top, tap Enter. You land on the first line of the theme block.Look at the top of your CSS. Everything between the two long comment lines is the theme. Change a value there and it changes everywhere on the page at once, because every rule below reads from it instead of having its own copy of the color.
| Variable | What it controls |
|---|---|
--bg-top and --bg-bottom | The two colors the background fades between |
--text | Every piece of normal text |
--muted | Labels and units. Dimmer than --text on purpose |
--card | The card's own fill. rgba because it is see-through |
--card-edge | The thin line around each card |
--track | The empty part of each bar |
--ok, --warn, --hot | Green, yellow and red |
--round | How rounded the corners are. 0px is sharp, 28px is a pill |
--font | The typeface for the whole page |
Try this first, so you can see how far one variable reaches:
--round: 0px;
Save, then refresh the browser. Every card went square. You changed one number.
Level 2: How to swap in a theme pack
Each pack replaces your whole :root block. Copy one in, refresh, look at it. Then take the one you like best and start changing its numbers.
:root, tap Enter. The cursor is now on the :root { line. Tap Ctrl+K 14 times, once per line, until the whole block is gone and the two comment lines are still above and below the gap. Then click Copy on a pack and press Ctrl+Shift+V.Theme packs: Terminal and Midnight
Terminal. Sharp corners, monospace, the look of a machine that does not care about your feelings.
:root {
--bg-top: #000000;
--bg-bottom: #0a1a0a;
--text: #d1fae5;
--muted: rgba(209, 250, 229, 0.55);
--card: rgba(34, 197, 94, 0.07);
--card-edge: rgba(34, 197, 94, 0.30);
--track: rgba(34, 197, 94, 0.15);
--ok: #22c55e;
--warn: #eab308;
--hot: #ef4444;
--round: 4px;
--font: "Courier New", monospace;
}
Midnight. Purple, soft, easy on the eyes in a dark room.
:root {
--bg-top: #16162e;
--bg-bottom: #4a1f6b;
--text: #f3e8ff;
--muted: rgba(243, 232, 255, 0.65);
--card: rgba(255, 255, 255, 0.10);
--card-edge: rgba(216, 180, 254, 0.28);
--track: rgba(255, 255, 255, 0.14);
--ok: #86efac;
--warn: #fde047;
--hot: #fb7185;
--round: 20px;
--font: system-ui, sans-serif;
}
Theme packs, continued
Volcano. For a Pi that runs hot and wants you to know it.
:root {
--bg-top: #1c0a08;
--bg-bottom: #57180f;
--text: #fff7ed;
--muted: rgba(255, 247, 237, 0.62);
--card: rgba(255, 255, 255, 0.09);
--card-edge: rgba(255, 170, 120, 0.25);
--track: rgba(255, 255, 255, 0.13);
--ok: #7ee787;
--warn: #fbbf24;
--hot: #ff5c4d;
--round: 14px;
--font: system-ui, sans-serif;
}
Sea Glass. Teal and calm. This one looks the most like something a company would ship.
:root {
--bg-top: #042f2e;
--bg-bottom: #115e59;
--text: #ecfeff;
--muted: rgba(236, 254, 255, 0.66);
--card: rgba(255, 255, 255, 0.11);
--card-edge: rgba(153, 246, 228, 0.26);
--track: rgba(255, 255, 255, 0.14);
--ok: #5eead4;
--warn: #fcd34d;
--hot: #fb7185;
--round: 18px;
--font: system-ui, sans-serif;
}
Theme packs, last two
Slate. Grey, quiet, professional. The boring one, and boring is sometimes correct.
:root {
--bg-top: #1e293b;
--bg-bottom: #334155;
--text: #f1f5f9;
--muted: rgba(241, 245, 249, 0.60);
--card: rgba(255, 255, 255, 0.07);
--card-edge: rgba(255, 255, 255, 0.14);
--track: rgba(255, 255, 255, 0.12);
--ok: #4ade80;
--warn: #facc15;
--hot: #f87171;
--round: 10px;
--font: system-ui, sans-serif;
}
Paper. A light theme. Look closely at this one: the text went dark, the cards went white, and the transparent values all flipped. Nothing outside the theme block changed.
:root {
--bg-top: #f8fafc;
--bg-bottom: #dbe3ec;
--text: #0f172a;
--muted: rgba(15, 23, 42, 0.60);
--card: rgba(255, 255, 255, 0.75);
--card-edge: rgba(15, 23, 42, 0.12);
--track: rgba(15, 23, 42, 0.10);
--ok: #16a34a;
--warn: #ca8a04;
--hot: #dc2626;
--round: 14px;
--font: system-ui, sans-serif;
}
Why nearly every one of these is dark
Your page has to show green, yellow and red and have all three read clearly. Those three colors have nowhere to go on a bright orange or hot pink background, they just turn to mud. Real dashboards, the ones air traffic controllers and network engineers stare at all day, are almost always dark for this reason.
If you build a bright theme anyway, check that a red card still shouts at you from across the room. If it does not, the theme is pretty and useless.
Build your own palette at coolors.co. Pick two close colors for the background and keep your green, yellow and red bright.
Level 3: A real typeface
<style>. Search <style>, tap Home, paste, tap Enter. (2) The --font line is already in your theme block. Search --font and change it.Free fonts from Google Fonts. Pick one, then add one line in your <head>, above <style>:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap">
And point the theme at it:
--font: "Space Grotesk", system-ui, sans-serif;
Swap the font name for any from the site, with + instead of spaces. Keep system-ui, sans-serif on the end. It is the backup, covering the two seconds before the font arrives, or forever if the Wi-Fi is down.
Level 3: A background that moves
</style>, tap Home, paste, tap Enter.Now a background that moves:
@keyframes drift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
body {
background: linear-gradient(135deg, var(--bg-top), var(--bg-bottom), var(--bg-top));
background-size: 400% 400%;
animation: drift 18s ease infinite;
}
The gradient is stretched to four times the screen and then slid slowly back and forth. Change 18s to 4s to see it clearly, then put it back to something calm. A background that races is a background nobody can read in front of.
Level 3: Glowing numbers
</style>. Search </style>, tap Home, paste, tap Enter. Your page already has a .value rule. Leave it alone. A second one at the bottom adds to it.Make the numbers glow:
.value {
text-shadow: 0 0 18px currentColor;
}
currentColor means "whatever color this text already is." So the green numbers glow green, and the moment a card goes red the glow goes red too. One line, and it follows your thresholds for free.
Level 3: A pulsing alarm
</style>.Make a hot card pulse:
@keyframes alarm {
0%, 100% { box-shadow: 0 0 0 0 rgba(248, 113, 113, 0.55); }
50% { box-shadow: 0 0 0 14px rgba(248, 113, 113, 0); }
}
.card.hot {
animation: alarm 1.4s ease-out infinite;
}
A ring pushes outward from the card and fades. It only runs on cards your JavaScript has marked hot, so most of the time you will never see it. Good alarms are quiet. Open six browser tabs to drive the CPU up and watch it fire.
Level 3: A clock
Edit 1, HTML. Search <h1>. Tap End, tap Enter, paste:
<p class="sub">Live from my Pi · <span id="clock">--:--:--</span></p>
Edit 2, JavaScript. Search </script>. Tap Home, paste, tap Enter:
function tick() {
document.getElementById("clock").textContent =
new Date().toLocaleTimeString();
}
tick();
setInterval(tick, 1000);
This clock is the browser's time, not the Pi's. It never asks the server anything.
Level 3: When the data last arrived
Edit 1, HTML. Search <script>. Tap Home, paste, tap Enter. It lands just under the cards:
<p class="sub" id="stamp">Waiting for the first reading.</p>
Level 3: The stamp, edit 2
refresh(), in the JavaScript.Edit 2, JavaScript. This one goes inside refresh(), after the for loop closes. Here is the end of refresh() so you can find the spot:
} else if (reading >= card.warn) {
box.classList.add("warn");
}
} <- this closes the for loop
<- paste the two stamp lines here
} <- this closes refresh()
Click at the end of the } that closes the for loop, tap Enter, paste:
document.getElementById("stamp").textContent =
"Last updated " + new Date().toLocaleTimeString();
Now you can tell "everything is fine" from "the server died three minutes ago." Every monitoring tool has this.
Level 3: One big card
</style>, tap Home, paste, tap Enter. Your .grid rule is already in the page, so you only add these two.Give the page a main character instead of four things shouting equally:
#card-cpu {
grid-column: span 2;
}
#card-cpu .value {
font-size: 72px;
}
CPU now takes two columns and a much bigger number. Want a different card to be the big one? Change cpu to memory, disk or temperature in both places.
Level 3: A photo behind everything
static folder, then add CSS. Part 2 is on the next slide.Part 1, get the photo. Find one at unsplash.com. In Chromium, right-click the photo and choose Save image as. Browse to Home, then your folder, then pi-dashboard, then static. Name it exactly pi-background.jpg and save. Check it landed:
ls ~/YOURNAME/pi-dashboard/static
Level 3: The photo, part 2
</style>.Part 2, the CSS. Search </style>, tap Home, paste, tap Enter:
body {
background-image:
linear-gradient(135deg, rgba(15, 32, 39, 0.85), rgba(44, 83, 100, 0.85)),
url("/pi-background.jpg");
background-size: cover;
background-position: center;
background-attachment: fixed;
}
Two backgrounds stacked: your gradient on top, at 85% opacity, and the photo under it. Without that gradient layer the photo eats your text.
/pi-background.jpg has to match the real file exactly, lowercase and all. Your server hands out anything in static, so the path starts with a single /.Level 3: Your own warning levels
CARDS, tap Enter.In the CARDS list at the top of your JavaScript:
{ key: "cpu", max: 100, warn: 50, hot: 80 },
Set warn to 5, save, and refresh. The card sits in yellow permanently, forever, no matter what the Pi is doing.
An alarm that is always on is worse than no alarm, because people stop looking at it. Real engineers argue about these two numbers for hours. Pick yours on purpose and be ready to say why in your reflection.
Level 3: Everything else
- Swap the emoji in the
<h2>lines - Put your name, or your Pi's name, in the
.subline - Change
gap: 18pxtogap: 4pxand thengap: 40px - Change
translateY(-4px)in.card:hovertoscale(1.04) - Delete a card you do not care about. Delete its line from
CARDStoo, or the JavaScript will look for something that is not there
Base and Stretch
Base · everyone
A working server on your Pi, serving your own dashboard, loading on a computer that is not the Pi, with at least two visible changes that are yours. You can explain what /api/stats sends back and who asks for it.
Stretch · required for high school
Add a fifth stat, wired all the way through. Not a new color. A new number that does not exist yet, travelling from the Pi's hardware to the screen. Three edits, one in each layer: Python, HTML, JavaScript.
Stretch edit 1: Python
server.py, not the page. Open it in Terminal 2 with nano -l ~/YOURNAME/pi-dashboard/server.py, or in Thonny (below).server.py in your pi-dashboard folder. Search with Ctrl+F instead of Ctrl+W, and paste with Ctrl+V. A missing comma shows up as a red SyntaxError with a line number when you click Run. Not set up yet? The Interpreter step is on Day 1.1. At the top, add one line under import os. Search import os, tap End, tap Enter, paste:
import time
Stretch edit 1, continued: the jsonify block
server.py, still open in Terminal 2.2. In the jsonify block, the old last line needs a comma added, and your new line goes under it with no comma:
"disk": psutil.disk_usage("/").percent,
"temperature": temperature_c(),
"uptime": round((time.time() - psutil.boot_time()) / 60)
})
"uptime": round((time.time() - psutil.boot_time()) / 60)
3. server.py changed, so this one does need a restart. In Terminal 1 press Ctrl+C, then run python3 server.py. In Thonny, click the red Stop button, then the green Run button. Reload /api/stats on the other computer.
SyntaxError in Terminal 1 almost always means a missing comma after temperature_c().Stretch edit 2: HTML
2. HTML. Add a fifth card. Search bar-temperature, tap the Down arrow once (that is the line closing the temperature card), tap End, tap Enter, paste:
<div class="card" id="card-uptime">
<h2>⏱️ Uptime</h2>
<div><span class="value" id="uptime">--</span><span class="unit">min</span></div>
<div class="bar"><div class="fill" id="bar-uptime"></div></div>
</div>
Making a different stat? Change uptime to your stat's name in all three ids, and change the min unit.
Stretch edit 3: JavaScript
CARDS list in your page, in the JavaScript. Search CARDS.3. JavaScript. Search CARDS. The old last line needs a comma, and the new line goes under it with no comma:
{ key: "disk", max: 100, warn: 70, hot: 90 },
{ key: "temperature", max: 90, warn: 60, hot: 75 },
{ key: "uptime", max: 1440, warn: 720, hot: 1200 }
];
max is where the bar is full. Uptime is in minutes, so 1440 is a full day. Your new key has to match the name you used in server.py exactly.
What you turn in
- Your two files,
server.pyandstatic/index.html dashboard.png: your finished dashboard on a computer that is not the Pi, with your Pi's IP in the address barapi-stats.png: the raw data from/api/stats, with your Pi's IP and/api/statsin the address bar- A reflection, 4 to 6 sentences, answering all three questions below
The reflection has to answer: what does /api/stats send back and what asks for it, how often does the page ask and which line decides that, and what did you change and what was the hardest part.
High school, and middle school going for bonus: name the stat you added and say which two files you had to edit to make it appear.
Everything is submitted in Schoology, along with a separate exit ticket.
Taking the two screenshots
Both screenshots are taken on the other computer, not on the Pi, because the address bar has to show your Pi's IP.
| File name | Address bar shows | Page shows | Take it |
|---|---|---|---|
api-stats.png | YOUR-PI-IP:5000/api/stats | One line of data | Day 2, as soon as it works |
dashboard.png | YOUR-PI-IP:5000 | Your finished dashboard | End of Day 3, after your changes |
Capture the whole screen, not a piece of it. A whole-screen capture always has the address bar in it. Cropping is how screenshots end up proving nothing.
| Computer | Whole-screen shortcut | It saves to |
|---|---|---|
| Windows | Windows + Print Screen | Pictures, then Screenshots |
| Mac | Cmd + Shift + 3 | The Desktop |
| Chromebook | Ctrl + the Show windows key (a rectangle with two lines, top row) | Downloads |
| Raspberry Pi | Print Screen. If nothing happens: Menu → Accessories → Screenshot | Your home folder, or a small capture window opens |
dashboard.png or api-stats.png, then open it and check you can read the address bar.Getting your files to Schoology: pick a route
Your two files are where you made them, on the Pi:
~/YOURNAME/pi-dashboard/server.py
~/YOURNAME/pi-dashboard/static/index.html
Your screenshots are on the other computer. The files have to get from the Pi to Schoology somehow. There are three ways. Try Route 1 first.
| Route | What happens | Logins |
|---|---|---|
| 1. Schoology on the Pi | Log in to Schoology on the Pi and upload the two files from there. | Two: the Pi, then the other computer for the screenshots |
| 2. Email yourself | Email the two files to yourself from the Pi, then open that email on another computer. | Email on the Pi, then Schoology on the other computer |
| 3. Download from the Pi | Your Pi shares your folder for a minute. You save the files on the other computer. | One: Schoology on the other computer |
Last resort: copy the pi-dashboard folder to a jump drive and submit from a lab computer.
Route 1: Schoology on the Pi
- Open Chromium on the Pi and press Ctrl+Shift+N. That opens a Private window (Chromium calls it Incognito). It forgets your login when you close it.
- Log in to Schoology. If Chromium offers to save the password, click Never. Do not tick any box that says remember me.
- Open the assignment, click Submit Assignment, then Upload.
- Pick
server.pyfrom Home → YOURNAME → pi-dashboard. Uploadindex.htmlfrom thestaticfolder inside it. - Check both names are in the list, then submit.
Then log in on the other computer and upload the two screenshots there.
Route 2: Email yourself
- On the Pi, press Ctrl+Shift+N in Chromium for a Private window.
- Log in to your email. If Chromium offers to save the password, click Never.
- Write a new message to yourself. Click the paperclip and attach
server.pyfrompi-dashboard, then attachindex.htmlfrom thestaticfolder inside it. - Send it. Wait until your email says the message was sent.
- Log out of your email, then close the Private window. Do this before you do anything else.
- On the other computer, open the email, download both files, and upload them to Schoology along with your screenshots.
Route 3: Download from the Pi
Your Pi can hand out your files the same way it hands out your dashboard. Nothing to log in to on the Pi.
On the Pi, in the Terminal:
cd ~/YOURNAME/pi-dashboard && python3 -m http.server 8001
It prints a line like this, then sits there listening:
Serving HTTP on 0.0.0.0 port 8001 (http://0.0.0.0:8001/) ...
On the other computer, go to this address with your Pi's IP:
http://YOUR-PI-IP:8001
- You see a list with
server.pyandstatic/. - Right-click
server.pyand choose Save link as. - Click
static/, then right-clickindex.htmland choose Save link as.
Upload the files and your screenshots to Schoology from the other computer. One login covers everything.
Words worth knowing
| Term | What it means |
|---|---|
| Terminal | A window where you type commands instead of clicking. Same computer, different door |
| Prompt | The text at the start of a Terminal line, waiting for you. Yours should start with (YOURNAME-pihealth) |
| nano | A text editor that runs inside the Terminal. Save with Ctrl+O, exit with Ctrl+X |
| Server | A computer whose job is to wait for requests and send back responses |
| Request | A device asking a server for something |
| Response | What the server sends back |
| Flask | The Python tool that turns a Python file into a web server |
| Route | A line like @app.route("/api/stats") saying which request a function answers |
| API | A door on a server that hands back data instead of a web page |
| JSON | A text format for data that programs can read, like {"cpu": 45} |
| IP Address | A device's house number on a network, like 10.0.4.71 |
| Port | The door number on that house. Ours is 5000 |
| localhost | A name that always means "this computer, right here" |
| SSH | Secure Shell. Typing commands on one computer that run on another |
| HTML | The structure of a page. What is on it |
| CSS | The style of a page. What it looks like |
| JavaScript | The behavior of a page. What it does and when |
| DOM | The browser's live copy of your HTML, which JavaScript can change |
| Virtual Environment | A clean workspace holding one project's Python tools |
| psutil | The Python library that reads the computer's own CPU, memory and temperature |
| Full-Stack | Working on both the server and the page. You did both |
When it goes wrong
ModuleNotFoundError: No module named 'flask'
(YOURNAME-pihealth)? If not, run source ~/YOURNAME/pihealth/bin/activate. You need this in every new Terminal window.Address already in use
pkill -f YOURNAME/pi-dashboard/server.py and start again. Never run plain pkill -f server.py. It stops every student’s server on this Pi, not just yours.The page says Not Found.
ls ~/YOURNAME/pi-dashboard/static. If index.html is not in that list, you saved it in the wrong folder.The other computer cannot load my dashboard.
:5000 after the IP address? Check hostname -I on the Pi too. The IP can change overnight.It loads someone else's dashboard.
hostname -I on your Pi again and fix your sticky note.Temperature shows two dashes.
I changed the page and nothing happened.
[ Wrote ... lines ] when it saves. Then hard refresh with Ctrl+Shift+R. If you changed server.py, that one needs a restart: Ctrl+C in the server's Terminal, then python3 server.py.Nothing appears when I type my password.
I pasted into nano and it came out mangled.
When it goes wrong, continued
My prompt does not start with (YOURNAME-pihealth).
source ~/YOURNAME/pihealth/bin/activate. A new Terminal window always starts outside it.No such file or directory.
ls ~ and look for your folder name. Not there? Find your Pi. Do not rebuild.nano says File Name to Write and I do not know what to do.
I pressed Ctrl+X and nano asks Save modified buffer?
Ctrl+C did not copy my text.
The Terminal looks frozen after python3 server.py.
My dashboard stopped updating.
python3 server.py again.Thonny says No module named 'flask'.
/home/USERNAME/YOURNAME/pihealth/bin/python3 with your own username and folder name. Click OK.Thonny says Address already in use.
I cannot see the Shell in Thonny.
Running on and your errors.My email will not take a file, or says it was blocked.
I broke it so badly I want to start over.
rm ~/YOURNAME/pi-dashboard/server.py and redo Step 6. This is a normal thing that normal programmers do.Appendix
Setting up a Pi from a blank card
Skip this unless Mr. Krigger tells you to do it. The Pis in the lab are already set up, and Day 1 assumes yours boots to a desktop. This appendix is here for the times it does not: a brand new Pi, a card that got wiped, or a class doing the whole build from scratch.
There are two walkthroughs below because the lab runs two versions of Raspberry Pi OS. Find out which one you need before you start. If the Pi still boots, open a Terminal and run:
cat /etc/os-release
Look at the VERSION_CODENAME line. It says either bookworm or trixie. If the card is blank and there is nothing to boot, use the Bookworm walkthrough unless you were told otherwise.
Version A: Bookworm
This is the version most of the lab is running.
A1. Get the Imager. On any Mac or Windows computer, download and install Raspberry Pi Imager. Put the microSD card into that computer.
A2. Choose the three things. Imager asks three questions. Choose Device, pick the Pi model you are holding, printed on the board. Choose OS, Bookworm is no longer the default so it is a few clicks in: Raspberry Pi OS (other), then the 64-bit Bookworm entry. Choose Storage, pick your SD card.
A3: Fill in OS customisation
Imager asks whether you want to customise before writing. Say yes. This is the whole reason setup is easy now.
| Field | What to put |
|---|---|
| Hostname | Something you will recognize, like krigger-pi |
| Username | Your choice. Write it down. There is no default anymore |
| Password | Your choice. Write it down |
| Wireless LAN | The network name and password Mr. Krigger gives you |
| Wireless LAN country | US |
| Locale and time zone | Whatever the school is on |
Then open the Services tab and turn on Enable SSH, with password authentication.
wpa_supplicant.conf to join Wi-Fi. That has not worked since 2023.A4 to A7: Write, boot, update, confirm
A4. Click Write and wait. This takes several minutes, then verifies what it wrote, which takes several more. Leave it alone.
A5. Put the card in the Pi, connect the monitor, keyboard and mouse, then plug in the power last. The first boot takes longer than normal and the Pi may restart itself once. Let it.
A6 and A7: Update it, then confirm
A6. Open the Terminal and run:
sudo apt update && sudo apt full-upgrade -y
sudo reboot
The first command can take fifteen minutes on a fresh card, longer if a whole class is doing it at once. sudo means run as administrator, apt update checks what new versions exist, full-upgrade -y installs them without asking you to confirm each one, and reboot restarts so the updates take effect.
A7. Confirm it is ready for Day 1:
whoami
hostname -I
The first prints the username you chose in Imager. The second prints an IP address. If either comes back empty or wrong, fix it now rather than in the middle of Day 1.
Version B: Trixie
Trixie is the newer Raspberry Pi OS, built on Debian 13. Some of the lab machines run it.
B1. Everything in A1 through A4 is the same. Same Imager, same three questions, same customisation screen, same Write button. One difference: at Choose OS, Trixie is the default now, so it is the plain Raspberry Pi OS (64-bit) entry at the top. You do not need to go into "other".
B2. First boot is the same as A5. The desktop looks different from Bookworm, redrawn, so the icons and wallpaper will not match what the student next to you is looking at. Nothing about this project changes because of it.
B3. Update, identical to A6:
sudo apt update && sudo apt full-upgrade -y
sudo reboot
B4. Confirm, identical to A7. Run whoami and hostname -I and write both down.
Where things are on each desktop
| What | Bookworm | Trixie |
|---|---|---|
| Terminal | The black rectangle icon on the top bar | |
| Thonny (Python editor) | Menu → Programming → Thonny | |
| SSH on or off | Menu → Preferences → Raspberry Pi Configuration → Interfaces | |
| Screenshot | Print Screen | Print Screen, or Menu → Accessories → Screenshot |
What is actually different between the two
Short answer, for the curious: almost nothing you will touch.
| Bookworm | Trixie | |
|---|---|---|
| Debian version underneath | 12 | 13 |
| Which one Imager offers first | Under "other" | The default |
| How the desktop looks | Older look | Redrawn |
| Terminal, Python, Flask, SSH | Identical | |
| Wi-Fi setup | NetworkManager | |
Default pi / raspberry login | None, on either | |
Every command in Days 1 through 3 runs the same on both, and that was deliberate. Commands stay stable for decades while menus get redesigned every couple of years, so the commands are the part worth learning.