Skip to content

Commit a1b5fc5

Browse files
authored
create obstruction map video (#23)
1 parent f91239b commit a1b5fc5

File tree

9 files changed

+699
-24
lines changed

9 files changed

+699
-24
lines changed

cmd/obstructionMapVideo/grpc.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"image"
8+
"image/color"
9+
"image/png"
10+
"time"
11+
12+
"google.golang.org/grpc"
13+
"google.golang.org/grpc/credentials/insecure"
14+
15+
device "github.com/clarkzjw/starlink-grpc-golang/pkg/spacex.com/api/device"
16+
"github.com/pbnjay/pixfont"
17+
)
18+
19+
var (
20+
defaultDishAddress = "192.168.100.1:9200"
21+
grpcTimeout = 5 * time.Second
22+
GRPC_ADDR_PORT string
23+
DURATION string
24+
DATA_DIR string
25+
FPS int
26+
)
27+
28+
type Exporter struct {
29+
Conn *grpc.ClientConn
30+
Client device.DeviceClient
31+
32+
DishID string
33+
CountryCode string
34+
}
35+
36+
func NewGrpcClient(address string) (*Exporter, error) {
37+
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
38+
if err != nil {
39+
return nil, fmt.Errorf("connect to Starlink dish gRPC interface failed: %s", err.Error())
40+
}
41+
42+
defer func() {
43+
if err != nil {
44+
conn.Close()
45+
}
46+
}()
47+
48+
ctx, cancel := context.WithTimeout(context.Background(), grpcTimeout)
49+
defer cancel()
50+
51+
client := device.NewDeviceClient(conn)
52+
resp, err := client.Handle(ctx, &device.Request{
53+
Request: &device.Request_GetDeviceInfo{},
54+
})
55+
if err != nil {
56+
return nil, fmt.Errorf("gRPC GetDeviceInfo failed: %s", err.Error())
57+
}
58+
59+
deviceInfo := resp.GetGetDeviceInfo().GetDeviceInfo()
60+
if deviceInfo == nil {
61+
return nil, fmt.Errorf("gRPC GetDeviceInfo failed: deviceInfo is nil")
62+
}
63+
64+
return &Exporter{
65+
Conn: conn,
66+
Client: client,
67+
DishID: deviceInfo.GetId(),
68+
CountryCode: deviceInfo.GetCountryCode(),
69+
}, nil
70+
}
71+
72+
func (e *Exporter) CollectDishStatus() *StarlinkGetStatusResponse {
73+
req := &device.Request{
74+
Request: &device.Request_GetStatus{},
75+
}
76+
77+
ctx, cancel := context.WithTimeout(context.Background(), grpcTimeout)
78+
defer cancel()
79+
resp, err := e.Client.Handle(ctx, req)
80+
if err != nil {
81+
fmt.Printf("gRPC GetStatus failed: %s", err.Error())
82+
return nil
83+
}
84+
85+
timestamp := time.Now().Format(time.RFC3339)
86+
87+
dishStatus := resp.GetDishGetStatus()
88+
dishStatusResp := &StarlinkGetStatusResponse{
89+
Timestamp: timestamp,
90+
HardwareVersion: dishStatus.DeviceInfo.GetHardwareVersion(),
91+
SoftwareVersion: dishStatus.DeviceInfo.GetSoftwareVersion(),
92+
CountryCode: dishStatus.DeviceInfo.GetCountryCode(),
93+
BuildID: dishStatus.DeviceInfo.GetBuildId(),
94+
DeviceUptimeSeconds: dishStatus.DeviceState.GetUptimeS(),
95+
ObstructionFractionObstructed: dishStatus.ObstructionStats.GetFractionObstructed(),
96+
ObstructionTimeObstructed: dishStatus.ObstructionStats.GetTimeObstructed(),
97+
DownlinkThroughputBps: dishStatus.GetDownlinkThroughputBps(),
98+
UplinkThroughputBps: dishStatus.GetUplinkThroughputBps(),
99+
PopPingLatencyMs: dishStatus.GetPopPingLatencyMs(),
100+
PhyRxBeamSnrAvg: dishStatus.GetPhyRxBeamSnrAvg(),
101+
}
102+
return dishStatusResp
103+
}
104+
105+
type StarlinkGetStatusResponse struct {
106+
Timestamp string
107+
HardwareVersion string
108+
SoftwareVersion string
109+
CountryCode string
110+
BuildID string
111+
DeviceUptimeSeconds uint64
112+
ObstructionFractionObstructed float32
113+
ObstructionTimeObstructed float32
114+
DownlinkThroughputBps float32
115+
UplinkThroughputBps float32
116+
PopPingLatencyMs float32
117+
PhyRxBeamSnrAvg float32
118+
}
119+
120+
func (e *Exporter) CollectDishObstructionMap() *StarlinkGetObstructionMapResponse {
121+
req := &device.Request{
122+
Request: &device.Request_DishGetObstructionMap{},
123+
}
124+
125+
ctx, cancel := context.WithTimeout(context.Background(), grpcTimeout)
126+
defer cancel()
127+
resp, err := e.Client.Handle(ctx, req)
128+
if err != nil {
129+
fmt.Printf("gRPC GetObstructionMap failed: %s", err.Error())
130+
return nil
131+
}
132+
133+
dishObstructionMap := resp.GetDishGetObstructionMap()
134+
rows := int(dishObstructionMap.NumRows)
135+
cols := int(dishObstructionMap.NumCols)
136+
referenceFrame := dishObstructionMap.GetMapReferenceFrame().String()
137+
data := dishObstructionMap.Snr
138+
139+
upLeft := image.Point{0, 0}
140+
lowRight := image.Point{cols * 2, rows * 2}
141+
142+
img := image.NewRGBA(image.Rectangle{upLeft, lowRight})
143+
144+
for x := 0; x < cols*2; x++ {
145+
for y := 0; y < rows*2; y++ {
146+
img.Set(x, y, color.Black)
147+
}
148+
}
149+
150+
offsetX := cols / 2
151+
offsetY := rows / 2
152+
153+
for x := 0; x < cols; x++ {
154+
for y := 0; y < rows; y++ {
155+
snr := data[y*cols+x]
156+
if snr > 1 {
157+
snr = 1.0
158+
}
159+
if snr == -1 {
160+
continue
161+
} else if snr >= 0 {
162+
// use the same image color style as in starlink-grpc-tools
163+
// https://github.com/sparky8512/starlink-grpc-tools/blob/a3860e0a73d0b2280eed92eb8a2a97de0ea5fe43/dish_obstruction_map.py#L59-L87
164+
r := 255
165+
g := snr * 255
166+
b := snr * 255
167+
alpha := 255
168+
img.Set(x+offsetX, y+offsetY, color.RGBA{uint8(r), uint8(g), uint8(b), uint8(alpha)})
169+
}
170+
}
171+
}
172+
173+
timestamp := time.Now().Format(time.RFC3339)
174+
pixfont.DrawString(img, 10, 10, timestamp, color.RGBA{255, 255, 255, 255})
175+
176+
var buf bytes.Buffer
177+
if err := png.Encode(&buf, img); err != nil {
178+
fmt.Printf("Failed to encode image: %s", err.Error())
179+
return nil
180+
}
181+
182+
dishObstructionMapResp := &StarlinkGetObstructionMapResponse{
183+
Timestamp: timestamp,
184+
MapReferenceFrame: referenceFrame,
185+
Rows: rows,
186+
Cols: cols,
187+
Data: buf.Bytes(),
188+
}
189+
return dishObstructionMapResp
190+
}
191+
192+
type StarlinkGetObstructionMapResponse struct {
193+
Timestamp string
194+
MapReferenceFrame string
195+
Rows int
196+
Cols int
197+
Data []byte
198+
}

cmd/obstructionMapVideo/main.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"os"
8+
"os/exec"
9+
"time"
10+
)
11+
12+
func getTimeString() string {
13+
return time.Now().UTC().Format("2006-01-02-15-04-05")
14+
}
15+
16+
func createVideo(dataDir string, fps int) {
17+
videoFile := fmt.Sprintf("%s/obstruction-map-video-%s.mp4", dataDir, getTimeString())
18+
cmd := exec.Command("ffmpeg",
19+
"-framerate", fmt.Sprintf("%d", fps),
20+
"-pattern_type", "glob",
21+
"-i", fmt.Sprintf("%s/*.png", dataDir),
22+
"-c:v", "libx264",
23+
"-pix_fmt", "yuv420p",
24+
videoFile,
25+
)
26+
stdout, err := cmd.CombinedOutput()
27+
if err != nil {
28+
log.Fatalf("Failed to create video: %s\nOutput: %s", err.Error(), stdout)
29+
return
30+
}
31+
fmt.Printf("Video created: %s\n", videoFile)
32+
}
33+
34+
func main() {
35+
flag.StringVar(&GRPC_ADDR_PORT, "addr_port", defaultDishAddress, "gRPC address and port of the Starlink dish")
36+
flag.StringVar(&DURATION, "duration", "10s", "Duration for the obstruction map video")
37+
flag.StringVar(&DATA_DIR, "data_dir", "./obstructionMapData", "Directory to save the obstruction map frames")
38+
flag.IntVar(&FPS, "fps", 10, "Frames per second for the video")
39+
flag.Parse()
40+
41+
if _, err := exec.LookPath("ffmpeg"); err != nil {
42+
log.Fatal("ffmpeg is not installed. Please install ffmpeg to create videos.")
43+
}
44+
45+
fmt.Printf("Using gRPC address: %s\n", GRPC_ADDR_PORT)
46+
fmt.Printf("Duration for video: %s\n", DURATION)
47+
48+
if GRPC_ADDR_PORT == "" {
49+
GRPC_ADDR_PORT = defaultDishAddress
50+
}
51+
durationSecond, err := time.ParseDuration(DURATION)
52+
if err != nil {
53+
fmt.Printf("Error parsing duration: %s\n", err)
54+
return
55+
}
56+
fmt.Printf("Duration in seconds: %.0f\n", durationSecond.Seconds())
57+
58+
startTime := getTimeString()
59+
DATA_DIR = fmt.Sprintf("%s/%s", DATA_DIR, startTime)
60+
61+
if _, err := os.Stat(DATA_DIR); os.IsNotExist(err) {
62+
err = os.MkdirAll(DATA_DIR, 0755)
63+
if err != nil {
64+
log.Fatalf("Error creating data directory: %s\n", err)
65+
}
66+
}
67+
68+
grpcClient, err := NewGrpcClient(GRPC_ADDR_PORT)
69+
if err != nil {
70+
log.Println("Error creating gRPC client: ", err)
71+
return
72+
}
73+
74+
timeNow := time.Now()
75+
timeEnd := timeNow.Add(durationSecond)
76+
77+
for time.Now().Before(timeEnd) {
78+
obstructionMap := grpcClient.CollectDishObstructionMap()
79+
if obstructionMap == nil {
80+
log.Println("Failed to collect obstruction map")
81+
return
82+
}
83+
84+
datetime := getTimeString()
85+
filename := fmt.Sprintf("%s/obstruction-map-%s.png", DATA_DIR, datetime)
86+
fmt.Printf("Saving obstruction map to %s\n", filename)
87+
f, _ := os.Create(filename)
88+
defer f.Close()
89+
_, err = f.Write(obstructionMap.Data)
90+
if err != nil {
91+
log.Println("Error writing obstruction map: ", err)
92+
}
93+
time.Sleep(time.Second * 1)
94+
}
95+
96+
createVideo(DATA_DIR, FPS)
97+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ toolchain go1.23.1
77
require (
88
github.com/clarkzjw/starlink-grpc-golang v0.0.0-20250411070715-a5267431cc5f
99
github.com/go-co-op/gocron/v2 v2.16.1
10+
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567
1011
google.golang.org/grpc v1.71.1
1112
gopkg.in/ini.v1 v1.67.0
1213
)

go.sum

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/clarkzjw/starlink-grpc-golang v0.0.0-20250314081002-7b0866824918 h1:uQpHgUcM7fHBrYMOcJwrdg2cSqbZNEcfc2hFKJEuj/c=
2-
github.com/clarkzjw/starlink-grpc-golang v0.0.0-20250314081002-7b0866824918/go.mod h1:iz7H7VRki7Z0uDdoeKQdeobXfQrF4htMOjQn081AzGQ=
31
github.com/clarkzjw/starlink-grpc-golang v0.0.0-20250411070715-a5267431cc5f h1:KnVKgqpo5LIIDcmot6SP+A/fxebZJoPcS7e037PUlKk=
42
github.com/clarkzjw/starlink-grpc-golang v0.0.0-20250411070715-a5267431cc5f/go.mod h1:iz7H7VRki7Z0uDdoeKQdeobXfQrF4htMOjQn081AzGQ=
53
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -18,46 +16,38 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
1816
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
1917
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
2018
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
19+
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 h1:pKjmNHL7BCXhgsnSlN6Ov3WAN2jbJMCx6IvrMN9GNfc=
20+
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567/go.mod h1:ytYavTmrpWG4s7UOfDhP6m4ASL5XA66nrOcUn1e2M78=
2121
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2222
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2323
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
2424
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
2525
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
2626
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
27-
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
28-
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
29-
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
30-
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
31-
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
32-
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
33-
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
34-
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
35-
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
36-
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
27+
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
28+
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
29+
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
30+
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
31+
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
32+
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
33+
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
34+
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
35+
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
36+
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
37+
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
38+
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
3739
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
3840
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
39-
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
40-
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
4141
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
4242
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
43-
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
44-
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
4543
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
4644
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
47-
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
48-
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
4945
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
5046
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
51-
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
52-
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
5347
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a h1:GIqLhp/cYUkuGuiT+vJk8vhOP86L4+SP5j8yXgeVpvI=
5448
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
55-
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
56-
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
5749
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
5850
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
59-
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
60-
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
6151
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
6252
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
6353
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=

vendor/github.com/pbnjay/pixfont/LICENSE.md

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)