The OpenSky "Default bounding box" now follows where flying happens. A new "Automatic" picker mode (the default) resolves the live-map area from a location cascade — drone telemetry → phone GPS → browser geolocation → the user's Region country → Europe — instead of a fixed box. Manual presets and Custom coordinates still work. - Web App: new shared countries.js dataset (all countries + bbox, offline point→country); the bbox picker gains all European countries and an Automatic option (client pref prefs.autoBbox); the Region setting expands from 6 locale entries to all countries; the live map resolves the cascade each poll and sends it as ?bbox=. - API Server: the states endpoint accepts and validates a ?bbox= override (validBBox); the Web App BFF forwards the query; the hub relays new phoneLatitude/phoneLongitude telemetry to the Web App. - Fly App: reports the phone's own GPS (geolocator) alongside telemetry, used as the "your location" fallback. - API panel: the OpenSky bbox picker lists all European countries. Builds verified across web, panel, both Go modules and the Fly App APK. Region list, Automatic default and the cascade ?bbox= override verified in the browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
3.8 KiB
Go
123 lines
3.8 KiB
Go
package hub
|
|
|
|
import "encoding/json"
|
|
|
|
// Telemetry holds the latest flight-controller / battery values for a device.
|
|
// Pointers distinguish "not yet reported" (nil) from a genuine zero value.
|
|
type Telemetry struct {
|
|
SatelliteCount *int `json:"satelliteCount,omitempty"`
|
|
IsFlying *bool `json:"isFlying,omitempty"`
|
|
FlightMode *string `json:"flightMode,omitempty"`
|
|
Altitude *float64 `json:"altitude,omitempty"`
|
|
Latitude *float64 `json:"latitude,omitempty"`
|
|
Longitude *float64 `json:"longitude,omitempty"`
|
|
// Phone's own GPS (reported by the Fly App), distinct from the drone's fix
|
|
// above — used as a location fallback for the Web App's auto bounding box.
|
|
PhoneLatitude *float64 `json:"phoneLatitude,omitempty"`
|
|
PhoneLongitude *float64 `json:"phoneLongitude,omitempty"`
|
|
VelocityX *float64 `json:"velocityX,omitempty"`
|
|
VelocityY *float64 `json:"velocityY,omitempty"`
|
|
VelocityZ *float64 `json:"velocityZ,omitempty"`
|
|
BatteryPercent *int `json:"batteryPercent,omitempty"`
|
|
}
|
|
|
|
// DeviceState is the server's aggregated view of one app/drone.
|
|
type DeviceState struct {
|
|
DeviceID string `json:"deviceId"`
|
|
Online bool `json:"online"` // app's websocket is connected to the server
|
|
Connected bool `json:"connected"` // a drone is connected to the app
|
|
Model string `json:"model"`
|
|
Registration string `json:"registration"`
|
|
Telemetry Telemetry `json:"telemetry"`
|
|
LastSeenMs int64 `json:"lastSeenMs"`
|
|
}
|
|
|
|
// TrackPoint is one sample of the drone's GPS track (for the map trail).
|
|
type TrackPoint struct {
|
|
Lat float64 `json:"lat"`
|
|
Lng float64 `json:"lng"`
|
|
Alt float64 `json:"alt"`
|
|
TS int64 `json:"ts"`
|
|
}
|
|
|
|
// ServerToUI is the message a dashboard receives over /ws/ui.
|
|
type ServerToUI struct {
|
|
Type string `json:"type"` // "snapshot" | "update" | "removed"
|
|
Device *DeviceState `json:"device,omitempty"`
|
|
Devices []*DeviceState `json:"devices,omitempty"`
|
|
DeviceID string `json:"deviceId,omitempty"` // for "removed"
|
|
Event map[string]any `json:"event,omitempty"` // the raw device event that triggered this
|
|
TS int64 `json:"ts"`
|
|
}
|
|
|
|
// Command is what the server pushes down to a device over /ws/device.
|
|
type Command struct {
|
|
Type string `json:"type"` // always "command"
|
|
Command string `json:"command"`
|
|
Payload map[string]any `json:"payload,omitempty"`
|
|
TS int64 `json:"ts"`
|
|
}
|
|
|
|
// toFloat coerces a JSON-decoded value into a float64.
|
|
func toFloat(v any) (float64, bool) {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return n, true
|
|
case float32:
|
|
return float64(n), true
|
|
case int:
|
|
return float64(n), true
|
|
case int64:
|
|
return float64(n), true
|
|
case json.Number:
|
|
f, err := n.Float64()
|
|
return f, err == nil
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// toInt coerces a JSON-decoded value into an int.
|
|
func toInt(v any) (int, bool) {
|
|
if f, ok := toFloat(v); ok {
|
|
return int(f), true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// applyTelemetry copies any present telemetry fields from a raw event map.
|
|
func applyTelemetry(t *Telemetry, raw map[string]any) {
|
|
if v, ok := toInt(raw["satelliteCount"]); ok {
|
|
t.SatelliteCount = &v
|
|
}
|
|
if v, ok := raw["isFlying"].(bool); ok {
|
|
t.IsFlying = &v
|
|
}
|
|
if v, ok := raw["flightMode"].(string); ok {
|
|
t.FlightMode = &v
|
|
}
|
|
if v, ok := toFloat(raw["altitude"]); ok {
|
|
t.Altitude = &v
|
|
}
|
|
if v, ok := toFloat(raw["latitude"]); ok {
|
|
t.Latitude = &v
|
|
}
|
|
if v, ok := toFloat(raw["longitude"]); ok {
|
|
t.Longitude = &v
|
|
}
|
|
if v, ok := toFloat(raw["phoneLatitude"]); ok {
|
|
t.PhoneLatitude = &v
|
|
}
|
|
if v, ok := toFloat(raw["phoneLongitude"]); ok {
|
|
t.PhoneLongitude = &v
|
|
}
|
|
if v, ok := toFloat(raw["velocityX"]); ok {
|
|
t.VelocityX = &v
|
|
}
|
|
if v, ok := toFloat(raw["velocityY"]); ok {
|
|
t.VelocityY = &v
|
|
}
|
|
if v, ok := toFloat(raw["velocityZ"]); ok {
|
|
t.VelocityZ = &v
|
|
}
|
|
}
|