Stage 3 of 6 · Lab · about 25 minutes
Flask: the Pi serves
Flask is Python's standard small web framework: a routing core, a template engine (Jinja), a development server, and deliberately little else. "Micro" is the design, not a limitation, and it is exactly the right size for a Pi serving a bench. Twelve lines from now, your Pi is a web server.
What Flask is
A web framework's job is the plumbing between the socket and your code: parse the request, match the path to a function, turn the function's return into a proper response. Flask, from the Pallets project, does that job and stops, which is why a complete app fits on a slide and why it has carried a generation of dashboards, APIs and course projects. Install it where every Pi-side library has lived since Week 4, the project venv:
(.venv) you@lastname-pi:~/iot-labs $ pip install flask
First serve
# app.py: the smallest real web server
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "<h1>Station s07</h1><p>The Pi is serving.</p>"
app.run(host="0.0.0.0", port=5000)
(.venv) you@lastname-pi:~/iot-labs $ python app.py * Serving Flask app 'app' * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 WARNING: This is a development server. Do not use it in a production deployment.
Read the pieces with the week's theory in hand. Flask(__name__) constructs the application object, Week 6 as ever. The decorator @app.route("/") registers home as the answerer for the root path. The return value is the response body, and Flask fills in the 200 and the headers. The warning banner is honesty, not failure: this built-in server is made for development and benches, and production fronts Flask with a hardened server and TLS, the same scoped-decision language as Week 9's broker. The bench runs the dev server, knows it, and writes it down.
Reachable: a wall you have met before
Two things stand between your server and the rest of the bench network, and you have torn down both before. First, the bind address: host="0.0.0.0" means "listen on all interfaces". Leave it out and Flask binds to loopback only, 127.0.0.1, reachable from the Pi alone, which is exactly Mosquitto's Week 9 wall wearing a Python costume; same concept, same symptom (works in a Pi terminal, refuses everyone else), same fix (bind wider, on purpose). Second, the firewall, and by now the ritual writes itself:
you@lastname-pi:~ $ sudo ufw allow 5000 you@lastname-pi:~ $ sudo ufw status verbose 22/tcp ALLOW IN Anywhere 1883 ALLOW IN Anywhere 5000 ALLOW IN Anywhere
Now the moment: take out your phone, join the bench Wi-Fi, and browse to the Pi's address, port 5000 (the URL anatomy, live). A page you wrote, served by a computer you configured, to a device nobody wired. Keep the phone handy; it is the week's best test instrument, because it shares none of your laptop's state.
Routes: the URL contract
Add routes by adding decorated functions; restrict methods where the verb's contract demands it (methods=["POST"] on anything that acts). One craft note carried over from topic design: keep paths lowercase, hierarchical and stable, with data routes gathered under /api/, so a page, a script and next term's curiosity all find the same contract in the same place.
Templates: HTML with blanks
Pages as Python strings stop scaling at about the second tag. Flask's answer is Jinja templates: real HTML files with {{ placeholders }}, rendered per request. Two rules do most of the work: templates live in a folder named exactly templates/ beside app.py, and render_template fills the blanks from keyword arguments:
# in app.py
from flask import render_template
@app.route("/")
def home():
return render_template("dashboard.html", station="s07")
<!-- templates/dashboard.html -->
<html>
<head><title>Station {{ station }}</title></head>
<body>
<h1>Station {{ station }}</h1>
<p>Light: <span id="pct">–</span> %</p>
</body>
</html>
The blanks fill at serve time on the server; the live numbers will arrive later, in the browser, by JavaScript, and the next page explains that division of labour. Jinja goes much further (loops, conditionals, inheritance) and your project may want it; the reference is in Resources.
The first API route
Pages are for people; dashboards also need routes that answer with data. Flask makes JSON a one-liner, and the format is an old friend, Week 9's structured-payload rung, now crossing the other protocol:
# a data route: answers with JSON, for any client from flask import jsonify @app.route("/api/light") def api_light(): return jsonify(pct=42.3, status="online") # demo values, for now
Browse to it and the browser shows the raw JSON; curl from a terminal shows the same; so will the dashboard's JavaScript. The hard-coded 42.3 is this page's only lie, and the next page replaces it with the truth flowing in from the broker.
Checklist for this stage
Check yourself
Your laptop gets "connection refused" from the Pi's port 5000, while the page loads fine in a browser on the Pi itself. Which of the two walls is this, and why is the symptom decisive?
host="0.0.0.0". A firewall block would drop silently and time out instead, Week 9's timeout-versus-refused reading, reused verbatim.Why is the dev-server warning acceptable on the bench, in the same sentence structure you used for allow_anonymous in Week 9?
What exactly does the @app.route decorator do, in route-table language?
render_template raises TemplateNotFound even though dashboard.html sits right next to app.py. Diagnose.
templates/ beside the app, not beside it in the same directory. Make the folder, move the file in, done. It is a convention-over-configuration rule: one fixed place, zero setup.Why do the live numbers arrive by JavaScript in the browser rather than by {{ pct }} in the template?
{{ pct }} would freeze the value as of the page load, and seeing a new reading would mean reloading the whole page. The division of labour is: template for the page's skeleton and identity, JavaScript asking /api/light for the changing numbers, which is the next page's whole subject.