OneTesselAway: Building a Real-Time Public Transit Status IoT Device

By Robert McGuire

The data management system borrows ideas from the Facebook Flux and pub-sub software architecture patterns.

The OneTesselAway backend software is a Node.js JavaScript application that runs on the Tessel 2. After initializing the hardware, it begins polling the OneBusAway API for bus arrival information every 5 seconds. When new arrival information is received, the LCD screen and other components update accordingly.

The OneTesselAway backend application also starts an Express.js web server which serves the web UI frontend application. The web UI is also a JavaScript application, but it runs the user’s web browser. It features simulated hardware that is synchronized with the backend application and hardware components. For example, the simulated LCD screen should always display the same arrival times as the physical LCD screen.

High-level interaction diagram of the backend (left) and frontend (right) applications. Note: while technically not part of the “frontend web UI”, most of the hardware is a user interface, hence why it’s on the “frontend” side of the fence. (It really doesn’t fit well on either side.)

The backbone of the OneTesselAway software architecture is the data management system which handles all information flowing throughout the backend and frontend applications. The data management system borrows ideas from the Facebook Flux and pub-sub software architecture patterns.

Simplified architecture diagram of Facebook’s Flux pattern. The data management in OneBusAway works in a similar way. Credit: Flux In-Depth Overview docs.

The data management system has a single global store to which all application data, backend and frontend, is saved. Components can subscribe to specific changes to the global store so they’ll be notified when information they care about is updated. All data flow is unidirectional. Information changes always begin in the global store, which then flows from the global store to subscribers downstream.

For example, when new bus arrival information comes into the application from the OneBusAway API, it is saved in the global store, which then notifies all subscribers, the LCD screens, of the new bus arrival information. The subscribers (the LCD screens) can then use this information (display new arrival times).

This unidirectional information flow is true for all data. There is no direct communication within or between applications or components. If a component needs to communicate with another, the data must start in the global store, and flow downstream to the other component.

For example, when the alarm button is pressed, an isAlarmSet flag is saved in the global store, to which the alarm LED is subscribed. When the flag is set, the alarm LED will illuminate, and when it is unset, the alarm LED will turn off.

When the device backend application starts, it runs an Express.js web server. The web server will serve the web UI frontend application when requested from a web browser. The web server uses Embedded JavaScript templating (EJS), to construct an HTML document of the web UI to be interpreted and run by the web browser.

The web UI’s template includes all the markup, styling, and JavaScript necessary to create a set of simulated hardware, advanced controls, and debug information. The backend application also injects a copy of the global store so the simulated hardware will show the correct initial state.

The web UI also maintains a real-time connection to the backend application using WebSockets via the Socket.IO library. Every time the global state is updated from the backend application, the web UI will update its copy of the global store to match.

When data is updated from the web UI, the backend application will update its global store, which in turn updates the global store of the web UI. This way, the global stores in the backend and frontend applications will be synchronized, and the physical and simulated hardware should always match.

This diagram shows the flow of data updates starting from the backend (left) and web UI (right) applications. Notice that when there is a data update on the web UI, the frontend’s global store is updated via the backend. This way, we only have to sync the global data store in a single direction, from the backend to the frontend. The synced global store on the frontend is there only for reference by the simulated hardware.

A low-level Tessel PWM API is used to achieve PWM value changes. Keep in mind that any changes to these PWM values affects all PWM pins. I learned the hard way when one of my LEDs was unexpectedly flickering to the beat of Nyan Cat.

The original “Nyan Cat” song which plays as the tune on the OneTesselAway musical alarm

The backend application usually runs on the device, but since it’s a Node.js application, it can be run anywhere Node.js can run, such as a desktop or laptop. You can start the OneTesselAway in “web only” mode and all device hardware will be mocked which allows the software to be run without the OneTesselAway device at all.

Starting in “web only” mode is a very useful feature when developing the software because uploading and running the code on the Tessel 2 can take multiple minutes, which adds up, and breaks your flow.