Files
rtnetmon/internal/starlink/starlink.go
T
clawbot 769511dec2
check / check (push) Failing after 4s
Add Starlink status lines for the physical gateway pane (closes #3) (#7)
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)
2026-09-22 20:56:01 +02:00

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)
}