Tailwind Icon

Tailwind

Multiple LED flight tracking boards driven by one backend

Tailwind board showing an example flight
Upcoming flight
Tailwind board showing an hourly weather forecast
Weather
Tailwind board showing custom text
Custom messages
Tailwind board showing F1 standings
F1 Standings

Inspiration

While scrolling on Instagram I saw a similar commercial product, an LED board which tracks overhead planes using freely avaliable ADS-B data. I liked the idea, but wanted expanded functionality to cover my own upcoming flights, as well as other smart home data.

I began development while the hardware was in the post, and wanted a way to preview the output of the screen - so I asked an AI to generate me a web preview of what this 64x64px display would look like. The Tailwind dashboard was then born, and I saw benefit in offloading the rendering to a server, while the board itself simply reads the pixel array.

Design Spec

- Automatic upcoming flight detection.
- Automatic 'in-air' detection with flight duration readout.
- "Downroute" mode to show weather and time in destination.
- Ability to add screens in rotation, including the intial ideas weather and Sonos Now playing.
- User customisable endpoints for flight detection/calendar entries/weather location and other data.

The Backend

I self host the backend because for a small scale project - it makes the most sense.
There isn't any ongoing monthly usage fees, only a minimal amount of electricity.
It also gives me full control over the stack I use.
It isn't without it's drawbacks though - primarily where it comes to uptime and security maintence, those two are both on me to ensure I keep up with.

Chosen Stack

Domain > Tunnel > Reverse Proxy > Tailwind Docker Container

Cloudflare

The domain was already bought through Cloudflare, and therefore DNS is managed there too.
The DNS entry points to a CF tunnel into my network, this prevents needing to setup DDNS or expose my home IP address.
Cloudflare also allows for setting up a Zero Trust login screen infront of my application, but I chose not to use it for this project as I want to scope the dashboard access for each user to their board.

Github

I'm using git for version control, and also as a repo from which to pull my docker container from.
This has actually proved really simple as I can commit and push from VSCode on my laptop, and simply hit "re-deploy" in my container management program to update the backend.
Currently I'm only using a 'main' branch, but as I gift these boards to friends and family, I'll setup a dev branch so I can test changes without impacting my production instance.

Fastify & Node.js

I'm using TypeScript based tooling as a single language on the backend for the following benefits:
1. Structure forces good habits.
2. Bugs caught at compile time, instead of when already deployed.
3. A lot of the web and many apps are built on TS, so it's a good and modern beginner language to get to grips with.

Auth

Boards are hardcoded with a bearer token, which is scoped to /api/<board>/frame endpoints.

Orignally I was handling single user auth myself with a plaintext username and a hashed password set in my Docker environment variables. However I realised as I added new users, or users wanted to add themselves, this wouldn't scale.
Multi user auth is now handled by Clerk, which is free for my use case and provides plenty of documentation on how to implement it into different environments.

When setting up a Tailwind board for the first time, the onboarding automatically creates an account scoped to only that boards setting endpoints. I can choose which boards existing users have access to with an API endpoint only authorised by the admin account.

Data Sources

Upcoming flight info

This comes from the Flighty app, which has the paid feature of exporting all flights to a calendar.
The .ics link is pulled every 1hr and parsed for info such as person taking the flight, flight route and time/date.

F1 Stats

These are kindly provided by F1API.dev
Currently I pull current/drivers-championship, current/constructors-champtionship, and current/last/race to display 3x standings cards with the top 6x drivers/constructors in order.

Sonos Now Playing

This was actually one of the easier integrations to build, as Sonos players announce themsleves on the network using multicast. They use a SOAP API, which the board polls for Now Playing info every 20s.
The board "POSTs" the data to the server, which renders it into a card.

HTTP
 POST /api/<board>/sonos/now-playing HTTP/1.1 
          Host: tailwind.themisto.app
          Authorization: Bearer BOARD_TOKEN
          Content-type: appication/json
          { isPlaying, trackTitle?, artist?, artworkUrl? }
        

An additional row in the database 'updatedAt' is set by the sever, and the card is rendered only when there is data fresher than 90s old. In my testing this was a happy medium to allow for any failed Board>Sonos or Board>Server requests, and drop the card if music is paused.

As I generally play music in a single room, or all the rooms together, I decided for simplicity the board would pick up the first Sonos to reply to the SOAP request. However I realised the Sonos generally always replies with null even if it's not playing, so I needed some validation logic to move onto the next player if for example my bedroom reply is null, but kitchen reply has actual track info.

The Hardware

Controller board - Interstate75w
- Dual core ARM M33 running at 150Mhz
- 520kB sRAM
- 4Mb flash storage
- Built-in WiFi 4 and Bluetooth 5.4
You can run programs on this board in C++ or MicroPython, and I picked the latter.

Display - 64x64 HUB75 LED Matrix
- Uses the HUB75 communication protocol/pins, common in large video walls.
- 20w max power draw easily covered by USB C power adapters, so nothing external is needed.
- In my testing, 8w draw was common for bright screens and 2w was common for text only - (both at 75% brightness).

API Test

Test my API with a GET request to healthz endpoint, if it's up you should get a JSON response with status and server build timestamp.

GET /api/healthz
Click SEND to test API response.

Future Aspirations

Audit data validation for API endpoints - prevent injection attacks, most endpoints already have this but I want to ensure they are all covered.

Rate limiting - prevent brute force attacks or abuse of the API endpoints. Set the rates to comfortably above the designed poll frequency of the boards / web dashboard.

On-device processing - I've began to experiment with the board doing it's own rendering, which comes with some key advantages:

1. Free public API endpoints (such as weather, F1 stats, etc.) can be polled by the board itself, reducing reliance on my own server.

2. Reduce server overhead.
Currently each additional board requires another set of pixels to be rendered server side, small now at less than 1% CPU use, but if this was a commercial project at scale this could become an unnecessary cost.

3. Unlocking the ability to animate screens, such as a pulsing status bar.
Currently with server side rendering, pixels are drawn and fetched by the board only once every 20 seconds, ruling out the possibility of animations.

Migrate to Better Auth - mainly because I'd like to support passkeys and hardware keys for authentication for future proofing. This is a paid-only feature with Clerk which I currently use.