This is a modification of the work in https://github.com/yashmulgaonkar/FlightScnr.
Go there for all information about the original project and buy him a coffee:
Firmware is released under CC BY-NC-SA 4.0 (see LICENSE) instead of MIT. Permissive MIT licensing on similar projects has made it easy for vendors to ship closed derivatives without keeping firmware open to the community. This license keeps the source shareable for hobbyists and open-source builders while discouraging proprietary takeaways.
I attempted to leave as much as possible of the excellent original project intact, while adding support for my local services and implementation choices.
One of the bigger changes to the project is to make the device battery-powered so that it can be carried around the house with me. To this end, I added a battery with a Qi-based wireless charging capability.
I added a Sparkfun Electronics Battery Babysitter board which allows the LilyGO device to remain powered on while the battery is being charged. This battery management board has a LiPo fuel gauge function available over I2C, that provides a series of metrics on the battery (e.g., its charge/discharge state, percentage of total charge remaining, etc.). I added a display page with this information, as well as adding the battery information to the Settings web page.
I want the device to stay powered on at all times so that it will provide instant information. I also want the battery and display to last as long as possible, so I want to implement a screen blanking function that triggers after some period of time (or whenever the device is placed on a wireless charger). Whenever the display is blanked, tapping the touchscreen (or removing it from the charger) with result in the display being instantly turned back on.
I added a screen-blanking function that turns the display on whenever the screen is touched (or the device is removed from the charger) and starts a timer that is reset every time the device is touched again. When this timer expires, the display is turned off. The display is turned on at boot time, when the screen is touched, or when the device is removed from the charging pad. The display remains on until a given time interval after the last time it was touched.
I added a REST API to allow programmatic changes to the settings offered in the application's Settings web page.
I added an in-RAM log capture that collects all diagnostic output since boot and makes it available via a browser page and REST endpoint, so the device can be debugged over Wi-Fi without a USB cable.
I have my own (unfiltered, 1080MHz and 980MHz) ADS-B receiver as well as a LAN-based service that provides additional information on flights (such as airline, route, airport, etc.) that I would like to use instead of the built in web-based services in the original project. The information server handles caching as well as falling back to different cloud-based APIs for this, as well as other applications in my home.
See AircraftRoute for more information about this service.
The Sparkfun Battery Babysitter board's BQ27441-G1A fuel gauge is connected via the T-Encoder Pro's Qwiic port. The Qwiic connector's SDA/SCL pins are GPIO16/GPIO15 respectively (the ESP32-S3's XTAL_32K_N/XTAL_32K_P pads, repurposed as GPIO since no 32 kHz crystal is fitted). These are on a separate I2C bus (Wire1) from the internal touch controller bus (Wire, GPIO5/GPIO6).
The BQ27441 driver polls the gauge every 10 seconds and calls displayBlankingNotifyCharging() / displayBlankingNotifyUncharging() on charging-state transitions, so the display blanks when placed on the charger and wakes when removed.
The blanking timeout is configurable via the settings web page ("Screen blanking" dropdown). Available options are Never, 30 seconds, 1 minute, 2 minutes, 5 minutes, and 10 minutes. The default is 1 minute. The setting is stored in NVS under the key blank_to_s and survives reboots.
Behavior:
- The idle timer starts when setup completes (after Wi-Fi connects and the boot splash appears).
- Any touch, swipe, knob press, or encoder rotation resets the timer. If the display was off, the triggering input wakes the display but is otherwise discarded — no accidental navigation occurs on wake.
- The radar sweep animation is paused while the display is off to avoid unnecessary SPI writes.
hardware::displayBlankingNotifyCharging()andhardware::displayBlankingNotifyUncharging()are called by the BQ27441 driver whenever a charging-state transition is detected.
All diagnostic output (every Log.printf / Log.println call in the firmware) is written to both the USB serial port and a 16 KB in-RAM buffer. The buffer is cleared on reboot and is not persisted to flash.
The buffer is exposed two ways:
http://flightscanner.local/log— browser page with a live-updating log viewer that auto-refreshes every 5 seconds and scrolls to the most recent entry.GET /api/log— rawtext/plainendpoint returning the full buffer, suitable for scripting (see REST API section below).
When the buffer is full, new output continues to reach the serial port but is no longer appended to the buffer. The /log page displays "(buffer full)" in the status line when this happens.
The global Log object (a Print-derived LogCapture class in include/services/log_capture.h) replaces all direct Serial.printf / Serial.println calls in the firmware. To add a log statement in new code:
#include "services/log_capture.h"
Log.printf("[module] message: %d\n", value);All settings on the flightscanner.local page are also accessible via a REST API served on port 80. Unlike the HTML form, API writes take effect immediately without a reboot (brightness is applied live; route server URL takes effect on the next lookup).
Returns all current settings as JSON.
GET http://flightscanner.local/api/settings
curl -X GET http://flightscanner.local/api/settings | jq '.'{
"radar_center": "37.619770, -122.372270",
"use_miles": false,
"show_cardinals": true,
"show_sweep": true,
"detail_timeout": 10,
"bright_pct": 80,
"blank_timeout": 60,
"ui_beep": true,
"beep_tone": "B",
"min_height": 500,
"range_idx": 2,
"route_server_url": "http://192.168.1.x:5000"
}Updates one or more settings. Send a JSON body with only the fields you want to change. Returns the full settings object after applying changes.
POST http://flightscanner.local/api/settings
Content-Type: application/json
{"bright_pct": 85, "blank_timeout": 120}
curl -X POST http://flightscanner.local/api/settings -H 'Content-Type: application/json' -d '{"bright_pct": 85, "blank_timeout": 120}'
| Field | Type | Values |
|---|---|---|
radar_center |
string | "lat, lon" decimal degrees |
use_miles |
bool | |
show_cardinals |
bool | |
show_sweep |
bool | |
detail_timeout |
int | 0 (manual), 10, 20, 30 |
bright_pct |
int | 20, 40, 60, 80, 100 |
blank_timeout |
int | 0 (never), 30, 60, 120, 300, 600 |
ui_beep |
bool | |
beep_tone |
string | "A" – "E" |
min_height |
int | feet, 0 = off |
range_idx |
int | 0 – N-1 (see radar scale bands) |
route_server_url |
string | base URL, e.g. "http://192.168.1.x:5000" |
Invalid or out-of-range values for a field are silently ignored (the field is left unchanged).
Live log viewer (browser page). See the in-RAM log capture section for full details.
Returns the full log buffer as text/plain.
curl http://flightscanner.local/api/logReturns the current state of the BQ27441-G1A fuel gauge. If the gauge is not detected, only "present": false is returned.
GET http://flightscanner.local/api/battery
curl -X GET http://flightscanner.local/api/battery | jq '.'{
"present": true,
"soc": 85,
"voltage_mv": 3987,
"current_ma": 450,
"remain_mah": 1020,
"full_mah": 1200,
"time_to_empty_min": 0,
"charging": true,
"full": false
}| Field | Type | Description |
|---|---|---|
present |
bool | false if gauge not detected; remaining fields absent |
soc |
int | State of charge, 0–100% |
voltage_mv |
int | Battery voltage in mV |
current_ma |
int | Average current in mA; positive = charging, negative = discharging |
remain_mah |
int | Remaining capacity in mAh |
full_mah |
int | Full charge capacity in mAh |
time_to_empty_min |
int | Estimated minutes to empty (0 when charging or unknown) |
charging |
bool | true while charging current is positive |
full |
bool | true when FC flag is set |
Triggers a clean reboot. Use this after changes that require it (e.g. Wi-Fi settings).
POST http://flightscanner.local/api/reboot
{"status": "rebooting"}The hardware that I'm using a LilyGO T-Encoder Pro with a Sparkfun Battery Babysitter, a Qi Wireless Charging Receiver, and a LiPo battery. I also 3D modeled and printed an enclosure for these components.
- Key Features
- ESP32-S3 R8
- Flash: 16 MB
- PSRAM: 8 MB
- WiFi and BTLE5
- 2x Qwiic 4x pin
- USB-C
- 1.2" 390x390 AMOLED Display (CO5300)
- QSPI interface
- CST816 Touchscreen Controller and Rotary Encoder
- Buzzer
- 5V @ 500mA
-
Key Features
- allows charging of battery and suppling the output load with 5V
- ????
- the board includes a BQ27441-G1A LiPo fuel gauge
-
BQ27441-G1A LiPo Fuel Gauge
- the LiPo fuel gauge has a (3.3V and 5V tolerant) I2C interface that is used to:
- measure a battery's:
- voltage (mV)
- state-of-charge (%)
- current (mA)
- power (mW)
- estimated remaining capacity (mAh)
- set the battery's full-charge capacity
- measure a battery's:
- the I2C address of this device is 0x55
- the LiPo fuel gauge has a programmable output, GPOUT, that can be used to signal:
- low battery alert
- change in state-of-charge
- the LiPo fuel gauge has a (3.3V and 5V tolerant) I2C interface that is used to:
image here
The wireless charging receiver unit consists of a coil and a small PCB. Most models also provide a self-adhesive-backed sheet of magnetic material to put on the back of the coil to improve the energy transfer and reduce the field effects on the colocated electronics.
image here
Original application code, tools, and documentation in this repository, as well as all modifications to it, are licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (LICENSE).
- Attribution: credit the author and link to the license when you share or adapt this work.
- NonCommercial: you may not use this material for commercial purposes without separate permission.
- ShareAlike: adaptations must be released under the same license.
Vendored libraries (lib/Arduino_GFX, lib/SensorLib, and PlatformIO registry dependencies) remain under their own licenses (GPL, MIT, etc.). Combining them into a binary does not re-license those components. Comply with each upstream license when you distribute builds.