Add Starlink status lines for the physical gateway pane (closes #3) (#7)
check / check (push) Failing after 4s
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)
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user