/starlink-rs

Rust client implementation to the gRPC endpoint exposed by the SpaceX Starlink user terminal

Primary LanguageRustApache License 2.0Apache-2.0

starlink-rs

CI crates.io docs.rs license: MIT/Apache-2.0

Rust client implementation to the gRPC endpoint exposed by the SpaceX Starlink user terminal.

Disclaimer: If you're Elon or another SpaceX-affiliated authority and don't want this to exist, please just contact me.

Background

The Starlink dish exposes an unauthenticated gRPC HTTP/2 server on its local network under 192.168.100.1:9200 that allows for server reflection. This contains the (probably still flawed) Protobuf definitions reversed out of it as well as a Rust client implementation being able to talk to the dish, for science.

The dish exposes two methods (as far as I could tell); one for request/response and one for streams:

service Device {
    rpc Handle (.SpaceX.API.Device.Request) returns (.SpaceX.API.Device.Response) {}
    rpc Stream (stream .SpaceX.API.Device.ToDevice) returns (stream .SpaceX.API.Device.FromDevice) {}
}

Example Usage

Working examples for request/response and streaming communication are in the examples directory.

Request / Response

use starlink::proto::space_x::api::device::{device_client::DeviceClient, request, GetStatusRequest, Request};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = DeviceClient::connect("http://192.168.100.1:9200").await?;

    let request = tonic::Request::new(Request {
        id: None,
        epoch_id: None,
        target_id: None,
        request: Some(request::Request::GetStatus(GetStatusRequest {})),
    });

    let response = client.handle(request).await?;

    dbg!(response);

    Ok(())
}
cargo run --example request_response

Prints something like:

Response {
    metadata: MetadataMap {
        headers: {
            "content-type": "application/grpc",
            "grpc-status": "0",
            "grpc-message": "",
        },
    },
    message: Response {
        id: None,
        status: None,
        response: Some(
            DishGetStatus(
                DishGetStatusResponse {
                    device_info: Some(
                        DeviceInfo {
                            id: Some(
                                "<my-ID>",
                            ),
                            hardware_version: Some(
                                "rev1_pre_production",
                            ),
                            software_version: Some(
                                "1f86ec34-34ea-4e7a-9758-3842e72422fb.release",
                            ),
                            country_code: Some(
                                "DE",
                            ),
                            utc_offset_s: Some(
                                1,
                            ),
                        },
                    ),
                    device_state: Some(
                        DeviceState {
                            uptime_s: Some(
                                26115,
                            ),
                        },
                    ),
                    state: Some(
                        Connected,
                    ),
                    alerts: Some(
                        DishAlerts {
                            motors_stuck: None,
                            thermal_throttle: None,
                            thermal_shutdown: None,
                            mast_not_near_vertical: None,
                            unexpected_location: None,
                            slow_ethernet_speeds: None,
                        },
                    ),
                    snr: Some(
                        6.0,
                    ),
                    seconds_to_first_nonempty_slot: None,
                    pop_ping_drop_rate: None,
                    downlink_throughput_bps: Some(
                        8584784.0,
                    ),
                    uplink_throughput_bps: Some(
                        311510.97,
                    ),
                    pop_ping_latency_ms: Some(
                        38.857143,
                    ),
                    obstruction_stats: Some(
                        DishObstructionStats {
                            currently_obstructed: None,
                            fraction_obstructed: Some(
                                0.0010516815,
                            ),
                            last_24h_obstructed_s: Some(
                                72.0,
                            ),
                            valid_s: Some(
                                21260.67,
                            ),
                            wedge_fraction_obstructed: [
                                1.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                            ],
                            wedge_abs_fraction_obstructed: [
                                0.0010516815,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                                0.0,
                            ],
                        },
                    ),
                },
            ),
        ),
    },
}

Streaming

use async_stream::stream;
use std::time::Duration;
use tokio::time::sleep;

use starlink::proto::space_x::api::device::{
    device_client::DeviceClient,
    request,
    to_device,
    GetStatusRequest,
    Request,
    ToDevice,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = DeviceClient::connect("http://192.168.100.1:9200").await?;

    let request_stream = stream! {
        loop {
            yield ToDevice {
                message: Some(to_device::Message::Request(Request {
                    id: None,
                    epoch_id: None,
                    target_id: None,
                    request: Some(request::Request::GetStatus(GetStatusRequest {})),
                })),
            };

            sleep(Duration::from_secs(1)).await;
        }
    };

    let request = tonic::Request::new(request_stream);

    let mut response_stream = client.stream(request).await?.into_inner();

    while let Some(message) = response_stream.message().await? {
        dbg!(message);
    }

    Ok(())
}
cargo run --example streaming

Development

Protobuf codegen is handled by the codegen crate in the workspace. Generated Protobuf files are checked in. To run the code generation, do:

cargo run --package starlink-codegen

License

starlink-rs is licensed under either of

at your option.