├── protos └── spacex │ └── api │ ├── common │ └── status │ │ └── status.proto │ └── device │ ├── service.proto │ ├── command.proto │ ├── wifi_config.proto │ ├── common.proto │ ├── transceiver.proto │ ├── dish.proto │ ├── wifi.proto │ └── device.proto ├── package.json ├── README.md └── index.js /protos/spacex/api/common/status/status.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Status; 4 | 5 | option go_package = "spacex.com/api/status"; 6 | 7 | message Status { 8 | optional int32 code = 1 [json_name="code"]; 9 | optional string message = 2 [json_name="message"]; 10 | } 11 | -------------------------------------------------------------------------------- /protos/spacex/api/device/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Device; 4 | 5 | option go_package = "spacex.com/api/device"; 6 | 7 | import "spacex/api/device/device.proto"; 8 | 9 | service Device { 10 | rpc Handle (.SpaceX.API.Device.Request) returns (.SpaceX.API.Device.Response) {} 11 | rpc Stream (stream .SpaceX.API.Device.ToDevice) returns (stream .SpaceX.API.Device.FromDevice) {} 12 | } 13 | -------------------------------------------------------------------------------- /protos/spacex/api/device/command.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Device; 4 | 5 | option go_package = "spacex.com/api/device"; 6 | 7 | message PublicKey { 8 | optional string key = 1 [json_name="key"]; 9 | repeated .SpaceX.API.Device.Capability capabilities = 2 [json_name="capabilities"]; 10 | } 11 | 12 | enum Capability { 13 | READ = 0; 14 | WRITE = 1; 15 | DEBUG = 2; 16 | ADMIN = 3; 17 | SETUP = 4; 18 | SET_SKU = 5; 19 | REFRESH = 6; 20 | READ_PRIVATE = 7; 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signalk-starlink", 3 | "version": "1.1.0", 4 | "description": "Starlink plugin for Signal K", 5 | "main": "index.js", 6 | "signalk-plugin-enabled-by-default": true, 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/itemir/signalk-starlink.git" 13 | }, 14 | "keywords": [ 15 | "signalk-node-server-plugin" 16 | ], 17 | "author": "Ilker Temir", 18 | "license": "Apache-2.0", 19 | "bugs": { 20 | "url": "https://github.com/itemir/signalk-starlink/issues" 21 | }, 22 | "homepage": "https://github.com/itemir/signalk-starlink#readme", 23 | "dependencies": { 24 | "@grpc/grpc-js": "^1.6.3", 25 | "@grpc/proto-loader": "^0.6.9" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Signal K Plugin for Starlink 2 | 3 | This plugin facilitates obtaining statistics from Starlink Dishy and optionally enables auto-stowing it while the vessel is in motion. If the option is enabled, it automatically unstows the Dishy when the vessel becomes stationary again, such as at an anchorage or dock. The plugin takes 10 minutes to detect if the vessel is moving or stationary. 4 | 5 | Starlink also includes a GPS. You can optionally utilize it as a backup GPS for your Signal K network or NMEA 2000. To do so, grant location access on the local network using the Starlink app. Navigate to your Starlink app, and follow `Advanced -> Debug Data -> Allow access on local network (at the very bottom)`. Additionally, toggle on the Use Starlink as a GPS source option in the plugin configuration. 6 | 7 | Currently, the plugin publishes the following information under `network.providers.starlink`: 8 | * `status`: Either "online" or "offline" 9 | * `hardware`: Starlink hardware version 10 | * `software`: Starlink software version 11 | * `uptime`: Uptime in seconds 12 | * `downlink_throughput`: Downlink throughput in bits per second 13 | * `uplink_throughput`: Uplink throughput in bits per second 14 | 15 | Additional information is provided when Starlink is offline: 16 | * `outage.cause`: Reason for the outage 17 | * `outage.start`: Outage start date 18 | * `outage.duration`: Duration of the outage in seconds 19 | 20 | It also publishes the following if local GPS access is enabled (refer to the note above): 21 | * `navigation.position` 22 | * `navigation.courseOverGroundTrue` 23 | * `navigation.speedOverGround` 24 | 25 | Starlink proto files are originally sourced from Elias Wilken's [starlink-rs](https://github.com/ewilken/starlink-rs) repository and have been modified. 26 | -------------------------------------------------------------------------------- /protos/spacex/api/device/wifi_config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Device; 4 | 5 | option go_package = "spacex.com/api/device"; 6 | 7 | message WifiConfig { 8 | optional string network_name = 1 [json_name="networkName"]; 9 | optional bool apply_network_name = 1001 [json_name="applyNetworkName"]; 10 | optional string network_password = 2 [json_name="networkPassword"]; 11 | optional bool apply_network_password = 1002 [json_name="applyNetworkPassword"]; 12 | optional string country_code = 3 [json_name="countryCode"]; 13 | optional bool wifi_disabled = 4 [json_name="wifiDisabled"]; 14 | optional bool apply_wifi_disabled = 1003 [json_name="applyWifiDisabled"]; 15 | optional string lan_ipv4 = 5 [json_name="lanIpv4"]; 16 | optional string lan_ipv4_subnet_mask = 6 [json_name="lanIpv4SubnetMask"]; 17 | optional bool setup_complete = 7 [json_name="setupComplete"]; 18 | optional bool apply_setup_complete = 1010 [json_name="applySetupComplete"]; 19 | optional uint32 factory_reset_ticker = 8 [json_name="factoryResetTicker"]; 20 | optional uint32 version = 9 [json_name="version"]; 21 | optional .SpaceX.API.Device.WifiConfig.Security wifi_security = 10 [json_name="wifiSecurity"]; 22 | optional bool apply_wifi_security = 1004 [json_name="applyWifiSecurity"]; 23 | optional string network_name_5ghz = 11 [json_name="networkName5ghz"]; 24 | optional bool apply_network_name_5ghz = 1005 [json_name="applyNetworkName5ghz"]; 25 | optional string mac_wan = 12 [json_name="macWan"]; 26 | optional bool apply_mac_wan = 1006 [json_name="applyMacWan"]; 27 | optional string mac_lan = 13 [json_name="macLan"]; 28 | optional bool apply_mac_lan = 1007 [json_name="applyMacLan"]; 29 | optional string mac_lan_2ghz = 14 [json_name="macLan2ghz"]; 30 | optional bool apply_mac_lan_2ghz = 1008 [json_name="applyMacLan2ghz"]; 31 | optional string mac_lan_5ghz = 15 [json_name="macLan5ghz"]; 32 | optional bool apply_mac_lan_5ghz = 1009 [json_name="applyMacLan5ghz"]; 33 | optional string device_id = 16 [json_name="deviceId"]; 34 | enum Security { 35 | UNKNOWN = 0; 36 | WPA2 = 1; 37 | WPA3 = 2; 38 | WPA2WPA3 = 3; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /protos/spacex/api/device/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Device; 4 | 5 | option go_package = "spacex.com/api/device"; 6 | 7 | message DeviceInfo { 8 | optional string id = 1 [json_name="id"]; 9 | optional string hardware_version = 2 [json_name="hardwareVersion"]; 10 | optional string software_version = 3 [json_name="softwareVersion"]; 11 | optional string country_code = 4 [json_name="countryCode"]; 12 | optional int32 utc_offset_s = 5 [json_name="utcOffsetS"]; 13 | } 14 | 15 | message DeviceState { 16 | optional uint64 uptime_s = 1 [json_name="uptimeS"]; 17 | } 18 | 19 | message SignedData { 20 | optional bytes data = 1 [json_name="data"]; 21 | optional bytes signature = 2 [json_name="signature"]; 22 | } 23 | 24 | message GetNextIdRequest { 25 | } 26 | 27 | message GetNextIdResponse { 28 | optional uint64 id = 1 [json_name="id"]; 29 | optional uint64 epoch_id = 2 [json_name="epochId"]; 30 | } 31 | 32 | message PingTarget { 33 | optional string service = 1 [json_name="service"]; 34 | optional string location = 2 [json_name="location"]; 35 | optional string address = 3 [json_name="address"]; 36 | } 37 | 38 | message PingResult { 39 | optional .SpaceX.API.Device.PingTarget target = 3 [json_name="target"]; 40 | optional float dropRate = 1 [json_name="dropRate"]; 41 | optional float latencyMs = 2 [json_name="latencyMs"]; 42 | } 43 | 44 | message BondingChallenge { 45 | optional string dish_id = 1 [json_name="dishId"]; 46 | optional string wifi_id = 2 [json_name="wifiId"]; 47 | optional bytes nonce = 3 [json_name="nonce"]; 48 | } 49 | 50 | message AuthenticateRequest { 51 | optional .SpaceX.API.Device.SignedData challenge = 1 [json_name="challenge"]; 52 | } 53 | 54 | message ChallengeResponse { 55 | optional bytes signature = 1 [json_name="signature"]; 56 | optional bytes certificate_chain = 2 [json_name="certificateChain"]; 57 | } 58 | 59 | message NetworkInterface { 60 | optional string name = 1 [json_name="name"]; 61 | optional .SpaceX.API.Device.NetworkInterface.RxStats rx_stats = 2 [json_name="rxStats"]; 62 | optional .SpaceX.API.Device.NetworkInterface.TxStats tx_stats = 3 [json_name="txStats"]; 63 | oneof interface { 64 | .SpaceX.API.Device.EthernetNetworkInterface ethernet = 1000 [json_name="ethernet"]; 65 | .SpaceX.API.Device.WifiNetworkInterface wifi = 1001 [json_name="wifi"]; 66 | } 67 | message RxStats { 68 | optional uint64 bytes = 1 [json_name="bytes"]; 69 | optional uint64 packets = 2 [json_name="packets"]; 70 | optional uint64 frame_errors = 3 [json_name="frameErrors"]; 71 | } 72 | message TxStats { 73 | optional uint64 bytes = 1 [json_name="bytes"]; 74 | optional uint64 packets = 2 [json_name="packets"]; 75 | } 76 | } 77 | 78 | message EthernetNetworkInterface { 79 | optional bool link_detected = 1 [json_name="linkDetected"]; 80 | optional uint32 speed_mbps = 2 [json_name="speedMbps"]; 81 | optional bool autonegotiation_on = 3 [json_name="autonegotiationOn"]; 82 | optional .SpaceX.API.Device.EthernetNetworkInterface.Duplex duplex = 4 [json_name="duplex"]; 83 | enum Duplex { 84 | UNKNOWN = 0; 85 | HALF = 1; 86 | FULL = 2; 87 | } 88 | } 89 | 90 | message WifiNetworkInterface { 91 | optional .SpaceX.API.Device.WifiNetworkInterface.ThermalStatus thermal_status = 1 [json_name="thermalStatus"]; 92 | message ThermalStatus { 93 | optional uint32 level = 1 [json_name="level"]; 94 | optional uint32 temp = 2 [json_name="temp"]; 95 | } 96 | } 97 | 98 | message LLAPosition { 99 | optional double lat = 1 [json_name="lat"]; 100 | optional double lon = 2 [json_name="lon"]; 101 | optional double alt = 3 [json_name="alt"]; 102 | } 103 | 104 | message ECEFPosition { 105 | optional double x = 1 [json_name="x"]; 106 | optional double y = 2 [json_name="y"]; 107 | optional double z = 3 [json_name="z"]; 108 | } 109 | -------------------------------------------------------------------------------- /protos/spacex/api/device/transceiver.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Device; 4 | 5 | option go_package = "spacex.com/api/device"; 6 | 7 | import "spacex/api/device/common.proto"; 8 | import "spacex/api/device/dish.proto"; 9 | 10 | message TransceiverIFLoopbackTestRequest { 11 | optional bool enable_if_loopback = 1 [json_name="enableIfLoopback"]; 12 | } 13 | 14 | message TransceiverIFLoopbackTestResponse { 15 | optional float ber_loopback_test = 1 [json_name="berLoopbackTest"]; 16 | optional float snr_loopback_test = 2 [json_name="snrLoopbackTest"]; 17 | optional float rssi_loopback_test = 3 [json_name="rssiLoopbackTest"]; 18 | optional bool pll_lock = 4 [json_name="pllLock"]; 19 | } 20 | 21 | message TransceiverGetStatusRequest { 22 | } 23 | 24 | message TransceiverGetStatusResponse { 25 | optional .SpaceX.API.Device.TransceiverModulatorState mod_state = 1 [json_name="modState"]; 26 | optional .SpaceX.API.Device.TransceiverModulatorState demod_state = 2 [json_name="demodState"]; 27 | optional .SpaceX.API.Device.TransceiverTxRxState tx_state = 3 [json_name="txState"]; 28 | optional .SpaceX.API.Device.TransceiverTxRxState rx_state = 4 [json_name="rxState"]; 29 | optional .SpaceX.API.Device.DishState state = 1006 [json_name="state"]; 30 | optional .SpaceX.API.Device.TransceiverFaults faults = 1007 [json_name="faults"]; 31 | optional .SpaceX.API.Device.TransceiverTransmitBlankingState transmit_blanking_state = 1008 [json_name="transmitBlankingState"]; 32 | optional float modem_asic_temp = 1009 [json_name="modemAsicTemp"]; 33 | optional float tx_if_temp = 1010 [json_name="txIfTemp"]; 34 | } 35 | 36 | message TransceiverFaults { 37 | optional bool over_temp_modem_asic_fault = 1 [json_name="overTempModemAsicFault"]; 38 | optional bool over_temp_pcba_fault = 2 [json_name="overTempPcbaFault"]; 39 | optional bool dc_voltage_fault = 3 [json_name="dcVoltageFault"]; 40 | } 41 | 42 | message TransceiverGetTelemetryRequest { 43 | } 44 | 45 | message TransceiverGetTelemetryResponse { 46 | optional uint32 antenna_pointing_mode = 1001 [json_name="antennaPointingMode"]; 47 | optional float antenna_pitch = 1002 [json_name="antennaPitch"]; 48 | optional float antenna_roll = 1003 [json_name="antennaRoll"]; 49 | optional float antenna_rx_theta = 1004 [json_name="antennaRxTheta"]; 50 | optional float antenna_true_heading = 1005 [json_name="antennaTrueHeading"]; 51 | optional uint32 rx_channel = 1006 [json_name="rxChannel"]; 52 | optional uint32 current_cell_id = 1007 [json_name="currentCellId"]; 53 | optional float seconds_until_slot_end = 1008 [json_name="secondsUntilSlotEnd"]; 54 | optional float wb_rssi_peak_mag_db = 1009 [json_name="wbRssiPeakMagDb"]; 55 | optional float pop_ping_drop_rate = 1010 [json_name="popPingDropRate"]; 56 | optional float snr_db = 1011 [json_name="snrDb"]; 57 | optional float l1_snr_avg_db = 1012 [json_name="l1SnrAvgDb"]; 58 | optional float l1_snr_min_db = 1013 [json_name="l1SnrMinDb"]; 59 | optional float l1_snr_max_db = 1014 [json_name="l1SnrMaxDb"]; 60 | optional uint32 lmac_satellite_id = 1015 [json_name="lmacSatelliteId"]; 61 | optional uint32 target_satellite_id = 1016 [json_name="targetSatelliteId"]; 62 | optional uint32 grant_mcs = 1017 [json_name="grantMcs"]; 63 | optional float grant_symbols_avg = 1018 [json_name="grantSymbolsAvg"]; 64 | optional uint32 ded_grant = 1019 [json_name="dedGrant"]; 65 | optional uint32 mobility_proactive_slot_change = 1020 [json_name="mobilityProactiveSlotChange"]; 66 | optional uint32 mobility_reactive_slot_change = 1021 [json_name="mobilityReactiveSlotChange"]; 67 | optional uint32 rfp_total_syn_failed = 1022 [json_name="rfpTotalSynFailed"]; 68 | optional uint32 num_out_of_seq = 1023 [json_name="numOutOfSeq"]; 69 | optional uint32 num_ulmap_drop = 1024 [json_name="numUlmapDrop"]; 70 | optional float current_seconds_of_schedule = 1025 [json_name="currentSecondsOfSchedule"]; 71 | optional uint32 send_label_switch_to_ground_failed_calls = 1026 [json_name="sendLabelSwitchToGroundFailedCalls"]; 72 | optional double ema_velocity_x = 1027 [json_name="emaVelocityX"]; 73 | optional double ema_velocity_y = 1028 [json_name="emaVelocityY"]; 74 | optional double ema_velocity_z = 1029 [json_name="emaVelocityZ"]; 75 | optional float ce_rssi_db = 1030 [json_name="ceRssiDb"]; 76 | } 77 | 78 | enum TransceiverModulatorState { 79 | MODSTATE_UNKNOWN = 0; 80 | MODSTATE_ENABLED = 1; 81 | MODSTATE_DISABLED = 2; 82 | } 83 | 84 | enum TransceiverTxRxState { 85 | TXRX_UNKNOWN = 0; 86 | TXRX_ENABLED = 1; 87 | TXRX_DISABLED = 2; 88 | } 89 | 90 | enum TransceiverTransmitBlankingState { 91 | TB_UNKNOWN = 0; 92 | TB_ENABLED = 1; 93 | TB_DISABLED = 2; 94 | } 95 | -------------------------------------------------------------------------------- /protos/spacex/api/device/dish.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Device; 4 | 5 | option go_package = "spacex.com/api/device"; 6 | 7 | import "spacex/api/device/common.proto"; 8 | 9 | message DishStowRequest { 10 | optional bool unstow = 1 [json_name="unstow"]; 11 | } 12 | 13 | message DishStowResponse { 14 | } 15 | 16 | message DishGetContextRequest { 17 | } 18 | 19 | message DishGetContextResponse { 20 | optional .SpaceX.API.Device.DeviceInfo device_info = 1 [json_name="deviceInfo"]; 21 | optional .SpaceX.API.Device.DeviceState device_state = 7 [json_name="deviceState"]; 22 | optional float obstruction_fraction = 2 [json_name="obstructionFraction"]; 23 | optional float obstruction_valid_s = 3 [json_name="obstructionValidS"]; 24 | optional uint32 cell_id = 4 [json_name="cellId"]; 25 | optional uint32 pop_rack_id = 5 [json_name="popRackId"]; 26 | optional uint32 initial_satellite_id = 8 [json_name="initialSatelliteId"]; 27 | optional uint32 initial_gateway_id = 9 [json_name="initialGatewayId"]; 28 | optional bool on_backup_beam = 10 [json_name="onBackupBeam"]; 29 | optional float seconds_to_slot_end = 6 [json_name="secondsToSlotEnd"]; 30 | } 31 | 32 | message DishGetHistoryResponse { 33 | optional uint64 current = 1 [json_name="current"]; 34 | repeated float pop_ping_drop_rate = 1001 [json_name="popPingDropRate"]; 35 | repeated float pop_ping_latency_ms = 1002 [json_name="popPingLatencyMs"]; 36 | repeated float downlink_throughput_bps = 1003 [json_name="downlinkThroughputBps"]; 37 | repeated float uplink_throughput_bps = 1004 [json_name="uplinkThroughputBps"]; 38 | repeated float snr = 1005 [json_name="snr"]; 39 | repeated bool scheduled = 1006 [json_name="scheduled"]; 40 | repeated bool obstructed = 1007 [json_name="obstructed"]; 41 | } 42 | 43 | message DishGetStatusResponse { 44 | optional .SpaceX.API.Device.DeviceInfo device_info = 1 [json_name="deviceInfo"]; 45 | optional .SpaceX.API.Device.DeviceState device_state = 2 [json_name="deviceState"]; 46 | optional .SpaceX.API.Device.DishState state = 1006 [json_name="state"]; 47 | optional .SpaceX.API.Device.DishAlerts alerts = 1005 [json_name="alerts"]; 48 | optional float snr = 1001 [json_name="snr"]; 49 | optional float seconds_to_first_nonempty_slot = 1002 [json_name="secondsToFirstNonemptySlot"]; 50 | optional float pop_ping_drop_rate = 1003 [json_name="popPingDropRate"]; 51 | optional float downlink_throughput_bps = 1007 [json_name="downlinkThroughputBps"]; 52 | optional float uplink_throughput_bps = 1008 [json_name="uplinkThroughputBps"]; 53 | optional float pop_ping_latency_ms = 1009 [json_name="popPingLatencyMs"]; 54 | optional .SpaceX.API.Device.DishObstructionStats obstruction_stats = 1004 [json_name="obstructionStats"]; 55 | optional bool stow_requested = 1010 [json_name="stowRequested"]; 56 | optional .SpaceX.API.Device.DishOutage outage = 1014; 57 | } 58 | 59 | message DishOutage { 60 | .SpaceX.API.Device.DishOutage.Cause cause = 1; 61 | int64 start_timestamp_ns = 2; 62 | uint64 duration_ns = 3; 63 | bool did_switch = 4; 64 | enum Cause { 65 | UNKNOWN = 0; 66 | BOOTING = 1; 67 | STOWED = 2; 68 | THERMAL_SHUTDOWN = 3; 69 | NO_SCHEDULE = 4; 70 | NO_SATS = 5; 71 | OBSTRUCTED = 6; 72 | NO_DOWNLINK = 7; 73 | NO_PINGS = 8; 74 | } 75 | } 76 | 77 | message DishGetObstructionMapRequest { 78 | } 79 | 80 | message DishGetObstructionMapResponse { 81 | optional uint32 num_rows = 1 [json_name="numRows"]; 82 | optional uint32 num_cols = 2 [json_name="numCols"]; 83 | repeated float snr = 3 [json_name="snr"]; 84 | } 85 | 86 | message DishAlerts { 87 | optional bool motors_stuck = 1 [json_name="motorsStuck"]; 88 | optional bool thermal_throttle = 3 [json_name="thermalThrottle"]; 89 | optional bool thermal_shutdown = 2 [json_name="thermalShutdown"]; 90 | optional bool mast_not_near_vertical = 5 [json_name="mastNotNearVertical"]; 91 | optional bool unexpected_location = 4 [json_name="unexpectedLocation"]; 92 | optional bool slow_ethernet_speeds = 6 [json_name="slowEthernetSpeeds"]; 93 | } 94 | 95 | message DishObstructionStats { 96 | optional bool currently_obstructed = 5 [json_name="currentlyObstructed"]; 97 | optional float fraction_obstructed = 1 [json_name="fractionObstructed"]; 98 | optional float last_24h_obstructed_s = 1006 [json_name="last24hObstructedS"]; 99 | optional float valid_s = 4 [json_name="validS"]; 100 | repeated float wedge_fraction_obstructed = 2 [json_name="wedgeFractionObstructed"]; 101 | repeated float wedge_abs_fraction_obstructed = 3 [json_name="wedgeAbsFractionObstructed"]; 102 | } 103 | 104 | message DishAuthenticateResponse { 105 | optional .SpaceX.API.Device.ChallengeResponse dish = 2 [json_name="dish"]; 106 | } 107 | 108 | enum DishState { 109 | UNKNOWN = 0; 110 | CONNECTED = 1; 111 | SEARCHING = 2; 112 | BOOTING = 3; 113 | } 114 | -------------------------------------------------------------------------------- /protos/spacex/api/device/wifi.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Device; 4 | 5 | option go_package = "spacex.com/api/device"; 6 | 7 | import "spacex/api/device/common.proto"; 8 | import "spacex/api/device/wifi_config.proto"; 9 | 10 | message WifiSetConfigRequest { 11 | optional .SpaceX.API.Device.WifiConfig wifi_config = 1 [json_name="wifiConfig"]; 12 | } 13 | 14 | message WifiSetConfigResponse { 15 | optional .SpaceX.API.Device.WifiConfig updated_wifi_config = 1 [json_name="updatedWifiConfig"]; 16 | } 17 | 18 | message WifiGetConfigRequest { 19 | } 20 | 21 | message WifiGetConfigResponse { 22 | optional .SpaceX.API.Device.WifiConfig wifi_config = 1 [json_name="wifiConfig"]; 23 | } 24 | 25 | message WifiGetClientsRequest { 26 | } 27 | 28 | message WifiGetClientsResponse { 29 | repeated .SpaceX.API.Device.WifiClient clients = 1 [json_name="clients"]; 30 | } 31 | 32 | message WifiGetHistoryResponse { 33 | optional uint64 current = 1 [json_name="current"]; 34 | repeated float ping_drop_rate = 1001 [json_name="pingDropRate"]; 35 | repeated float ping_latency_ms = 1002 [json_name="pingLatencyMs"]; 36 | } 37 | 38 | message WifiNewClientConnectedEvent { 39 | optional .SpaceX.API.Device.WifiClient client = 1 [json_name="client"]; 40 | } 41 | 42 | message WifiClient { 43 | optional string name = 1 [json_name="name"]; 44 | optional string mac_address = 2 [json_name="macAddress"]; 45 | optional string ip_address = 3 [json_name="ipAddress"]; 46 | optional float signal_strength = 4 [json_name="signalStrength"]; 47 | optional .SpaceX.API.Device.WifiClient.RxStats rx_stats = 5 [json_name="rxStats"]; 48 | optional .SpaceX.API.Device.WifiClient.TxStats tx_stats = 6 [json_name="txStats"]; 49 | optional uint32 associated_time_s = 7 [json_name="associatedTimeS"]; 50 | optional string mode_str = 8 [json_name="modeStr"]; 51 | optional .SpaceX.API.Device.WifiClient.Interface iface = 9 [json_name="iface"]; 52 | optional float snr = 10 [json_name="snr"]; 53 | optional int32 psmode = 11 [json_name="psmode"]; 54 | message RxStats { 55 | optional uint64 bytes = 1 [json_name="bytes"]; 56 | optional uint64 count_errors = 2 [json_name="countErrors"]; 57 | optional int32 nss = 3 [json_name="nss"]; 58 | } 59 | message TxStats { 60 | optional uint64 bytes = 1 [json_name="bytes"]; 61 | optional uint64 success_bytes = 2 [json_name="successBytes"]; 62 | optional int32 nss = 3 [json_name="nss"]; 63 | } 64 | enum Interface { 65 | UNKNOWN = 0; 66 | ETH = 1; 67 | RF_2GHZ = 2; 68 | RF_5GHZ = 3; 69 | } 70 | } 71 | 72 | message WifiSetupRequest { 73 | optional bool skip = 1 [json_name="skip"]; 74 | optional string network_name = 2 [json_name="networkName"]; 75 | optional string network_password = 3 [json_name="networkPassword"]; 76 | } 77 | 78 | message WifiSetupResponse { 79 | } 80 | 81 | message WifiGetStatusResponse { 82 | optional .SpaceX.API.Device.DeviceInfo device_info = 3 [json_name="deviceInfo"]; 83 | optional .SpaceX.API.Device.DeviceState device_state = 4 [json_name="deviceState"]; 84 | optional bool captive_portal_enabled = 1 [json_name="captivePortalEnabled"]; 85 | repeated .SpaceX.API.Device.WifiClient clients = 2 [json_name="clients"]; 86 | optional string serial_number = 1001 [json_name="serialNumber"]; 87 | optional string sku = 1002 [json_name="sku"]; 88 | optional string ipv4_wan_address = 1003 [json_name="ipv4WanAddress"]; 89 | optional float ping_drop_rate = 1004 [json_name="pingDropRate"]; 90 | optional float ping_latency_ms = 1005 [json_name="pingLatencyMs"]; 91 | } 92 | 93 | message WifiAuthenticateRequest { 94 | optional .SpaceX.API.Device.SignedData challenge = 1 [json_name="challenge"]; 95 | } 96 | 97 | message WifiAuthenticateResponse { 98 | optional .SpaceX.API.Device.ChallengeResponse wifi = 1 [json_name="wifi"]; 99 | optional .SpaceX.API.Device.ChallengeResponse dish = 2 [json_name="dish"]; 100 | } 101 | 102 | message WifiAccountBondingEvent { 103 | optional string dish_id = 1 [json_name="dishId"]; 104 | } 105 | 106 | message PingMetrics { 107 | optional float latency_mean_ms = 1 [json_name="latencyMeanMs"]; 108 | optional float latency_stddev_ms = 2 [json_name="latencyStddevMs"]; 109 | optional float latency_mean_ms_5m = 3 [json_name="latencyMeanMs5m"]; 110 | optional float latency_mean_ms_1h = 4 [json_name="latencyMeanMs1h"]; 111 | optional float latency_mean_ms_1d = 5 [json_name="latencyMeanMs1d"]; 112 | optional float drop_rate = 6 [json_name="dropRate"]; 113 | optional float drop_rate_5m = 7 [json_name="dropRate5m"]; 114 | optional float drop_rate_1h = 8 [json_name="dropRate1h"]; 115 | optional float drop_rate_1d = 9 [json_name="dropRate1d"]; 116 | optional float seconds_since_last_success = 10 [json_name="secondsSinceLastSuccess"]; 117 | optional float seconds_since_last_1s_outage = 11 [json_name="secondsSinceLast1sOutage"]; 118 | optional float seconds_since_last_2s_outage = 15 [json_name="secondsSinceLast2sOutage"]; 119 | optional float seconds_since_last_5s_outage = 12 [json_name="secondsSinceLast5sOutage"]; 120 | optional float seconds_since_last_15s_outage = 18 [json_name="secondsSinceLast15sOutage"]; 121 | optional float seconds_since_last_60s_outage = 19 [json_name="secondsSinceLast60sOutage"]; 122 | optional float seconds_since_last_300s_outage = 20 [json_name="secondsSinceLast300sOutage"]; 123 | optional float happy_hours_1s_1d = 13 [json_name="happyHours1s1d"]; 124 | optional float happy_hours_2s_1d = 16 [json_name="happyHours2s1d"]; 125 | optional float happy_hours_5s_1d = 14 [json_name="happyHours5s1d"]; 126 | } 127 | 128 | message WifiGetPingMetricsRequest { 129 | } 130 | 131 | message WifiGetPingMetricsResponse { 132 | optional .SpaceX.API.Device.PingMetrics internet = 1 [json_name="internet"]; 133 | } 134 | 135 | message WifiGetDiagnosticsRequest { 136 | } 137 | 138 | message WifiGetDiagnosticsResponse { 139 | optional .SpaceX.API.Device.WifiScanResults network_scan = 1 [json_name="networkScan"]; 140 | repeated .SpaceX.API.Device.WifiNetwork wifi_networks = 2 [json_name="wifiNetworks"]; 141 | } 142 | 143 | message WifiScanResults { 144 | repeated .SpaceX.API.Device.WifiScanResults.Network networks = 1 [json_name="networks"]; 145 | message Network { 146 | optional .SpaceX.API.Device.WifiScanResults.Network.Source source = 1 [json_name="source"]; 147 | optional string ssid = 2 [json_name="ssid"]; 148 | optional string bssid = 3 [json_name="bssid"]; 149 | optional string frequency_ghz = 4 [json_name="frequencyGhz"]; 150 | optional int32 channel = 5 [json_name="channel"]; 151 | optional int32 signal_level_dbm = 6 [json_name="signalLevelDbm"]; 152 | optional int32 noise_level_dbm = 7 [json_name="noiseLevelDbm"]; 153 | optional bool has_encryption_key = 8 [json_name="hasEncryptionKey"]; 154 | optional string phy_mode_str = 9 [json_name="phyModeStr"]; 155 | enum Source { 156 | UNKNOWN = 0; 157 | SCAN_2_4GHZ = 1; 158 | SCAN_5GHZ = 2; 159 | } 160 | } 161 | } 162 | 163 | message WifiNetwork { 164 | optional .SpaceX.API.Device.WifiNetwork.Band band = 1 [json_name="band"]; 165 | optional string ssid = 2 [json_name="ssid"]; 166 | optional uint32 channel = 3 [json_name="channel"]; 167 | optional string encryption_type_str = 4 [json_name="encryptionTypeStr"]; 168 | enum Band { 169 | WIFI_UNKNOWN = 0; 170 | WIFI_2_4GHZ = 1; 171 | WIFI_5GHZ = 2; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022-2024 Ilker Temir 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const POLL_STARLINK_INTERVAL = 15 // Poll every N seconds 17 | const STARLINK = 'network.providers.starlink' 18 | 19 | const path = require('path'); 20 | const protoLoader = require('@grpc/proto-loader'); 21 | const grpc = require('@grpc/grpc-js'); 22 | const gRpcOptions = { 23 | keepCase: true, 24 | longs: String, 25 | enums: String, 26 | defaults: true, 27 | oneofs: true, 28 | includeDirs: [path.join(__dirname, '/protos')] 29 | } 30 | const packageDefinition = protoLoader.loadSync('spacex/api/device/service.proto', gRpcOptions); 31 | const Device = grpc.loadPackageDefinition(packageDefinition).SpaceX.API.Device.Device; 32 | var client = new Device( 33 | "192.168.100.1:9200", 34 | grpc.credentials.createInsecure() 35 | ); 36 | 37 | var dishyStatus; 38 | var stowRequested; 39 | var positions = []; 40 | var gpsSource; 41 | var errorCount = 0; 42 | var previousLatitude; 43 | var previousLongitude; 44 | 45 | module.exports = function(app) { 46 | var plugin = {}; 47 | var unsubscribes = []; 48 | var pollProcess; 49 | 50 | plugin.id = "signalk-starlink"; 51 | plugin.name = "Starlink"; 52 | plugin.description = "Starlink plugin for Signal K"; 53 | 54 | plugin.schema = { 55 | type: 'object', 56 | required: [], 57 | properties: { 58 | retrieveGps: { 59 | type: "boolean", 60 | title: "Use Starlink as a GPS source (requires enabling access on local network)", 61 | default: true 62 | }, 63 | stowWhileMoving: { 64 | type: "boolean", 65 | title: "Stow Dishy while moving", 66 | default: false 67 | }, 68 | gpsSource: { 69 | type: "string", 70 | title: "GPS source (Optional - only if you have multiple GPS sources and you want to use an explicit source)" 71 | }, 72 | enableStatusNotification: { 73 | type: "boolean", 74 | title: "Send a notification when offline", 75 | default: true 76 | }, 77 | statusNotificationState: { 78 | title: 'State', 79 | description: 80 | 'When an offline notifcation is sent, this wil be used as the notitication state', 81 | type: 'string', 82 | default: 'warn', 83 | enum: ['alert', 'warn', 'alarm', 'emergency'] 84 | }, 85 | } 86 | } 87 | 88 | plugin.start = function(options) { 89 | gpsSource = options.gpsSource; 90 | let subscription = { 91 | context: 'vessels.self', 92 | subscribe: [{ 93 | path: 'navigation.position', 94 | period: 60 * 1000 // Every minute 95 | }] 96 | }; 97 | 98 | app.subscriptionmanager.subscribe(subscription, unsubscribes, function() { 99 | app.debug('Subscription error'); 100 | }, data => processDelta(options, data)); 101 | 102 | pollProcess = setInterval( function() { 103 | if (options.retrieveGps) { 104 | client.Handle({ 105 | 'get_location': {} 106 | }, (error, response) => { 107 | if (error) { 108 | app.debug('Cannot retrieve position from Starlink'); 109 | return; 110 | } 111 | let latitude = response.get_location.lla.lat; 112 | let longitude = response.get_location.lla.lon; 113 | let values; 114 | if ((previousLatitude) && (previousLongitude)) { 115 | courseAndSpeedOverGround = calculateCourseAndSpeed( 116 | previousLatitude, 117 | previousLongitude, 118 | latitude, 119 | longitude 120 | ); 121 | values = [ 122 | { 123 | path: 'navigation.position', 124 | value: { 125 | 'longitude': longitude, 126 | 'latitude': latitude 127 | }, 128 | }, { 129 | path: 'navigation.courseOverGroundTrue', 130 | value: courseAndSpeedOverGround.course 131 | }, { 132 | path: 'navigation.speedOverGround', 133 | value: courseAndSpeedOverGround.speed 134 | } 135 | ] 136 | } else { 137 | values = [ 138 | { 139 | path: 'navigation.position', 140 | value: { 141 | 'longitude': longitude, 142 | 'latitude': latitude 143 | } 144 | } 145 | ] 146 | } 147 | app.handleMessage('signalk-starlink', { 148 | updates: [{ 149 | values: values 150 | }] 151 | }); 152 | previousLatitude = latitude; 153 | previousLongitude = longitude; 154 | app.debug(`Position received from Starlink (${latitude}, ${longitude})`); 155 | }); 156 | } 157 | 158 | client.Handle({ 159 | 'get_status': {} 160 | }, (error, response) => { 161 | if (error) { 162 | app.debug(`Error reading from Dishy.`); 163 | if (errorCount++ >= 5) { 164 | client.close(); 165 | client = new Device( 166 | "192.168.100.1:9200", 167 | grpc.credentials.createInsecure() 168 | ); 169 | errorCount = 0; 170 | app.debug(`Retrying connection`); 171 | } 172 | return; 173 | } 174 | let values; 175 | if (response.dish_get_status.outage) { 176 | let duration = response.dish_get_status.outage.duration_ns / 1000 / 1000 /1000; 177 | duration = timeSince(duration); 178 | app.setPluginStatus(`Starlink has been offline (${response.dish_get_status.outage.cause}) for ${duration}`); 179 | dishyStatus = response.dish_get_status.outage.cause; 180 | 181 | values = [ 182 | { 183 | path: `${STARLINK}.status`, 184 | value: 'offline' 185 | }, 186 | { 187 | path: `${STARLINK}.outage.cause`, 188 | value: response.dish_get_status.outage.cause 189 | }, 190 | { 191 | path: `${STARLINK}.outage.start`, 192 | value: new Date(response.dish_get_status.outage.start_timestamp_ns/1000/1000) 193 | }, 194 | { 195 | path: `${STARLINK}.outage.duration`, 196 | value: response.dish_get_status.outage.duration_ns/1000/1000/1000 197 | }, 198 | { 199 | path: `${STARLINK}.uptime`, 200 | value: response.dish_get_status.device_state.uptime_s 201 | }, 202 | { 203 | path: `${STARLINK}.hardware`, 204 | value: response.dish_get_status.device_info.hardware_version 205 | }, 206 | { 207 | path: `${STARLINK}.software`, 208 | value: response.dish_get_status.device_info.software_version 209 | }, 210 | { 211 | path: `${STARLINK}.alerts`, 212 | value: response.dish_get_status.alerts 213 | } 214 | ]; 215 | } else { 216 | if (dishyStatus != "online") { 217 | app.setPluginStatus('Starlink is online'); 218 | dishyStatus = "online"; 219 | } 220 | values = [ 221 | { 222 | path: `${STARLINK}.status`, 223 | value: 'online' 224 | }, 225 | { 226 | path: `${STARLINK}.uptime`, 227 | value: response.dish_get_status.device_state.uptime_s 228 | }, 229 | { 230 | path: `${STARLINK}.hardware`, 231 | value: response.dish_get_status.device_info.hardware_version 232 | }, 233 | { 234 | path: `${STARLINK}.software`, 235 | value: response.dish_get_status.device_info.software_version 236 | }, 237 | { 238 | path: `${STARLINK}.downlink_throughput`, 239 | value: response.dish_get_status.downlink_throughput_bps || 0 240 | }, 241 | { 242 | path: `${STARLINK}.uplink_throughput`, 243 | value: response.dish_get_status.uplink_throughput_bps || 0 244 | }, 245 | { 246 | path: `${STARLINK}.latency`, 247 | value: response.dish_get_status.pop_ping_latency_ms 248 | }, 249 | { 250 | path: `${STARLINK}.alerts`, 251 | value: response.dish_get_status.alerts 252 | } 253 | ]; 254 | } 255 | app.handleMessage('signalk-starlink', { 256 | updates: [{ 257 | values: values 258 | }] 259 | }); 260 | 261 | if ( options.enableStatusNotification === undefined || options.enableStatusNotification === true ) { 262 | const path = `notifications.${STARLINK}.state` 263 | let state = dishyStatus !== 'online' 264 | ? options.statusNotificationState || 'warn' 265 | : 'normal' 266 | 267 | let method = ['visual', 'sound'] 268 | const existing = app.getSelfPath(path) 269 | if (existing && existing.state !== 'normal' && existing.method ) { 270 | method = existing.method 271 | } 272 | const status = dishyStatus === 'online' ? 'online' : 'offline' 273 | app.handleMessage('signalk-starlink', { 274 | updates: [{ 275 | values: [{ 276 | path, 277 | value: { 278 | state, 279 | method, 280 | message: `starlink is ${status}` 281 | } 282 | }] 283 | }] 284 | }) 285 | } 286 | }); 287 | }, POLL_STARLINK_INTERVAL * 1000); 288 | } 289 | 290 | plugin.stop = function() { 291 | clearInterval(pollProcess); 292 | app.setPluginStatus('Pluggin stopped'); 293 | }; 294 | 295 | function stowDishy() { 296 | client.Handle({ 297 | 'dish_stow': {} 298 | }, (error, response) => { 299 | if (!error) { 300 | stowRequested = true; 301 | } 302 | }); 303 | } 304 | 305 | function unstowDishy() { 306 | client.Handle({ 307 | 'dish_stow': { 308 | unstow: true 309 | } 310 | }, (error, response) => { 311 | if (!error) { 312 | stowRequested = false; 313 | } 314 | }); 315 | } 316 | 317 | function calculateDistance(lat1, lon1, lat2, lon2) { 318 | if ((lat1 == lat2) && (lon1 == lon2)) { 319 | return 0; 320 | } 321 | else { 322 | var radlat1 = Math.PI * lat1/180; 323 | var radlat2 = Math.PI * lat2/180; 324 | var theta = lon1-lon2; 325 | var radtheta = Math.PI * theta/180; 326 | var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta); 327 | if (dist > 1) { 328 | dist = 1; 329 | } 330 | dist = Math.acos(dist); 331 | dist = dist * 180/Math.PI; 332 | dist = dist * 60 * 1.1515; 333 | dist = dist * 0.8684; // Convert to Nautical miles 334 | return dist; 335 | } 336 | } 337 | 338 | function processDelta(options, data) { 339 | let source = data.updates[0]['$source']; 340 | let dict = data.updates[0].values[0]; 341 | let path = dict.path; 342 | let value = dict.value; 343 | 344 | switch (path) { 345 | case 'navigation.position': 346 | if (!gpsSource) { 347 | gpsSource = source; 348 | app.debug(`Setting GPS source to ${source}.`); 349 | } else if (gpsSource != source) { 350 | app.debug(`Ignoring position from ${source}.`); 351 | break; 352 | } 353 | positions.unshift({ 354 | latitude: value.latitude, 355 | longitude: value.longitude 356 | }); 357 | positions = positions.slice(0, 10); // Keep 10 minutes of positions 358 | if (positions.length < 10) { 359 | app.debug(`Not enough position reports yet (${positions.length}) to calculate distance.`); 360 | break; 361 | } 362 | let distance = 0; 363 | for (let i=1;i < positions.length;i++) { 364 | let previousPosition = positions[i-1]; 365 | let position = positions[i]; 366 | distance = distance + calculateDistance(position.latitude, 367 | position.longitude, 368 | previousPosition.latitude, 369 | previousPosition.longitude); 370 | } 371 | app.debug (`Distance covered in the last 10 minutes is ${distance} miles.`); 372 | if (options.stowWhileMoving && (distance >= 0.15)) { 373 | if (dishyStatus == "online") { 374 | app.debug (`Vessel is moving, stowing Dishy.`); 375 | stowDishy(); 376 | } else { 377 | app.debug(`Vessel is moving but Dishy is not online.`); 378 | } 379 | } else { 380 | if (dishyStatus == "online") { 381 | app.debug (`Vessel is stationary, and dishy is not stowed.`); 382 | } else if (dishyStatus == "STOWED") { 383 | if (stowRequested) { 384 | app.debug (`Vessel is stationary, and we previously stowed Dishy. Unstowing.`); 385 | unstowDishy(); 386 | } else { 387 | app.debug (`Vessel is stationary, and Dishy is stowed, but not by us. Ignoring.`); 388 | } 389 | } 390 | } 391 | break; 392 | case 'environment.wind.speedApparent': 393 | break; 394 | default: 395 | app.error('Unknown path: ' + path); 396 | } 397 | } 398 | 399 | function timeSince(seconds) { 400 | var interval = seconds / 31536000; 401 | if (interval > 1) { 402 | return Math.floor(interval) + " years"; 403 | } 404 | interval = seconds / 2592000; 405 | if (interval > 1) { 406 | return Math.floor(interval) + " months"; 407 | } 408 | interval = seconds / 86400; 409 | if (interval > 1) { 410 | return Math.floor(interval) + " days"; 411 | } 412 | interval = seconds / 3600; 413 | if (interval > 1) { 414 | return Math.floor(interval) + " hours"; 415 | } 416 | interval = seconds / 60; 417 | if (interval > 1) { 418 | return Math.floor(interval) + " minutes"; 419 | } 420 | return Math.floor(seconds) + " seconds"; 421 | } 422 | 423 | 424 | function haversine(lat1, lon1, lat2, lon2) { 425 | const earthRadius = 6371e3; // Radius of the Earth in meters 426 | // Convert latitude and longitude from degrees to radians 427 | const lat1Rad = lat1 * Math.PI / 180; 428 | const lon1Rad = lon1 * Math.PI / 180; 429 | const lat2Rad = lat2 * Math.PI / 180; 430 | const lon2Rad = lon2 * Math.PI / 180; 431 | // Haversine formula 432 | const dLat = lat2Rad - lat1Rad; 433 | const dLon = lon2Rad - lon1Rad; 434 | const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(dLon/2) * Math.sin(dLon/2); 435 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 436 | const distance = earthRadius * c; // Distance in meters 437 | return distance; 438 | } 439 | 440 | function calculateCourseAndSpeed(lat1, lon1, lat2, lon2) { 441 | // Calculate distance between the two points 442 | const distance = haversine(lat1, lon1, lat2, lon2); 443 | // Calculate speed over ground (SOG) in meters per second 444 | const speed = distance / POLL_STARLINK_INTERVAL; 445 | // Calculate course over ground (COG) in radians 446 | const angle = Math.atan2(lon2 - lon1, lat2 - lat1); 447 | const course = angle < 0 ? angle + 2 * Math.PI : angle; 448 | return { course, speed }; 449 | } 450 | 451 | return plugin; 452 | } 453 | -------------------------------------------------------------------------------- /protos/spacex/api/device/device.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package SpaceX.API.Device; 4 | 5 | option go_package = "spacex.com/api/device"; 6 | 7 | import "spacex/api/common/status/status.proto"; 8 | import "spacex/api/device/command.proto"; 9 | import "spacex/api/device/common.proto"; 10 | import "spacex/api/device/dish.proto"; 11 | import "spacex/api/device/transceiver.proto"; 12 | import "spacex/api/device/wifi.proto"; 13 | 14 | message ToDevice { 15 | oneof message { 16 | .SpaceX.API.Device.Request request = 1 [json_name="request"]; 17 | } 18 | } 19 | 20 | message FromDevice { 21 | oneof message { 22 | .SpaceX.API.Device.Response response = 1 [json_name="response"]; 23 | .SpaceX.API.Device.Event event = 2 [json_name="event"]; 24 | } 25 | } 26 | 27 | message Request { 28 | optional uint64 id = 1 [json_name="id"]; 29 | optional uint64 epoch_id = 14 [json_name="epochId"]; 30 | optional string target_id = 13 [json_name="targetId"]; 31 | oneof request { 32 | .SpaceX.API.Device.SignedData signed_request = 15 [json_name="signedRequest"]; 33 | .SpaceX.API.Device.GetNextIdRequest get_next_id = 1006 [json_name="getNextId"]; 34 | .SpaceX.API.Device.AuthenticateRequest authenticate = 1005 [json_name="authenticate"]; 35 | .SpaceX.API.Device.EnableFlowRequest enable_flow = 1018 [json_name="enableFlow"]; 36 | .SpaceX.API.Device.FactoryResetRequest factory_reset = 1011 [json_name="factoryReset"]; 37 | .SpaceX.API.Device.GetDeviceInfoRequest get_device_info = 1008 [json_name="getDeviceInfo"]; 38 | .SpaceX.API.Device.GetHistoryRequest get_history = 1007 [json_name="getHistory"]; 39 | .SpaceX.API.Device.GetLogRequest get_log = 1012 [json_name="getLog"]; 40 | .SpaceX.API.Device.GetNetworkInterfacesRequest get_network_interfaces = 1015 [json_name="getNetworkInterfaces"]; 41 | .SpaceX.API.Device.GetPingRequest get_ping = 1009 [json_name="getPing"]; 42 | .SpaceX.API.Device.PingHostRequest ping_host = 1016 [json_name="pingHost"]; 43 | .SpaceX.API.Device.GetStatusRequest get_status = 1004 [json_name="getStatus"]; 44 | .SpaceX.API.Device.RebootRequest reboot = 1001 [json_name="reboot"]; 45 | .SpaceX.API.Device.SetSkuRequest set_sku = 1013 [json_name="setSku"]; 46 | .SpaceX.API.Device.SetTrustedKeysRequest set_trusted_keys = 1010 [json_name="setTrustedKeys"]; 47 | .SpaceX.API.Device.SpeedTestRequest speed_test = 1003 [json_name="speedTest"]; 48 | .SpaceX.API.Device.UpdateRequest update = 1014 [json_name="update"]; 49 | .SpaceX.API.Device.GetLocationRequest get_location = 1017 [json_name="getLocation"]; 50 | .SpaceX.API.Device.GetHeapDumpRequest get_heap_dump = 1019 [json_name="getHeapDump"]; 51 | .SpaceX.API.Device.RestartControlRequest restart_control = 1020 [json_name="restartControl"]; 52 | .SpaceX.API.Device.FuseRequest fuse = 1021 [json_name="fuse"]; 53 | .SpaceX.API.Device.DishStowRequest dish_stow = 2002 [json_name="dishStow"]; 54 | .SpaceX.API.Device.DishGetContextRequest dish_get_context = 2003 [json_name="dishGetContext"]; 55 | .SpaceX.API.Device.DishGetObstructionMapRequest dish_get_obstruction_map = 2008 [json_name="dishGetObstructionMap"]; 56 | .SpaceX.API.Device.DishEmcRequest dish_emc = 2007 [json_name="dishEmc"]; 57 | .SpaceX.API.Device.TransceiverIFLoopbackTestRequest transceiver_if_loopback_test = 4001 [json_name="transceiverIfLoopbackTest"]; 58 | .SpaceX.API.Device.TransceiverGetStatusRequest transceiver_get_status = 4003 [json_name="transceiverGetStatus"]; 59 | .SpaceX.API.Device.TransceiverGetTelemetryRequest transceiver_get_telemetry = 4004 [json_name="transceiverGetTelemetry"]; 60 | .SpaceX.API.Device.WifiGetClientsRequest wifi_get_clients = 3002 [json_name="wifiGetClients"]; 61 | .SpaceX.API.Device.WifiGetDiagnosticsRequest wifi_get_diagnostics = 3008 [json_name="wifiGetDiagnostics"]; 62 | .SpaceX.API.Device.WifiGetPingMetricsRequest wifi_get_ping_metrics = 3007 [json_name="wifiGetPingMetrics"]; 63 | .SpaceX.API.Device.WifiSetConfigRequest wifi_set_config = 3001 [json_name="wifiSetConfig"]; 64 | .SpaceX.API.Device.WifiGetConfigRequest wifi_get_config = 3009 [json_name="wifiGetConfig"]; 65 | .SpaceX.API.Device.WifiSetupRequest wifi_setup = 3003 [json_name="wifiSetup"]; 66 | } 67 | } 68 | 69 | message Response { 70 | optional uint64 id = 1 [json_name="id"]; 71 | optional .SpaceX.API.Status.Status status = 2 [json_name="status"]; 72 | optional uint64 api_version = 3 [json_name="apiVersion"]; 73 | oneof response { 74 | .SpaceX.API.Device.GetNextIdResponse get_next_id = 1006 [json_name="getNextId"]; 75 | .SpaceX.API.Device.EnableFlowResponse enable_flow = 1018 [json_name="enableFlow"]; 76 | .SpaceX.API.Device.FactoryResetResponse factory_reset = 1011 [json_name="factoryReset"]; 77 | .SpaceX.API.Device.GetDeviceInfoResponse get_device_info = 1004 [json_name="getDeviceInfo"]; 78 | .SpaceX.API.Device.GetLogResponse get_log = 1012 [json_name="getLog"]; 79 | .SpaceX.API.Device.GetNetworkInterfacesResponse get_network_interfaces = 1015 [json_name="getNetworkInterfaces"]; 80 | .SpaceX.API.Device.GetPingResponse get_ping = 1009 [json_name="getPing"]; 81 | .SpaceX.API.Device.PingHostResponse ping_host = 1016 [json_name="pingHost"]; 82 | .SpaceX.API.Device.RebootResponse reboot = 1001 [json_name="reboot"]; 83 | .SpaceX.API.Device.SpeedTestResponse speed_test = 1003 [json_name="speedTest"]; 84 | .SpaceX.API.Device.SetSkuResponse set_sku = 1013 [json_name="setSku"]; 85 | .SpaceX.API.Device.SetTrustedKeysResponse set_trusted_keys = 1010 [json_name="setTrustedKeys"]; 86 | .SpaceX.API.Device.UpdateResponse update = 1014 [json_name="update"]; 87 | .SpaceX.API.Device.GetLocationResponse get_location = 1017 [json_name="getLocation"]; 88 | .SpaceX.API.Device.GetHeapDumpResponse get_heap_dump = 1019 [json_name="getHeapDump"]; 89 | .SpaceX.API.Device.RestartControlResponse restart_control = 1020 [json_name="restartControl"]; 90 | .SpaceX.API.Device.FuseResponse fuse = 1021 [json_name="fuse"]; 91 | .SpaceX.API.Device.DishAuthenticateResponse dish_authenticate = 2005 [json_name="dishAuthenticate"]; 92 | .SpaceX.API.Device.DishGetContextResponse dish_get_context = 2003 [json_name="dishGetContext"]; 93 | .SpaceX.API.Device.DishGetHistoryResponse dish_get_history = 2006 [json_name="dishGetHistory"]; 94 | .SpaceX.API.Device.DishGetStatusResponse dish_get_status = 2004 [json_name="dishGetStatus"]; 95 | .SpaceX.API.Device.DishGetObstructionMapResponse dish_get_obstruction_map = 2008 [json_name="dishGetObstructionMap"]; 96 | .SpaceX.API.Device.DishStowResponse dish_stow = 2002 [json_name="dishStow"]; 97 | .SpaceX.API.Device.DishEmcResponse dish_emc = 2007 [json_name="dishEmc"]; 98 | .SpaceX.API.Device.TransceiverIFLoopbackTestResponse transceiver_if_loopback_test = 4001 [json_name="transceiverIfLoopbackTest"]; 99 | .SpaceX.API.Device.TransceiverGetStatusResponse transceiver_get_status = 4003 [json_name="transceiverGetStatus"]; 100 | .SpaceX.API.Device.TransceiverGetTelemetryResponse transceiver_get_telemetry = 4004 [json_name="transceiverGetTelemetry"]; 101 | .SpaceX.API.Device.WifiAuthenticateResponse wifi_authenticate = 3005 [json_name="wifiAuthenticate"]; 102 | .SpaceX.API.Device.WifiGetClientsResponse wifi_get_clients = 3002 [json_name="wifiGetClients"]; 103 | .SpaceX.API.Device.WifiGetDiagnosticsResponse wifi_get_diagnostics = 3008 [json_name="wifiGetDiagnostics"]; 104 | .SpaceX.API.Device.WifiGetHistoryResponse wifi_get_history = 3006 [json_name="wifiGetHistory"]; 105 | .SpaceX.API.Device.WifiGetPingMetricsResponse wifi_get_ping_metrics = 3007 [json_name="wifiGetPingMetrics"]; 106 | .SpaceX.API.Device.WifiGetStatusResponse wifi_get_status = 3004 [json_name="wifiGetStatus"]; 107 | .SpaceX.API.Device.WifiSetConfigResponse wifi_set_config = 3001 [json_name="wifiSetConfig"]; 108 | .SpaceX.API.Device.WifiGetConfigResponse wifi_get_config = 3009 [json_name="wifiGetConfig"]; 109 | .SpaceX.API.Device.WifiSetupResponse wifi_setup = 3003 [json_name="wifiSetup"]; 110 | } 111 | } 112 | 113 | message Event { 114 | oneof event { 115 | .SpaceX.API.Device.WifiNewClientConnectedEvent wifi_new_client_connected = 3001 [json_name="wifiNewClientConnected"]; 116 | .SpaceX.API.Device.WifiAccountBondingEvent wifi_account_bonding = 3002 [json_name="wifiAccountBonding"]; 117 | } 118 | } 119 | 120 | message EnableFlowRequest { 121 | optional string name = 1 [json_name="name"]; 122 | optional uint32 duration_h = 2 [json_name="durationH"]; 123 | } 124 | 125 | message EnableFlowResponse { 126 | } 127 | 128 | message FactoryResetRequest { 129 | } 130 | 131 | message FactoryResetResponse { 132 | } 133 | 134 | message FuseRequest { 135 | } 136 | 137 | message FuseResponse { 138 | } 139 | 140 | message GetHistoryRequest { 141 | } 142 | 143 | message GetLogRequest { 144 | } 145 | 146 | message GetLogResponse { 147 | optional string syslog = 1 [json_name="syslog"]; 148 | optional string offline_log = 2 [json_name="offlineLog"]; 149 | } 150 | 151 | message GetPingRequest { 152 | } 153 | 154 | message GetPingResponse { 155 | repeated .SpaceX.API.Device.GetPingResponse.ResultsEntry results = 1 [json_name="results"]; 156 | message ResultsEntry { 157 | optional string key = 1 [json_name="key"]; 158 | optional .SpaceX.API.Device.PingResult value = 2 [json_name="value"]; 159 | } 160 | } 161 | 162 | message PingHostRequest { 163 | optional string address = 3 [json_name="address"]; 164 | } 165 | 166 | message PingHostResponse { 167 | optional .SpaceX.API.Device.PingResult result = 1 [json_name="result"]; 168 | } 169 | 170 | message GetStatusRequest { 171 | } 172 | 173 | message RebootRequest { 174 | } 175 | 176 | message RebootResponse { 177 | } 178 | 179 | message SpeedTestRequest { 180 | } 181 | 182 | message SpeedTestResponse { 183 | optional float download_bps = 1 [json_name="downloadBps"]; 184 | optional float upload_bps = 2 [json_name="uploadBps"]; 185 | optional float latency_s = 3 [json_name="latencyS"]; 186 | optional float download_mbps = 4 [json_name="downloadMbps"]; 187 | optional float upload_mbps = 5 [json_name="uploadMbps"]; 188 | optional float latency_ms = 6 [json_name="latencyMs"]; 189 | optional float download_mbps_1_tcp_conn = 7 [json_name="downloadMbps1TcpConn"]; 190 | optional float upload_mbps_1_tcp_conn = 8 [json_name="uploadMbps1TcpConn"]; 191 | optional float download_mbps_4_tcp_conn = 9 [json_name="downloadMbps4TcpConn"]; 192 | optional float upload_mbps_4_tcp_conn = 10 [json_name="uploadMbps4TcpConn"]; 193 | optional float download_mbps_16_tcp_conn = 11 [json_name="downloadMbps16TcpConn"]; 194 | optional float upload_mbps_16_tcp_conn = 12 [json_name="uploadMbps16TcpConn"]; 195 | optional float download_mbps_64_tcp_conn = 13 [json_name="downloadMbps64TcpConn"]; 196 | optional float upload_mbps_64_tcp_conn = 14 [json_name="uploadMbps64TcpConn"]; 197 | } 198 | 199 | message GetDeviceInfoRequest { 200 | } 201 | 202 | message GetDeviceInfoResponse { 203 | optional .SpaceX.API.Device.DeviceInfo device_info = 1 [json_name="deviceInfo"]; 204 | } 205 | 206 | message SetTrustedKeysRequest { 207 | repeated .SpaceX.API.Device.PublicKey keys = 1 [json_name="keys"]; 208 | } 209 | 210 | message SetTrustedKeysResponse { 211 | } 212 | 213 | message SetSkuRequest { 214 | optional string sku = 1 [json_name="sku"]; 215 | optional string country_code = 2 [json_name="countryCode"]; 216 | optional bool apply_country_code = 4 [json_name="applyCountryCode"]; 217 | } 218 | 219 | message SetSkuResponse { 220 | } 221 | 222 | message UpdateRequest { 223 | } 224 | 225 | message UpdateResponse { 226 | } 227 | 228 | message RestartControlRequest { 229 | } 230 | 231 | message RestartControlResponse { 232 | } 233 | 234 | message GetNetworkInterfacesRequest { 235 | } 236 | 237 | message GetNetworkInterfacesResponse { 238 | repeated .SpaceX.API.Device.NetworkInterface network_interfaces = 1006 [json_name="networkInterfaces"]; 239 | } 240 | 241 | message GetHeapDumpRequest { 242 | } 243 | 244 | message GetHeapDumpResponse { 245 | optional string heap_dump = 1 [json_name="heapDump"]; 246 | } 247 | 248 | message GetLocationRequest { 249 | } 250 | 251 | message GetLocationResponse { 252 | optional .SpaceX.API.Device.LLAPosition lla = 1 [json_name="lla"]; 253 | optional .SpaceX.API.Device.ECEFPosition ecef = 2 [json_name="ecef"]; 254 | } 255 | 256 | message DishEmcRequest { 257 | optional double theta = 1 [json_name="theta"]; 258 | optional double phi = 2 [json_name="phi"]; 259 | optional uint32 rx_chan = 3 [json_name="rxChan"]; 260 | optional uint32 tx_chan = 4 [json_name="txChan"]; 261 | optional uint32 modulation = 5 [json_name="modulation"]; 262 | optional double desired_tilt_angle = 7 [json_name="desiredTiltAngle"]; 263 | optional bool chan_override = 8 [json_name="chanOverride"]; 264 | optional bool theta_enabled = 9 [json_name="thetaEnabled"]; 265 | optional bool phi_enabled = 10 [json_name="phiEnabled"]; 266 | optional bool idle = 11 [json_name="idle"]; 267 | optional bool fast_switching = 12 [json_name="fastSwitching"]; 268 | optional bool sky_search = 13 [json_name="skySearch"]; 269 | optional bool force_pll_unlock = 14 [json_name="forcePllUnlock"]; 270 | optional bool force_eirp_failure = 15 [json_name="forceEirpFailure"]; 271 | optional bool snow_active_override = 16 [json_name="snowActiveOverride"]; 272 | optional bool manual_tilting = 18 [json_name="manualTilting"]; 273 | optional bool tilt_to_stowed = 19 [json_name="tiltToStowed"]; 274 | optional bool reboot = 20 [json_name="reboot"]; 275 | } 276 | 277 | message DishEmcResponse { 278 | optional bool is_test_device = 1 [json_name="isTestDevice"]; 279 | optional string uuid = 2 [json_name="uuid"]; 280 | optional uint64 timestamp = 3 [json_name="timestamp"]; 281 | optional string state = 4 [json_name="state"]; 282 | optional string sky_search_state = 5 [json_name="skySearchState"]; 283 | optional float snr = 6 [json_name="snr"]; 284 | optional float seconds_until_schedule = 7 [json_name="secondsUntilSchedule"]; 285 | optional double snr_uptime = 8 [json_name="snrUptime"]; 286 | optional double cplane_uptime = 9 [json_name="cplaneUptime"]; 287 | optional double percent_scheduled = 10 [json_name="percentScheduled"]; 288 | optional uint32 cplane_updates = 11 [json_name="cplaneUpdates"]; 289 | optional double downlink_throughput = 12 [json_name="downlinkThroughput"]; 290 | optional double uplink_throughput = 13 [json_name="uplinkThroughput"]; 291 | optional bool connected = 14 [json_name="connected"]; 292 | optional int32 sys_uptime = 15 [json_name="sysUptime"]; 293 | optional double gps_latitude = 16 [json_name="gpsLatitude"]; 294 | optional double gps_longitude = 17 [json_name="gpsLongitude"]; 295 | optional double gps_pdop = 18 [json_name="gpsPdop"]; 296 | optional uint32 rf_mode = 19 [json_name="rfMode"]; 297 | optional double phi = 20 [json_name="phi"]; 298 | optional double theta = 21 [json_name="theta"]; 299 | optional uint32 rx_channel = 22 [json_name="rxChannel"]; 300 | optional uint32 tx_channel = 23 [json_name="txChannel"]; 301 | optional float t_dbf_max = 24 [json_name="tDbfMax"]; 302 | optional double t_center = 25 [json_name="tCenter"]; 303 | optional double baseline_heating = 26 [json_name="baselineHeating"]; 304 | optional double additional_heating = 27 [json_name="additionalHeating"]; 305 | optional double total_heating = 28 [json_name="totalHeating"]; 306 | optional double target_total_heating = 29 [json_name="targetTotalHeating"]; 307 | optional bool auto_power_snow_melt_enabled = 30 [json_name="autoPowerSnowMeltEnabled"]; 308 | optional double voltage = 32 [json_name="voltage"]; 309 | optional uint32 rx_beam_state = 33 [json_name="rxBeamState"]; 310 | optional uint32 tx_beam_state = 34 [json_name="txBeamState"]; 311 | optional uint32 half_duplex_state = 35 [json_name="halfDuplexState"]; 312 | optional bool manual_tilt_enabled = 36 [json_name="manualTiltEnabled"]; 313 | optional double tilt_angle = 37 [json_name="tiltAngle"]; 314 | optional uint32 pll_tx_lock_detected = 38 [json_name="pllTxLockDetected"]; 315 | optional bool eirp_exceeded_threshold = 39 [json_name="eirpExceededThreshold"]; 316 | optional float eirp_scale_override = 40 [json_name="eirpScaleOverride"]; 317 | optional bool idle_override_enabled = 41 [json_name="idleOverrideEnabled"]; 318 | optional bool theta_override_enabled = 42 [json_name="thetaOverrideEnabled"]; 319 | optional double theta_override_value = 43 [json_name="thetaOverrideValue"]; 320 | optional bool phi_override_enabled = 44 [json_name="phiOverrideEnabled"]; 321 | optional double phi_override_value = 45 [json_name="phiOverrideValue"]; 322 | optional uint32 rx_chan_override_value = 46 [json_name="rxChanOverrideValue"]; 323 | optional uint32 tx_chan_override_value = 47 [json_name="txChanOverrideValue"]; 324 | optional bool sky_search_override_enabled = 48 [json_name="skySearchOverrideEnabled"]; 325 | optional bool fast_switching_enabled = 49 [json_name="fastSwitchingEnabled"]; 326 | optional uint32 modulation_override_value = 50 [json_name="modulationOverrideValue"]; 327 | optional bool force_eirp_failure = 51 [json_name="forceEirpFailure"]; 328 | optional bool force_pll_unlock = 52 [json_name="forcePllUnlock"]; 329 | optional uint32 ut_ine_success = 53 [json_name="utIneSuccess"]; 330 | optional bool rf_ready = 54 [json_name="rfReady"]; 331 | } 332 | --------------------------------------------------------------------------------