Initial upload of history parser and notes file

This commit is contained in:
sparky8512 2020-12-22 15:51:37 -08:00 committed by GitHub
parent 33e14d2073
commit a0f4ed2595
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 167 additions and 0 deletions

22
get_history_notes.txt Normal file
View file

@ -0,0 +1,22 @@
This file contains notes about the data returned from a "get_history" gRPC request sent to a Starlink dish, based on observing the data over time and comparing it to what shows up in the Statistics page of the Starlink Android app (which appears to use this same data via the gRPC service). The names below are from the JSON format data that grpcurl spits out, so are slightly different from what would show up in .proto files that describe the data structures.
DISCLAIMER: This is not official documentation of this data format, and some of what is written here could be a misinterpretation of what the data actually means, and thus be utterly wrong.
Sample points are one per second, the entire data set covers up to 12 hours, and it is arranged in what appears to be a ring buffer. It's not clear what exactly a "ping" means in this data, or between which points it's measuring RTT, but the latency values correspond roughly with ICMP echo ping times from a PC on my home network and IP address 8.8.8.8, so it's presumably at least all the way through a ground station.
"current" is the total number of samples that have been written to the ring buffer, irrespective of buffer wrap. Since the samples are written once per second and the buffer resets on dish reboot, this value will be roughly equal to dish uptime in seconds. This value can be used to index into the data arrays to find the location of the current data sample. The data arrays are represented as fixed size (currently 43200 element) ring buffers. In other words: If "current" is less than 43200, then the valid data samples can be found in each array at index 0 (oldest data sample) through (and including) index "current" - 1 (latest data sample). If "current" is greater or equal to 43200, then all array elements contain valid data, but it will be ordered within each array as index "current" mod 43200 (oldest data sample) through index 43199, then index 0 through index ("current" mod 43200) - 1 (latest data sample). If the data arrays are different size in the future, then use that size in the modulo arithmetic instead of 43200.
"popPingDropRate": Fraction of lost ping replies per sample. Given the small fractions reported for some, this implies the dish is collecting stats for many pings per second, and just reporting totals and/or averages per second, but that's just conjecture on my part.
"popPingLatencyMs": Round trip time, in milliseconds. NOTE: Whenever the popPingDropRate value is 1 for a sample, it appears the popPingLatencyMs value will just repeat prior value rather than leaving a hole in the data.
"downlinkThroughputBps", "uplinkThroughputBps": The app labels these "Download Usage" and "Upload Usage", so this is presumably number of bits actually used, not what is available to be used. From the max values, it appears to be in bits per second.
"snr": Signal to noise ratio, capped at 9.
"scheduled": When false, ping drop shows up as "No satellites" in app, so presumably true means at least 1 satellite is within geometric view of the dish.
"obstructed": When true, ping drop shows up as "Obstructed" in app, so presumably means dish has somehow detected the view in the direction of the satellite is obstructed.
There is no specific data field that obviously correlates with "Beta Downtime", so I'm guessing those are the points where "popPingDropRate" is 1, but the reason could not be identified because "scheduled" is true and "obstructed" is false.

145
parseJsonHistory.py Normal file
View file

@ -0,0 +1,145 @@
#!/usr/bin/python
######################################################################
#
# Example parser for the JSON format history stats output of grpcurl
# for the gRPC service provided on a Starlink user terminal.
#
# Expects input as from the following command:
# grpcurl -plaintext -d {\"get_history\":{}} 192.168.100.1:9200 SpaceX.API.Device.Device/Handle
#
# This script examines the most recent samples from the history data
# and computes several different metrics related to packet loss. By
# default, it will print the results in CSV format.
#
######################################################################
import json
import datetime
import sys
import getopt
from itertools import chain
fArgError = False
try:
opts, args = getopt.getopt(sys.argv[1:], "ahs:vH")
except getopt.GetoptError as err:
print(str(err))
fArgError = True
# Default to 1 hour worth of data samples.
parseSamples = 3600
fUsage = False
fVerbose = False
fParseAll = False
fHeader = False
if not fArgError:
if len(args) > 1:
fArgError = True
else:
for opt, arg in opts:
if opt == "-a":
fParseAll = True
elif opt == "-h":
fUsage = True
elif opt == "-s":
parseSamples = int(arg)
elif opt == "-v":
fVerbose = True
elif opt == "-H":
fHeader = True
if fUsage or fArgError:
print("Usage: "+sys.argv[0]+" [options...] [<file>]")
print(" where <file> is the file to parse, default: stdin")
print("Options:")
print(" -a: Parse all valid samples")
print(" -h: Be helpful")
print(" -s <num>: Parse <num> data samples, default: "+str(parseSamples))
print(" -v: Be verbose")
print(" -H: print CSV header instead of parsing file")
sys.exit(1 if fArgError else 0)
if fHeader:
print("datetimestamp_utc,samples,total_ping_drop,count_full_ping_drop,count_obstructed,total_obstructed_ping_drop,count_full_obstructed_ping_drop,count_unscheduled,total_unscheduled_ping_drop,count_full_unscheduled_ping_drop")
sys.exit(0)
# Allow "-" to be specified as file for stdin.
if len(args) == 0 or args[0] == "-":
jsonData = json.load(sys.stdin)
else:
jsonFile = open(args[0])
jsonData = json.load(jsonFile)
jsonFile.close()
historyData = jsonData['dishGetHistory']
# 'current' is the count of data samples written to the ring buffer,
# irrespective of buffer wrap.
current = int(historyData['current'])
nSamples = len(historyData['popPingDropRate'])
if fVerbose:
print("current: " + str(current))
print("All samples: " + str(nSamples))
nSamples = min(nSamples,current)
if fVerbose:
print("Valid samples: " + str(nSamples))
# This is ring buffer offset, so both index to oldest data sample and
# index to next data sample after the newest one.
offset = current % nSamples
tot = 0
totOne = 0
totUnsched = 0
totUnschedD = 0
totUnschedOne = 0
totObstruct = 0
totObstructD = 0
totObstructOne = 0
if fParseAll or nSamples < parseSamples:
parseSamples = nSamples
# Parse the most recent parseSamples-sized set of samples. This will
# iterate samples in order from oldest to newest, although that's not
# actually required for the current set of stats being computed below.
if parseSamples <= offset:
sampleRange = range(offset - parseSamples, offset)
else:
sampleRange = chain(range(nSamples + offset - parseSamples, nSamples), range(0, offset))
for i in sampleRange:
d = historyData["popPingDropRate"][i]
tot += d
if d >= 1:
totOne += d
if not historyData["scheduled"][i]:
totUnsched += 1
totUnschedD += d
if d >= 1:
totUnschedOne += d
if historyData["obstructed"][i]:
totObstruct += 1
totObstructD += d
if d >= 1:
totObstructOne += d
if fVerbose:
print("Parsed samples: " + str(parseSamples))
print("Total ping drop: " + str(tot))
print("Count of drop == 1: " + str(totOne))
print("Obstructed: " + str(totObstruct))
print("Obstructed ping drop: " + str(totObstructD))
print("Obstructed drop == 1: " + str(totObstructOne))
print("Unscheduled: " + str(totUnsched))
print("Unscheduled ping drop: " + str(totUnschedD))
print("Unscheduled drop == 1: " + str(totUnschedOne))
else:
# NOTE: When changing data output format, also change the -H header printing above.
print(datetime.datetime.utcnow().replace(microsecond=0).isoformat()+","+str(parseSamples)+","+str(tot)+","+str(totOne)+","+str(totObstruct)+","+str(totObstructD)+","+str(totObstructOne)+","+str(totUnsched)+","+str(totUnschedD)+","+str(totUnschedOne))