go run ./cmd/tuiThis repository can record raw F1 Live Timing SignalR events to SQLite and expose a recorded session through a local SignalR-compatible replay server.
The recorder does not merge reference messages and deltas. It stores each raw event independently with its sequence number and receive timestamp.
- Go 1.26 or newer
- Network access to the F1 Live Timing service when recording
Start the recorder before or during a live F1 session:
go run ./cmd/recorder -database f1-livetiming.dbThe default database file is f1-livetiming.db. You can provide another path:
go run ./cmd/recorder -database data/monza-practice.dbThe recorder prints the generated session ID when it starts:
recording live timing to f1-livetiming.db (session 20260902T212436.123456789Z)
Save this session ID. It is required to start the replay server.
Stop recording with Ctrl+C. Events are written as they arrive. If SQLite temporarily fails, the recorder retries each write and then drops the event after the retry limit; dropped sequence numbers are logged so the loss is visible.
Start the local SignalR-compatible server using the database and session ID from the recording step:
go run ./cmd/replay \
-database f1-livetiming.db \
-session 20260902T212436.123456789Z \
-address :8080The server listens at:
http://localhost:8080
Each subscriber gets its own replay iterator, so every new subscriber receives the session from its first stored event. Subscribers do not share replay position.
A Go subscriber can connect to the replay server through the same thin client used for the live F1 service:
package main
import (
"context"
"fmt"
"log"
"github.com/bcdxn/f1/livetiming"
)
func main() {
ctx := context.Background()
client := livetiming.NewClient(
livetiming.WithBaseURL("http://localhost:8080"),
livetiming.UnsafeWithoutTLS(),
)
events, err := client.Stream(ctx)
if err != nil {
log.Fatal(err)
}
for event := range events {
if event.Err != nil {
log.Fatal(event.Err)
}
fmt.Println(string(event.Data))
}
}The client performs the SignalR handshake and subscription automatically. The replayed payloads are the original raw JSON SignalR messages, emitted in their recorded order.
Set the speed query parameter for an individual WebSocket subscriber. A speed of 1 uses the original timing, 2 is twice as fast, and 10 is ten times as fast:
client := livetiming.NewClient(
livetiming.WithBaseURL("http://localhost:8080"),
livetiming.UnsafeWithoutTLS(),
livetiming.WithWebSocketQuery("speed", "10"),
)The server applies this speed independently per subscriber. One subscriber can use real-time playback while another uses accelerated playback.
The speed must be a positive finite number. If it is omitted, the server uses speed 1.
Create one client per consumer, or call Stream independently for each consumer. Each connection negotiates separately and starts at the beginning of the selected recording:
replay server
|-- subscriber A: speed=1
|-- subscriber B: speed=10
`-- subscriber C: speed=100
The TUI reconstructs timing state in memory and uses the same client for live and replay connections. Start the replay server with a session from one of the recordings in data/, then connect the TUI at an accelerated speed:
go run ./cmd/replay \
-database data/monza-practice.db \
-session <session-id> \
-address :8080
go run ./cmd/tui \
--mode replay \
--address http://localhost:8080 \
--speed 10Use q or Ctrl+C to exit; use j/k, arrow keys, or page keys to navigate race-control messages. Replay is the deterministic development path: inspect the standings, mini-sectors, status, weather, and race-control ticker, then repeat the run after any rendering or state correction. Live mode uses the default F1 service:
go run ./cmd/tui --mode liveThe dashboard selects its timing table by session: free practice and testing use the existing mini-sector view, qualifying and sprint qualifying show best-lap sectors plus Q1/Q2/Q3 columns, and races and sprint races show interval, last lap, sectors, tyre, and best lap. The repository's captured sessions are practice sessions; qualifying Q1/Q2/Q3 rendering is covered by deterministic synthetic fixtures until a qualifying capture is available.
Run the reducer and view tests independently while iterating:
go test ./tui
go test -race ./...The replay server is transport-focused. Consumers can log events, decode them into application state, or forward them to another system without changing the stored event stream.
The TUI also displays Team Radio captures below the weather panel. Captures show the driver name and transcript when supplied; otherwise they show message.... Team Radio playback uses pure-Go MP3 decoding through Beep and ALSA output. The devcontainer includes the required ALSA development packages:
go run ./cmd/tui --mode replay --address http://localhost:8080 --speed 10Team Radio dialogs appear one second before playback, remain visible while the MP3 plays, and stay for two seconds after playback completes.
Count data-stream events in a recorded session. A single stored database row containing WeatherData, Position, and TimingAppData counts as three events. SignalR control/ping rows are excluded:
go run ./cmd/event-counter \
-database data/monza-qualifying.db \
-session 20260905T135428.431444000ZThe command prints both the stream-event count and the number of stored rows.
Run the tests:
go test ./...Run the race detector:
go test -race ./...Use a different server port if 8080 is already in use:
go run ./cmd/replay \
-database f1-livetiming.db \
-session <session-id> \
-address :9090