A standalone FPV drone lap timer: an ESP32 and one RX5808 5.8 GHz receiver at the gate, and a phone as the display. Voice announcements, race modes, race history, saved pilots and a race screen, all from a web page the timer serves itself. No app, no internet.
Based on PhobosLT and PhobosLT_pooling.
Racing
- One pilot per timer, sampled continuously for full precision (see timing precision).
- Race modes: practice (unlimited), timed (finish on the first pass after the time is up) and lap race (finish after N laps).
- Countdown start (3-2-1-go beeps) or the race starts on the first gate pass (see Starting a race).
- Last lap, delta to best, best, average, consistency, best 2 and best 3 consecutive, total.
- Lap-time chart: every lap at a glance, scaled to your laps so 0.3 s shows; the best lap, the best 3 in a row and your target marked, a crash lap kept off the scale. Tap a lap to read it.
- Pace target: a target lap time per pilot, mainly for practice; each race keeps the one it started with. Laps are announced against it ("plus 0.40", "On target"; Announcer → Then compare with: Target), and the Race tab, race screen, chart and race image show it.
- Race screen: big numbers for a phone or tablet at the field, with a live current-lap timer.
Voice and sound
- The phone announces lap times in English, with deltas, best laps and the finish; each phone chooses whether it speaks.
- Voice commands in Chrome: "start", "stop", "best time", "clear time"; the phone answers (see Voice).
- Buzzer and LED on the timer for laps, countdown, time up, race finished and low battery.
Setup and calibration
- Saved pilots: every named pilot is remembered with their channel and thresholds; one tap switches who is flying.
- Band and channel picker: A, B, E, F, R and L, each channel with its frequency. L band (5362-5621 MHz) is below the RX5808's specified range, so reception there isn't guaranteed.
- Calibration graph with 25 ms resolution, and auto-calibration: fly 3+ passes and it suggests Enter/Exit from the real passes (the peaks that stand out, at least a minimum lap apart) and the signal level between them, or says what to change when they don't stand out.
- Channel scan to see which channels are busy before choosing.
After the race
- Every race with a pass is saved on the timer (last 30) with CSV export.
- Rename a race ("Club night heat 2"); unnamed races show their date.
- Save image / Share image: a picture of the race (stats, chart, target, every lap against your best) to share or save, or Copy as text.
- Fix laps: merge two laps split by a false pass, or split a lap where a pass was missed.
Connection
- Own hotspot, or up to 5 saved WiFi networks (the strongest in range is used), such as your home WiFi or your phone's hotspot at the field.
http://laptimer.localon your WiFi, or an internet name (laptimer-jura.duckdns.org) that follows the timer onto a phone's hotspot; firmware updates over WiFi from the page.- Settings save automatically and stay consistent across several open phones.
- ESP32 dev board (classic ESP32; ESP32-C3 and ESP32-S3 targets are also in
targets/) - RX5808 5.8 GHz receiver module (SPI-modded)
- Optional: buzzer, LED, 1S LiPo with a 1:2 voltage divider for the battery reading
| ESP32 | Connects to |
|---|---|
| 33 | RX5808 RSSI |
| 19 | RX5808 CH1 (data) |
| 22 | RX5808 CH2 (select) |
| 23 | RX5808 CH3 (clock) |
| 3V3 / GND | RX5808 power |
| 21 | LED (+ resistor) to GND |
| 27 | Buzzer to GND |
| 35 | Battery voltage through a 1:2 divider |
A 3D-printable case for the LilyGO T-Energy board is in stl/.
Power: the timer draws about 0.10-0.15 A at 5 V (measured with a USB meter; the upper end on its own hotspot, whose radio can't rest between beacons). A 5 600 mAh power bank (3.7 V cells, about 20 Wh) runs it for about a day. Some power banks switch off below 50-100 mA as if a phone were full: leave it running on the bank for 15 minutes at home first, or use the bank's always-on or low-current mode.
- Install PlatformIO (VS Code extension or CLI).
- Build and flash the firmware and the web pages:
If the board doesn't enter flash mode, hold BOOT, tap EN, then release BOOT.
pio run -e PhobosLT -t upload pio run -e PhobosLT -t uploadfs
Later updates can go over WiFi: open Setup → Timer → Firmware update, or run
python tools/ota_upload.py <timer-ip> fw fs.
Timer hotspot (default)
- WiFi network
LapTimer_xxxx 192.168.4.1, passwordlaptimer. The address is in the name. - Open
http://192.168.4.1. If the phone says the WiFi has no internet, stay connected; if the page doesn't load, turn off mobile data. - There is no internet on it, so no voice commands (announcements work). Earlier versions' hotspot faded away on many pilot channels; why and how it was fixed: docs/hotspot.md.
Your WiFi, or your phone's hotspot
- Setup → WiFi networks: add the network (Scan helps), then Restart timer.
- Open
http://laptimer.local, or the timer's IP (Setup → Timer shows it; so do your router and the phone's hotspot settings). Android doesn't resolvelaptimer.localon its own hotspot (and often not elsewhere), and a phone hotspot hands out a different address every time: give the timer an internet name instead (next section). - At power-up it joins the strongest saved network in range (60 s to connect). If none is seen, it tries the newest one for 20 s (a hidden network, or a phone hotspot still starting), then starts its own hotspot.
- Changes to the list, also removing the network in use, take effect at the next restart. Forget all restarts into the hotspot.
Internet name (for a phone hotspot, or any network with internet)
- At duckdns.org (free, sign in with Google or GitHub) add a
subdomain, e.g.
laptimer-jura, and copy the token shown there. - Setup → Internet name: enter the name and the token, Save. The token stays on the timer and is never shown again (leave it empty to keep it when changing the name); saving with an empty name removes name and token.
- Whenever the timer joins a network with internet, it sends its current address to DuckDNS,
and
http://laptimer-jura.duckdns.orgopens the page from the phone whose hotspot it is on (and from home). The card shows the address sent and when, or why it failed (no internet on the network, DuckDNS not answering, name or token refused); it tries again by itself. On the timer's own hotspot there is no internet, so nothing is sent.
- If the connected network goes away, the timer keeps timing and saving races and rejoins it when it's back (a short beep every minute until then). For its own hotspot instead, switch it off and on.
- At the field, the timer on your phone's hotspot gives voice commands: the phone shares its mobile data. The phone needs signal.
- Setup: enter the pilot's name and channel, or tap a saved pilot. Settings save automatically. Optionally a Target lap in seconds for the next races (empty = off); it is saved with the pilot.
- Calibrate at the gate: switch on Auto-calibrate and fly 3+ passes, then Apply. By hand: Enter below the peaks of your passes and above what the timer reads with the drone on the pad, Exit just above the level while the drone is away (a pass ends when the signal drops below Exit). Run a channel scan first if others are flying.
- Race: press Start (or say "start"). Open the Race screen for big numbers.
- History: every race with a pass is saved. Open one to see its chart and all laps,
Fix laps, Rename it, Save image or export CSV. The timer's page is plain
http://, where phones offer web pages no share menu: Save image opens the picture full screen, press and hold it for the phone's own menu (Android: Share image / Download image; iPhone: Share… / Save to Photos), or tap Download. With the timer's address inchrome://flags/#unsafely-treat-insecure-origin-as-secure(the same setting as for voice commands; the picture's screen shows the steps) the button becomes Share image and opens the share menu directly.
- Countdown (Setup → Race → Countdown start): 3-2-1-go beeps. The race starts at GO, and the first pass after it is the start pass. A drone waiting on the pad during the countdown gets its start pass when it takes off.
- First pass: after Start the timer waits, and the race begins at the first pass through the gate. With Enter above the pad level this is simply the first gate crossing.
- Drone on the pad above Enter (a pad close to the timer): after about 10 s with a steady signal the timer treats it as parked, and the take-off through the gate starts the race, provided the gate reads clearly stronger than the pad (5 or more above the pad level on the calibration graph). A take-off within those 10 s counts the time on the pad as the start pass, so lap 1 comes out too long; each rise of the signal on the pad (VTX warming up, the drone moved) starts the 10 s again. Arm, set it down, wait a moment, then fly; or set Enter above the pad level, which avoids all of this.
- Announcer (Setup → Announcer, saved on the timer): Announce each lap (lap time, 2 or 3 consecutive laps, a beep on the phone, nothing, or best lap time only: lap 1, then only a lap faster than all before it, "best lap 9.17"), Say "best" on a new best ("Best lap", "Best 2 laps", "Best 3 laps"), Then compare with (nothing, your best so far: "minus 0.12", or the target: "plus 0.40", "On target" within 0.05 s), and the speech rate. Everything refers to the value announced: with 2 or 3 laps the comparison is with your best 2 or 3 laps, or with 2 or 3 times the target lap. A preview under the settings shows what you'll hear for four example laps. Every phone with Voice on speaks it. The timer's own buzzer is separate (Setup → Alerts).
- This phone (Setup → This phone, for each phone, not saved on the timer): Voice (speaks lap times, race events and the answers to voice commands), Voice commands, and Test voice. Phones allow speech only after a tap: tap Test voice once after opening the page. Voice works in any normal browser tab (Chrome, Brave, Safari).
- Voice commands: say "start" (or "go"), "stop", "best time" or "clear time". The
phone answers ("Race stopped", "Times cleared", "Nothing to clear", ...), the same as
when you press the buttons. They need:
- Chrome: Brave blocks the speech service.
- Internet on the timer's network: Chrome's speech recognition runs on Google's servers. Your home WiFi, or the timer on your phone's hotspot; not the timer's own hotspot (mobile data doesn't help: Android then stops reaching the timer).
- The microphone allowed: the page is plain
http://(the timer has no certificate), and Chrome gives the microphone only to trusted sites. Add the timer's address once inchrome://flags/#unsafely-treat-insecure-origin-as-secure(for examplehttp://192.168.4.1; add every address you use), relaunch Chrome and allow the microphone when asked. The same setting turns History's Save image into Share image.
- Mic icon in the top bar: green is listening, red is a problem, grey is off or starting. Tap it for the reason and what to do, with the addresses to copy. It recovers by itself (back online, microphone back). Chrome closes a listening session after a few seconds of silence and it reopens at once: a word said in that short gap is missed, so say it again.
The receiver stays on the pilot's channel and reads the signal thousands of times per second. A pass is timed at the middle of the signal peak (a drone that stays in range of the timer for more than 10 s is parked, not passing: see Starting a race), so the timer itself adds about a millisecond. What remains is how sharp the peak is: put the timer right at the gate, keep the drone far away for the rest of the lap, and calibrate Enter/Exit from real passes. How fast it reads, and a faster option kept for later: docs/sampling.md.
Why one pilot: after every channel change the RX5808 needs 36-45 ms to lock (measured), so a receiver shared between pilots reads each of them only every 100-200 ms, and a fast whoop pass falls between the readings. Racing several pilots needs a receiver per pilot, as in RotorHazard.
See CLAUDE.md for how the project is built, tested and released, and
tools/ for the simulated timer, WiFi upload, device test, RSSI/pass logger, boot
log and hotspot signal scripts.
Tests: python tools/mock_server.py, then node tools/run_page_test.js checks the web page
in a headless Chrome (every tab, both themes at phone width, two phones, connection loss), and
python tools/device_test.py <timer-ip> checks the timer's API (also against the mock).
node tools/readme_images.js takes this README's screenshots from the mock.
MIT, see LICENSE. Based on PhobosLT by phobos- and PhobosLT_pooling by nikbg3.









