Flask & the project: the system gets a face
420-302-VA · WEEK 11 · FALL 2026

Stage 2 of 6 · Theory · about 30 minutes

The web, quickly

Week 9 taught one way for programs to talk; this page teaches the other, the one the entire web runs on. HTTP is older, simpler and more asked-about in interviews than anything else in this course, and your dashboard needs exactly the slice presented here: the cycle, the addresses, the verbs, the verdicts, and ten tags of HTML.

The other protocol

HTTP, the Hypertext Transfer Protocol, was created by Tim Berners-Lee at CERN around 1989–91 as the fetching half of the web: a client (then and now, usually a browser) opens a TCP connection to a server, sends one request naming a resource, receives one response carrying it, and that is the whole transaction. Three design choices from that birth still shape everything you build today. It is client-initiated: servers never call browsers; they answer. It is request/response: every exchange is a matched pair, an ask and an answer. And it is stateless: each request stands alone, carrying everything the server needs, with no memory of the last one built into the protocol. That last property is why the web scales to billions of clients, and it is also precisely why your dashboard will need the bridge: a stateless server that is asked "what is the light level?" must have the answer already in hand, because the protocol gives it no way to go and wait for one.

One request, anatomized

One HTTP exchange. The browser sends a request card reading: method GET, path /api/light, protocol HTTP/1.1, and a Host header. The Flask server on the Pi answers with a response card reading: HTTP/1.1, status 200 OK, a Content-Type header of application/json, and a JSON body with the light percentage. Arrows: request right, response left. Browser the client asks Flask on the Pi, the server answers GET /api/light HTTP/1.1 Host: 192.168.1.42:5000 method · path · protocol, then headers; GET carries no body HTTP/1.1 200 OK Content-Type: application/json {"pct": 42.3, "status": "online"} status line, headers, blank line, body
An ask and an answer, both plain text. The request names a method, a path and a host; the response leads with a verdict, declares its body's type, and delivers it. Everything the dashboard does is this exchange, repeated.

Reading a URL

The URL http colon slash slash 192.168.1.42 colon 5000 slash api slash light, segmented and labeled: http is the scheme, the protocol to speak; 192.168.1.42 is the host, which machine, here the Pi from hostname dash capital I; 5000 is the port, which door, Flask's; slash api slash light is the path, which resource, matched by a route. http:// 192.168.1.42 :5000 /api/light scheme which protocol host which machine: the Pi, per hostname -I port which door: Flask's path which resource: matched by a route
Four questions, one line. Protocol, machine, door, resource. Browsers hide :80 (HTTP's default port) the way Week 9 hid nothing: your Pi serves on 5000, so the port must be written. Scheme and host find the server; the path is yours to design.

GET, POST, and the verbs' meaning

The method is the request's verb, and the two you need carry a real semantic contract. GET reads: it should change nothing on the server, which lets browsers cache it, prefetch it and repeat it freely. POST submits: it acts, creates or changes, and clients treat it carefully because repeating it repeats the act. Your dashboard respects the contract exactly: reading the light level is a GET, while setting the loop's target is a POST, because it moves a physical LED. If that "safe to repeat" reasoning feels familiar, it should: it is Week 9's idempotency argument from the QoS table, arrived on the other protocol, and the same design instinct answers both.

Status codes: the server's verdicts

CodeMeaningOn your bench, it means
200 OKServed as askedThe normal case; the body is your answer
404 Not FoundNo resource at that pathA route/path mismatch: a typo, or the stray trailing slash
400 Bad RequestThe request itself was malformedUsually the setpoint form sent something float() rejects
500 Internal Server ErrorThe server's code raisedYour Python has a traceback waiting in the Flask terminal; read it bottom-up, Week 4 style

Just enough HTML

Responses carrying pages speak HTML, a tree of tagged elements. The dashboard needs about ten tags, and here they are:

TagJob
<html>, <head>, <body>The document, its metadata, its visible content
<h1>, <p>Heading and paragraph
<div>, <span>A block box and an inline run; the hooks JavaScript updates by id
<input>, <button>A field to type the setpoint, a button to send it
<script>, <style>The page's JavaScript and CSS, inline for a one-file dashboard
<!-- the whole shape of a page -->
<html>
  <head><title>Station s07</title></head>
  <body>
    <h1>Station s07</h1>
    <p>Light: <span id="pct">–</span> %</p>
  </body>
</html>

Two patterns, two jobs

Week 9's opening argument was "why not just request the data?", and it buried polling for machine telemetry. This week completes the thought: request/response was never the wrong pattern, only the wrong pattern for that job. Put them side by side and each one's territory is obvious:

Two panels. Left, request/response: a browser and a server joined by a paired ask arrow and answer arrow; the client starts every exchange, on demand. Right, publish/subscribe: a node publishes once to a broker which fans out to three subscribers; the data's owner starts every message, on events. Caption under each names its territory. Request / response browser server ask answer client-initiated · on demand · one answerer Publish / subscribe node broker subsubsub owner-initiated · on events · any audience
Who speaks first is the whole difference. Left: the party who wants starts, right for humans and on-demand queries. Right: the party who knows starts, right for telemetry and fan-out. Your system now runs both, joined at the Pi.
HTTP request/responseMQTT publish/subscribe
Who initiatesThe client who wants dataThe device that has data
WhenOn demandOn events
AudienceOne asker, one answererOne publish, any subscribers
StateStateless: each request self-containedBroker holds retained values and wills
Shines atHuman interfaces, documents, APIs, commands wanting a confirmed answerMachine telemetry, fleet fan-out, loose coupling
On your benchbrowser ↔ Flasknode ↔ broker ↔ monitors

One honest edge note: the browser speaks HTTP (and WebSockets), not raw TCP, so it cannot join the broker directly the way paho does; MQTT-over-WebSockets exists for exactly that, and it is a fine thing to know the name of and not build this term. Your bridge does the joining instead, next two pages.

The Pi's three doors

By tonight, sudo ufw status verbose on your Pi reads like a résumé: 22 (SSH, Week 3), 1883 (MQTT, Week 9), 5000 (HTTP, today). One machine, three services, three deliberately opened doors, each one justified in writing at the moment it opened. On the public internet the web answers at 80 and, encrypted, at 443; 5000 is Flask's development convention, and the same production honesty from Week 9 applies: real deployments put a hardened server and TLS in front. The bench stays plain and local, on purpose, and knows it.

Checklist for this stage

Check yourself

Why does HTTP's statelessness force the dashboard to keep a "latest values" structure on the server?
A request must be answerable from what the server holds at that instant: the protocol offers no way for a response to wait around for the next MQTT message, and no memory linking this request to any earlier one. So the answer must already be in hand, which is the bridge dict's entire job: MQTT keeps it current, HTTP reads it on demand.
Segment http://192.168.1.42:5000/api/light and say which Week built your knowledge of each part.
Scheme http (this week), host 192.168.1.42 (the Pi's address, the hostname -I habit from Week 2), port 5000 (the door concept from Weeks 3 and 9, new number today), path /api/light (this week's route, designed with Week 9's namespace instincts).
Why is the setpoint endpoint a POST when a GET "works" in a quick test?
GET's contract is "safe to repeat, changes nothing", and browsers, caches and prefetchers take it at its word: a prefetched or cached GET that moves a physical LED is an actuator firing without a human asking. POST declares "this acts", so nothing repeats it casually. Same instinct as choosing QoS for a command in Week 9: match the mechanism to the consequence.
The browser shows a 500 on /api/light. Where is the actual error, and what is the reading discipline?
In the terminal running Flask: a 500 means your route's Python raised, and Flask prints the full traceback there. Week 4's rule applies unchanged: read it bottom-up, last line names the exception, the frames above it name the line of yours that caused it.
A classmate says "Week 9 proved polling is bad, and now we poll every 2 seconds, contradiction?" Resolve it.
Week 9's case was against many consumers polling many constrained devices for machine telemetry: traffic and battery spent on "nothing new". Here one browser politely asks one mains-powered aggregator, over a cheap LAN, at a human screen's timescale, and the devices themselves still push. Pattern choice is per link, not per system; this link's job fits request/response.
Why can't the browser simply subscribe to bench/s07/light itself?
Browsers speak HTTP and WebSockets, not arbitrary TCP, so they cannot open a raw MQTT connection to port 1883. The standard answers are a bridge (ours: MQTT into shared state, served over HTTP) or MQTT-over-WebSockets, which brokers like Mosquitto can offer; knowing the second exists is enough for this course.