2025-08-23 22:09:37 +02:00
|
|
|
package parser
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2025-08-23 22:52:16 +02:00
|
|
|
type TrackPoint struct {
|
|
|
|
|
Timestamp time.Time `json:"timestamp"`
|
|
|
|
|
Latitude float64 `json:"lat"`
|
|
|
|
|
Longitude float64 `json:"lon"`
|
|
|
|
|
Altitude int `json:"altitude"`
|
|
|
|
|
Speed int `json:"speed"`
|
|
|
|
|
Track int `json:"track"`
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 22:09:37 +02:00
|
|
|
type Aircraft struct {
|
2025-08-24 00:57:49 +02:00
|
|
|
Hex string `json:"hex"`
|
|
|
|
|
Flight string `json:"flight,omitempty"`
|
|
|
|
|
Altitude int `json:"alt_baro,omitempty"`
|
|
|
|
|
GroundSpeed int `json:"gs,omitempty"`
|
|
|
|
|
Track int `json:"track,omitempty"`
|
|
|
|
|
Latitude float64 `json:"lat,omitempty"`
|
|
|
|
|
Longitude float64 `json:"lon,omitempty"`
|
|
|
|
|
VertRate int `json:"vert_rate,omitempty"`
|
|
|
|
|
Squawk string `json:"squawk,omitempty"`
|
|
|
|
|
Emergency bool `json:"emergency,omitempty"`
|
|
|
|
|
OnGround bool `json:"on_ground,omitempty"`
|
|
|
|
|
LastSeen time.Time `json:"last_seen"`
|
|
|
|
|
Messages int `json:"messages"`
|
2025-08-23 22:52:16 +02:00
|
|
|
TrackHistory []TrackPoint `json:"track_history,omitempty"`
|
2025-08-24 00:57:49 +02:00
|
|
|
RSSI float64 `json:"rssi,omitempty"`
|
|
|
|
|
Country string `json:"country,omitempty"`
|
|
|
|
|
Registration string `json:"registration,omitempty"`
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AircraftData struct {
|
|
|
|
|
Now int64 `json:"now"`
|
|
|
|
|
Messages int `json:"messages"`
|
|
|
|
|
Aircraft map[string]Aircraft `json:"aircraft"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ParseSBS1Line(line string) (*Aircraft, error) {
|
|
|
|
|
parts := strings.Split(strings.TrimSpace(line), ",")
|
|
|
|
|
if len(parts) < 22 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 23:20:31 +02:00
|
|
|
// messageType := parts[1]
|
|
|
|
|
// Accept all message types to get complete data
|
|
|
|
|
// MSG types: 1=ES_IDENT_AND_CATEGORY, 2=ES_SURFACE_POS, 3=ES_AIRBORNE_POS
|
|
|
|
|
// 4=ES_AIRBORNE_VEL, 5=SURVEILLANCE_ALT, 6=SURVEILLANCE_ID, 7=AIR_TO_AIR, 8=ALL_CALL_REPLY
|
2025-08-23 22:09:37 +02:00
|
|
|
|
|
|
|
|
aircraft := &Aircraft{
|
|
|
|
|
Hex: strings.TrimSpace(parts[4]),
|
|
|
|
|
LastSeen: time.Now(),
|
|
|
|
|
Messages: 1,
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 23:20:31 +02:00
|
|
|
// Different message types contain different fields
|
|
|
|
|
// Always try to extract what's available
|
2025-08-23 22:09:37 +02:00
|
|
|
if parts[10] != "" {
|
|
|
|
|
aircraft.Flight = strings.TrimSpace(parts[10])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if parts[11] != "" {
|
|
|
|
|
if alt, err := strconv.Atoi(parts[11]); err == nil {
|
|
|
|
|
aircraft.Altitude = alt
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if parts[12] != "" {
|
|
|
|
|
if gs, err := strconv.Atoi(parts[12]); err == nil {
|
|
|
|
|
aircraft.GroundSpeed = gs
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if parts[13] != "" {
|
2025-08-23 23:20:31 +02:00
|
|
|
if track, err := strconv.ParseFloat(parts[13], 64); err == nil {
|
|
|
|
|
aircraft.Track = int(track)
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if parts[14] != "" && parts[15] != "" {
|
|
|
|
|
if lat, err := strconv.ParseFloat(parts[14], 64); err == nil {
|
|
|
|
|
aircraft.Latitude = lat
|
|
|
|
|
}
|
|
|
|
|
if lon, err := strconv.ParseFloat(parts[15], 64); err == nil {
|
|
|
|
|
aircraft.Longitude = lon
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if parts[16] != "" {
|
|
|
|
|
if vr, err := strconv.Atoi(parts[16]); err == nil {
|
|
|
|
|
aircraft.VertRate = vr
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if parts[17] != "" {
|
|
|
|
|
aircraft.Squawk = strings.TrimSpace(parts[17])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if parts[21] != "" {
|
|
|
|
|
aircraft.OnGround = parts[21] == "1"
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 23:20:31 +02:00
|
|
|
aircraft.Country = getCountryFromICAO(aircraft.Hex)
|
|
|
|
|
aircraft.Registration = getRegistrationFromICAO(aircraft.Hex)
|
|
|
|
|
|
2025-08-23 22:09:37 +02:00
|
|
|
return aircraft, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 23:20:31 +02:00
|
|
|
func getCountryFromICAO(icao string) string {
|
|
|
|
|
if len(icao) < 6 {
|
|
|
|
|
return "Unknown"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prefix := icao[:1]
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:20:31 +02:00
|
|
|
switch prefix {
|
|
|
|
|
case "4":
|
|
|
|
|
return getCountryFrom4xxxx(icao)
|
|
|
|
|
case "A":
|
|
|
|
|
return "United States"
|
|
|
|
|
case "C":
|
|
|
|
|
return "Canada"
|
|
|
|
|
case "D":
|
|
|
|
|
return "Germany"
|
|
|
|
|
case "F":
|
|
|
|
|
return "France"
|
|
|
|
|
case "G":
|
|
|
|
|
return "United Kingdom"
|
|
|
|
|
case "I":
|
|
|
|
|
return "Italy"
|
|
|
|
|
case "J":
|
|
|
|
|
return "Japan"
|
|
|
|
|
case "P":
|
|
|
|
|
return getPCountry(icao)
|
|
|
|
|
case "S":
|
|
|
|
|
return getSCountry(icao)
|
|
|
|
|
case "O":
|
|
|
|
|
return getOCountry(icao)
|
|
|
|
|
default:
|
|
|
|
|
return "Unknown"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getCountryFrom4xxxx(icao string) string {
|
|
|
|
|
if len(icao) >= 2 {
|
|
|
|
|
switch icao[:2] {
|
|
|
|
|
case "40":
|
|
|
|
|
return "United Kingdom"
|
|
|
|
|
case "44":
|
|
|
|
|
return "Austria"
|
|
|
|
|
case "45":
|
|
|
|
|
return "Denmark"
|
|
|
|
|
case "46":
|
|
|
|
|
return "Germany"
|
|
|
|
|
case "47":
|
|
|
|
|
return "Germany"
|
|
|
|
|
case "48":
|
|
|
|
|
return "Netherlands"
|
|
|
|
|
case "49":
|
|
|
|
|
return "Netherlands"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "Europe"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getPCountry(icao string) string {
|
|
|
|
|
if len(icao) >= 2 {
|
|
|
|
|
switch icao[:2] {
|
|
|
|
|
case "PH":
|
|
|
|
|
return "Netherlands"
|
|
|
|
|
case "PJ":
|
|
|
|
|
return "Netherlands Antilles"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "Unknown"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getSCountry(icao string) string {
|
|
|
|
|
if len(icao) >= 2 {
|
|
|
|
|
switch icao[:2] {
|
|
|
|
|
case "SE":
|
|
|
|
|
return "Sweden"
|
|
|
|
|
case "SX":
|
|
|
|
|
return "Greece"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "Unknown"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getOCountry(icao string) string {
|
|
|
|
|
if len(icao) >= 2 {
|
|
|
|
|
switch icao[:2] {
|
|
|
|
|
case "OO":
|
|
|
|
|
return "Belgium"
|
|
|
|
|
case "OH":
|
|
|
|
|
return "Finland"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "Unknown"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getRegistrationFromICAO(icao string) string {
|
|
|
|
|
// This is a simplified conversion - real registration lookup would need a database
|
|
|
|
|
country := getCountryFromICAO(icao)
|
|
|
|
|
switch country {
|
|
|
|
|
case "Germany":
|
|
|
|
|
return "D-" + icao[2:]
|
|
|
|
|
case "United Kingdom":
|
|
|
|
|
return "G-" + icao[2:]
|
|
|
|
|
case "France":
|
|
|
|
|
return "F-" + icao[2:]
|
|
|
|
|
case "Netherlands":
|
|
|
|
|
return "PH-" + icao[2:]
|
|
|
|
|
case "Sweden":
|
|
|
|
|
return "SE-" + icao[2:]
|
|
|
|
|
default:
|
|
|
|
|
return icao
|
|
|
|
|
}
|
|
|
|
|
}
|