check / check (push) Failing after 4s
Detection is a TCP connect to the dish endpoint bound to the physical interface; once a dish answers, get_status is read every 5s and two status lines render under the physical pane, red on disconnect or alert. No dish: nothing drawn, no RPC. Minimal vendored proto bindings for get_status only. Independent review passed (PR 7 comment 100012). Model: opus-4-8 (implementation and review); model: claude-fable-5 (merge)
82 lines
2.7 KiB
Go
82 lines
2.7 KiB
Go
// Package starlink detects a Starlink dish on the local network and reads a
|
|
// short status summary from the dish's local gRPC endpoint. Detection and
|
|
// the status fetch sit behind the Client interface, so the monitor is
|
|
// tested with a fake and needs no dish and no network. Render turns a
|
|
// Status into the two lines the UI draws and is a pure function.
|
|
package starlink
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Address is the dish's fixed local gRPC endpoint. Every dish answers here
|
|
// from behind the Starlink router, whatever the LAN's own addressing is, so
|
|
// this is not the default gateway (that is the router).
|
|
const Address = "192.168.100.1:9200"
|
|
|
|
// StateConnected is the dish state string that means the link is up; any
|
|
// other state is treated as an alert condition by Render.
|
|
const StateConnected = "CONNECTED"
|
|
|
|
// Units used when reducing the dish's raw values to display values.
|
|
const (
|
|
percentScale = 100.0
|
|
bitsPerMegabit = 1_000_000.0
|
|
hoursPerDay = 24
|
|
minutesPerHour = 60
|
|
)
|
|
|
|
// Status is the subset of the dish's get_status response that rtnetmon
|
|
// shows. Throughput is megabits per second; obstruction and drop are
|
|
// percentages on a 0..100 scale.
|
|
type Status struct {
|
|
State string
|
|
Uptime time.Duration
|
|
ObstructionPct float64
|
|
Alerts int
|
|
PopPingMs float64
|
|
PopPingDropPct float64
|
|
DownlinkMbps float64
|
|
UplinkMbps float64
|
|
}
|
|
|
|
// Client probes for a dish and reads its status. The real implementation
|
|
// speaks gRPC to Address over one bound interface; tests use a fake.
|
|
type Client interface {
|
|
// Probe reports whether a dish answers on Address over the bound
|
|
// interface. It is cheap: a TCP connect, no gRPC call.
|
|
Probe(ctx context.Context) bool
|
|
// Status fetches the dish's current status.
|
|
Status(ctx context.Context) (Status, error)
|
|
}
|
|
|
|
// Render formats a Status as the two status lines drawn under the physical
|
|
// pane, and reports whether they warrant an alert (red) color: the dish is
|
|
// not connected, or an alert is active.
|
|
func Render(s Status) (string, string, bool) {
|
|
line1 := fmt.Sprintf(
|
|
"Starlink: %s uptime %s obstruction %.1f%% alerts %d",
|
|
s.State, formatUptime(s.Uptime), s.ObstructionPct, s.Alerts)
|
|
line2 := fmt.Sprintf(
|
|
" pop ping %.0fms / drop %.1f%% down %.1fMbps / up %.1fMbps",
|
|
s.PopPingMs, s.PopPingDropPct, s.DownlinkMbps, s.UplinkMbps)
|
|
alert := s.State != StateConnected || s.Alerts > 0
|
|
|
|
return line1, line2, alert
|
|
}
|
|
|
|
// formatUptime renders a duration as whole days, hours and minutes.
|
|
func formatUptime(d time.Duration) string {
|
|
if d < 0 {
|
|
d = 0
|
|
}
|
|
|
|
days := int64(d / (hoursPerDay * time.Hour))
|
|
hours := int64(d/time.Hour) % hoursPerDay
|
|
mins := int64(d/time.Minute) % minutesPerHour
|
|
|
|
return fmt.Sprintf("%dd %dh %dm", days, hours, mins)
|
|
}
|