skyview/internal/client/dump1090.go
Ole-Morten Duesund 1425f0a018 Restructure assets to top-level package and add Reset Map button
- Move assets from internal/assets to top-level assets/ package for clean embed directive
- Consolidate all static files in single location (assets/static/)
- Remove duplicate static file locations to maintain single source of truth
- Add Reset Map button to map controls with full functionality
- Implement resetMap() method to return map to calculated origin position
- Store origin in this.mapOrigin for reset functionality
- Fix go:embed pattern to work without parent directory references

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-24 00:57:49 +02:00

267 lines
6 KiB
Go

package client
import (
"bufio"
"context"
"fmt"
"log"
"net"
"sync"
"time"
"skyview/internal/config"
"skyview/internal/parser"
)
type Dump1090Client struct {
config *config.Config
aircraftMap map[string]*parser.Aircraft
mutex sync.RWMutex
subscribers []chan parser.AircraftData
subMutex sync.RWMutex
}
func NewDump1090Client(cfg *config.Config) *Dump1090Client {
return &Dump1090Client{
config: cfg,
aircraftMap: make(map[string]*parser.Aircraft),
subscribers: make([]chan parser.AircraftData, 0),
}
}
func (c *Dump1090Client) Start(ctx context.Context) error {
go c.startDataStream(ctx)
go c.startPeriodicBroadcast(ctx)
go c.startCleanup(ctx)
return nil
}
func (c *Dump1090Client) startDataStream(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
if err := c.connectAndRead(ctx); err != nil {
log.Printf("Connection error: %v, retrying in 5s", err)
time.Sleep(5 * time.Second)
}
}
}
}
func (c *Dump1090Client) connectAndRead(ctx context.Context) error {
address := fmt.Sprintf("%s:%d", c.config.Dump1090.Host, c.config.Dump1090.DataPort)
conn, err := net.Dial("tcp", address)
if err != nil {
return fmt.Errorf("failed to connect to %s: %w", address, err)
}
defer conn.Close()
log.Printf("Connected to dump1090 at %s", address)
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
select {
case <-ctx.Done():
return nil
default:
line := scanner.Text()
c.processLine(line)
}
}
return scanner.Err()
}
func (c *Dump1090Client) processLine(line string) {
aircraft, err := parser.ParseSBS1Line(line)
if err != nil || aircraft == nil {
return
}
c.mutex.Lock()
if existing, exists := c.aircraftMap[aircraft.Hex]; exists {
c.updateExistingAircraft(existing, aircraft)
} else {
c.aircraftMap[aircraft.Hex] = aircraft
}
c.mutex.Unlock()
}
func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraft) {
existing.LastSeen = update.LastSeen
existing.Messages++
if update.Flight != "" {
existing.Flight = update.Flight
}
if update.Altitude != 0 {
existing.Altitude = update.Altitude
}
if update.GroundSpeed != 0 {
existing.GroundSpeed = update.GroundSpeed
}
if update.Track != 0 {
existing.Track = update.Track
}
if update.Latitude != 0 && update.Longitude != 0 {
existing.Latitude = update.Latitude
existing.Longitude = update.Longitude
// Add to track history if position changed significantly
if c.shouldAddTrackPoint(existing, update) {
trackPoint := parser.TrackPoint{
Timestamp: update.LastSeen,
Latitude: update.Latitude,
Longitude: update.Longitude,
Altitude: update.Altitude,
Speed: update.GroundSpeed,
Track: update.Track,
}
existing.TrackHistory = append(existing.TrackHistory, trackPoint)
// Keep only last 200 points (about 3-4 hours at 1 point/minute)
if len(existing.TrackHistory) > 200 {
existing.TrackHistory = existing.TrackHistory[1:]
}
}
}
if update.VertRate != 0 {
existing.VertRate = update.VertRate
}
if update.Squawk != "" {
existing.Squawk = update.Squawk
}
existing.OnGround = update.OnGround
// Preserve country and registration
if update.Country != "" && update.Country != "Unknown" {
existing.Country = update.Country
}
if update.Registration != "" {
existing.Registration = update.Registration
}
}
func (c *Dump1090Client) shouldAddTrackPoint(existing, update *parser.Aircraft) bool {
// Add track point if:
// 1. No history yet
if len(existing.TrackHistory) == 0 {
return true
}
lastPoint := existing.TrackHistory[len(existing.TrackHistory)-1]
// 2. At least 30 seconds since last point
if time.Since(lastPoint.Timestamp) < 30*time.Second {
return false
}
// 3. Position changed by at least 0.001 degrees (~100m)
latDiff := existing.Latitude - lastPoint.Latitude
lonDiff := existing.Longitude - lastPoint.Longitude
distanceChange := latDiff*latDiff + lonDiff*lonDiff
return distanceChange > 0.000001 // ~0.001 degrees squared
}
func (c *Dump1090Client) GetAircraftData() parser.AircraftData {
c.mutex.RLock()
defer c.mutex.RUnlock()
aircraftMap := make(map[string]parser.Aircraft)
totalMessages := 0
for hex, aircraft := range c.aircraftMap {
aircraftMap[hex] = *aircraft
totalMessages += aircraft.Messages
}
return parser.AircraftData{
Now: time.Now().Unix(),
Messages: totalMessages,
Aircraft: aircraftMap,
}
}
func (c *Dump1090Client) Subscribe() <-chan parser.AircraftData {
c.subMutex.Lock()
defer c.subMutex.Unlock()
ch := make(chan parser.AircraftData, 10)
c.subscribers = append(c.subscribers, ch)
return ch
}
func (c *Dump1090Client) startPeriodicBroadcast(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
data := c.GetAircraftData()
c.broadcastToSubscribers(data)
}
}
}
func (c *Dump1090Client) broadcastToSubscribers(data parser.AircraftData) {
c.subMutex.RLock()
defer c.subMutex.RUnlock()
for i, ch := range c.subscribers {
select {
case ch <- data:
default:
close(ch)
c.subMutex.RUnlock()
c.subMutex.Lock()
c.subscribers = append(c.subscribers[:i], c.subscribers[i+1:]...)
c.subMutex.Unlock()
c.subMutex.RLock()
}
}
}
func (c *Dump1090Client) startCleanup(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
c.cleanupStaleAircraft()
}
}
}
func (c *Dump1090Client) cleanupStaleAircraft() {
c.mutex.Lock()
defer c.mutex.Unlock()
cutoff := time.Now().Add(-2 * time.Minute)
trackCutoff := time.Now().Add(-24 * time.Hour)
for hex, aircraft := range c.aircraftMap {
if aircraft.LastSeen.Before(cutoff) {
delete(c.aircraftMap, hex)
} else {
// Clean up old track points (keep last 24 hours)
validTracks := make([]parser.TrackPoint, 0)
for _, point := range aircraft.TrackHistory {
if point.Timestamp.After(trackCutoff) {
validTracks = append(validTracks, point)
}
}
aircraft.TrackHistory = validTracks
}
}
}