The Ampelmännchen are a popular symbol from East Germany, which display on the pedestrian traffic lights at every street corner. There's even a Berlin-based retail chain that's inspired by their design.
I came across a set of secondhand Ampelmann signals at a flea market, and I wanted to control them from my phone. If you'd like to do the same, read on!
Health Warning: This project uses 220V mains power. I am not an electrician. Follow these instructions at your own risk.
I wanted to build something with a Berlin aesthetic, and would be fun for visitors to my home to interact with. Unfortunately our visitor numbers declined severely this year, but still hoping for a great reaction in 2021...?
Each device on your local network has it's own private IP address (e.g. 192.168.1.20
). Some routers such as the FritzBox also let you browse by local hostnames, so that 192.168.1.20
is also accessible at mydevice.fritz.box
. For the traffic light, the device hostname is traffic-light
so we can visit it at http://traffic-light.fritz.box
.
The webapp is a very simple responsive single-page application. It shows:
Code is under the /webapp directory. No external dependencies are required since it's just relying on standard browser features, like CSS transform, WebSockets, and XHR. You can preview the running application here although it won't control anything, since of course you're not on my LAN. If you visited it from the local network, at e.g. http://traffic-light.fritz.box
it would work fully.
Upon loading the page, the application makes a GET request to find the current status at /api/status
, and then opens a WebSocket connection to the server on port 81
. Subsequent status updates will always come via the WebSocket, in order to keep multiple clients in sync. Each time a websocket event arrives, we apply the changes to a single global state
object. Shortly afterwards, the updateScreen()
method applies those changes to the DOM.
On startup we also detect if the user is on a mobile or desktop device, to handle either touch events or click events. We actually use the touchend
event to send commands to the server, because this performed more reliably on the iPhone X. Swiping up from the bottom of the screen to exit Safari was firing the touchstart
event, making it impossible to exit the app without turning on the green light!
Finally, we want to reduce load on the server wherever possible. Remember the ESP8266 is running on an 80MHz processor with only about 50kB of RAM. It is NOT a beefy device. So when the browser is inactive, we disconnect the websocket. When the tab or browser reopens, we again check for status, and reconnect the WebSocket.
The ESP8266 is busy handling API requests and timing code, so it doesn't have the necessary resources to serve the webapp itself. Also, making cosmetic changes to the webapp is difficult, if I need to physically connect to the hardware every time I want to apply an update.
The webapp's index.html follows the single-page application principle that everything should be rendered by Javascript, making the HTML content itself very small. Like 550 bytes small. Everything else is loaded by the client's browser, without needing to make further calls to the server. So the webapp is actually hosted in its entirety on GitHub Pages, a free static site hosting tool. Hitting /index.html
actually makes a proxy request to GitHub pages, and returns the result to the client browser.
Now we can change anything in the webapp, and the server is unaffected. Great! Well, almost...
Most of the code for this webapp is in the CSS and JS files, not in index.html
itself. Browsers cache any loaded files for an indeterminate period of time before it re-requests them. If index.html doesn't change, but we've deployed a new JS version, how will our clients know they need to load the new JS version?
When we push any new version of our code to the git master
branch, a GitHub Action runs, that executes the deployment to GitHub Pages where the page is actually served to the public. The trick here is in appending the suffix ?version=latest
to the end of our own CSS and JS files, in the index.html
. Before it copies the content to the gh-pages
branch, the action uses the command sed
to replace that "latest
" with the value of the variable $GITHUB_SHA
, which is actually the last commit ID on the master
branch. (e.g. a value like b43200422c4f5da6dd70676456737e5af46cb825
).
Then the next time a client visits the webapp, the browser will see a new, different value after the ?version=
, and request the new, updated JS or CSS file, which it will not have already cached.
See the setup(void)
method in traffic-light-controller.ino
and the Arduino Code section for how this works in practice.
I decided to use both REST and WebSockets in tandem. REST is mostly used by clients to control the server. WebSockets are used to broadcast status information to clients. There are many tools like Postman which allow you to easily experiment with REST API's, so I found this more convenient.
HTTP API: Refer to the Swagger documentation here.
WebSocket API: The websocket connection sends JSON blobs which the webapp uses to update its internal state. A websocket event can contain one or more fields to update. An example containing environmental info might look like:
{
"redTemperature" : 21.6,
"greenTemperature" : 22.7,
"greenHumidity" : 55,
"redHumidity" : 59
}
No data is current sent from the client to the server via the websocket, although this is possible.
The arduino code is all within a single file that includes explanatory comments.
It begins with a set of definitions for pin locations, library imports and hardcoded values for things like HTTP Content-Types and response code values. Following that are a set of variable which can change at runtime, all prefixed with underscores. A few objects are also initialised here, including those for the web server, web socket server, WiFi client, and temperature sensors. The "system clock" is maintained by the _currentMillis
field.
After booting, the setup(void)
method runs. After doing some pin setup, it creates the necessary mappings for the REST endpoints, and starts the servers listening for client requests. The loop(void)
method is in charge of everything else. Each cycle it processes any pending web requests, updates the rhythm cycle, and reads the sensors if necessary. If we're in party mode, it will set the current flash/pulse state.
The rhythm (for party mode) is hardcoded to play the sequence in the field RHYTHM_PATTERN
, but in theory it could be changed at runtime to anything else. Every time we call the rhythm()
method, we use the current _bpm
and _currentMillis
values to work out what position we should be in the pattern. This is stored in the _rhythmStep
field.
During the rhythm pattern, there are periods where both of the relays are actually switched off. But because the lights are incandescent bulbs, they don't start or stop emitting light instantly. It looks like the bulbs take about 1.7 seconds to switch fully on or off. So by adding a period within the pattern where both are switched off, we end up with a gentle pulsing pattern as the bulbs warm up and cool down.
In the partyFlash()
method, we get the pattern item that is supposed to currently be displayed (or both are to be switched off) and call lightSwitch(...)
with the appropriate parameters. lightSwitch(...)
in turn calls sendToWebSocketClients(...)
so that all connected clients are updated to the new state.
If the user simply clicks on one of the lights to turn it on or off, the process is similar, but handled as a REST request. One of the handleX
methods is called, which validates the request, and in turn calls lightSwitch(...)
.
At a more infrequent interval, we check the temperature of the two enclosures, and also send this via WebSocket to all clients. This is currently only used for informational purposes but it could be used to disable the lights when the temperature exceeds some safety limit.
Credit to @mrcosta for his help reviewing this article.