A web switch becomes more interesting when the browser and the physical device can disagree. Someone clicks “on,” but the controller is offline. A device reconnects and reports its actual pins. A second board belongs to the same account. These are state-synchronization problems, not just HTTP requests.
AlgoFly's MicroPython and NodeMCU video links a small public control-center project. It pairs a Flask web application with ESP8266 firmware, and the description highlights multiple-device synchronization and token-based grouping. This article follows that code and explains the useful distinction between the state a user requested and the state a device reported.

The browser, server, and board have different jobs
The browser displays devices and lets someone change a named switch. Flask keeps a per-token collection of device records. Each microcontroller connects to Wi-Fi, polls the server for requested updates, writes its GPIO outputs, and sends its observed pin values back.
Because the board initiates requests, it can reach a server without accepting inbound connections through a home router. That is a practical simplification for a prototype. It does not remove the need to secure the server or authenticate each device correctly.
The repository includes the Flask application, a device-state helper, HTML templates, and a MicroPython main.py. The public README is brief, so the implementation is the most useful reference for understanding the message flow.
Requested state is not the same as reported state
The ESPDevice helper keeps user_state and mcu_state separately. The first holds the desired switch values; the second records what the board last sent. Starting values are marked unknown rather than assuming every output is off.
This gives the UI the raw material for honest feedback. A click can become “requested,” and only a matching device report can become “confirmed.” If the device stops reporting, the last value should be shown as stale rather than as a fresh observation.
The helper also records a timestamp and includes expiry-related logic. When extending the application, check that the active status path actually uses that freshness logic; having an expiry method in a class is not the same as consistently enforcing device liveness everywhere.
A simple polling loop connects the two states
The firmware sends device identity, a token, switch labels, and an alias as request headers. It gets desired updates from /mcu-get-update, applies recognized values to known pins, and posts the resulting state to /mcu-set-update.
The published loop sleeps for 0.3 seconds between iterations after its network work. That is not a guaranteed 300 ms end-to-end response time: request duration, network conditions, and error recovery add to the interval.
A conceptual message, using fresh placeholders rather than the repository's example settings, looks like this:
# Illustrative state payload, not deployment credentials.
reported_state = {
"D1": "on",
"D2": "off",
}
# Desired state can differ while a command is in flight.
desired_state = {"D1": "off"}
Choose firmware and pin mapping for the actual board
MicroPython's ESP8266 guide explains firmware selection, serial access, networking, and GPIO naming. Choose a released firmware build appropriate for the board's flash capacity. The latest development documentation can describe features absent from an older installed image.
The project maps NodeMCU names such as D1 to chip GPIO numbers. Those are not interchangeable numbering systems. Boot-strapping pins and onboard LEDs can also behave differently from an ordinary output; confirm the board's pinout before connecting a relay or other peripheral.
Start with a harmless low-voltage indicator and one output. Verify an initial connection, a requested change, a matching report, and recovery after a disconnected network before expanding to more switches. This keeps failures understandable while the complete loop is still small.
Token grouping is a starting point, not a complete security model
The original server groups devices by a token supplied by the caller. That is useful for a demo, but the code does not constitute a full account system with token issuance, rotation, or robust per-device authorization. Some browser interactions place the token in URL parameters, which can be retained in logs and history.
For a deployed version, issue strong credentials, validate them rather than merely using them as dictionary keys, and scope each device to the operations it may perform. Keep secrets out of firmware examples and use HTTPS with certificate validation supported by the device.
The Flask code also stores its device registry in process memory. Restarting the process loses that registry, and multiple independent web workers do not share it automatically. Add a shared durable state store before assuming the same design scales across workers.
Make retries and stale devices visible
A more robust protocol includes a command identifier or revision so the UI can relate a requested change to a later report. Reconnects can resend a command, so applying the same target state twice should be safe. Avoid treating an old report as confirmation of a newer request.
Bound connection attempts, close HTTP response objects on constrained devices, and add backoff when the server is unavailable. Polling every fraction of a second may be fine for a bench demo but becomes expensive as device count grows.
For larger fleets, a message broker may be appropriate, but the key lesson survives the transport change: desired state, reported state, and freshness belong in the model. The NodeMCU project is a compact way to see those ideas working together before adding more infrastructure.
