kopia lustrzana https://github.com/cyoung/stratux
QoS graphs from replay logs.
rodzic
e13ba80f17
commit
df916eb24b
|
@ -0,0 +1,198 @@
|
|||
package main
|
||||
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
// "time"
|
||||
"./uatparse"
|
||||
"os"
|
||||
"bufio"
|
||||
"strings"
|
||||
"unicode"
|
||||
"strconv"
|
||||
"github.com/gonum/plot"
|
||||
"github.com/gonum/plot/plotter"
|
||||
"github.com/gonum/plot/plotutil"
|
||||
"github.com/gonum/plot/vg"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
UPLINK_FRAME_DATA_BYTES = 432
|
||||
)
|
||||
|
||||
|
||||
/*
|
||||
|
||||
From AC 00-45G [http://www.faa.gov/documentLibrary/media/Advisory_Circular/AC_00-45G_CHG_1-2.pdf]
|
||||
|
||||
1.3.7.1 Flight Information Service – Broadcast (FIS-B)
|
||||
|
||||
Table 1-1. FIS-B Over UAT Product Update and Transmission Intervals:
|
||||
|
||||
Product FIS-B Over UAT Service Update Intervals(1) FIS-B Service Transmission Intervals(2)
|
||||
AIRMET As available 5 minutes
|
||||
Convective SIGMET As available 5 minutes
|
||||
METARs / SPECIs 1 minute / as available 5 minutes
|
||||
NEXRAD Composite Reflectivity (CONUS) 15 minutes 15 minutes
|
||||
NEXRAD Composite Reflectivity (Regional) 5 minutes 2.5 minutes
|
||||
NOTAMs-D/FDC/TFR As available 10 minutes
|
||||
PIREP As available 10 minutes
|
||||
SIGMET As available 5 minutes
|
||||
Special Use Airspace Status As available 10 minutes
|
||||
TAF/AMEND 8 hours/as available 10 minutes
|
||||
Temperature Aloft 12 hours 10 minutes
|
||||
Winds Aloft 12 hours 10 minutes
|
||||
|
||||
|
||||
(1) The Update Interval is the rate at which the product data is available from the source.
|
||||
(2) The Transmission Interval is the amount of time within which a new or updated product transmission must be
|
||||
completed and the rate or repetition interval at which the product is rebroadcast.
|
||||
|
||||
*/
|
||||
|
||||
func append_metars(rawUplinkMessage string, curMetars []string) []string {
|
||||
ret := curMetars
|
||||
|
||||
buf, err := uatparse.ParseInput(rawUplinkMessage)
|
||||
if err != nil {
|
||||
return ret
|
||||
}
|
||||
//fmt.Printf("*************************\n")
|
||||
metars := uatparse.DecodeUplink(buf)
|
||||
for _, v := range metars {
|
||||
//fmt.Printf("EE: %s\n", v)
|
||||
vSplit := strings.Split(v, " ")
|
||||
if vSplit[0] != "METAR" || len(vSplit) < 3 { // Only looking for METARs.
|
||||
continue
|
||||
}
|
||||
ret = append(ret, v)
|
||||
}
|
||||
//fmt.Printf("=========================\n")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Average number of METARs received for an airport for which you first received a METAR in the first 5 minutes, over 10 minutes. Divided by two.
|
||||
*/
|
||||
|
||||
func metar_qos_one_period(a, b []string) float64 {
|
||||
numMetarByIdent := make(map[string]uint)
|
||||
for _, v := range a {
|
||||
vSplit := strings.Split(v, " ")
|
||||
numMetarByIdent[vSplit[1]]++
|
||||
}
|
||||
// b is treated differently - new airports in b aren't counted.
|
||||
for _, v := range b {
|
||||
vSplit := strings.Split(v, " ")
|
||||
if _, ok := numMetarByIdent[vSplit[1]]; ok {
|
||||
numMetarByIdent[vSplit[1]]++
|
||||
}
|
||||
}
|
||||
// Final count.
|
||||
ret := float64(0.0)
|
||||
for _, num := range numMetarByIdent {
|
||||
ret += float64(num)
|
||||
}
|
||||
if len(numMetarByIdent) > 0 {
|
||||
ret = ret / float64(2 * len(numMetarByIdent))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Printf("%s <replay log>\n", os.Args[0])
|
||||
return
|
||||
}
|
||||
f, err := os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Printf("error opening '%s': %s\n", os.Args[1], err.Error())
|
||||
return
|
||||
}
|
||||
rdr := bufio.NewReader(f)
|
||||
|
||||
// For now, "windows" are 5 minute intervals.
|
||||
qos := make(map[int64]float64) // window number -> qos value
|
||||
curWindow := int64(0)
|
||||
windowOffset := int64(0)
|
||||
metarsByWindow := make(map[int64][]string)
|
||||
for {
|
||||
buf, err := rdr.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
buf = strings.TrimFunc(buf, func(r rune) bool {return unicode.IsControl(r)})
|
||||
linesplit := strings.Split(buf, ",")
|
||||
if len(linesplit) < 2 { // Blank line or invalid.
|
||||
continue
|
||||
}
|
||||
if linesplit[0] == "START" { // Reset ticker, new start.
|
||||
//TODO: Support multiple sessions.
|
||||
// Reset the counters, new session.
|
||||
// qos = make(map[uint]float64)
|
||||
// curWindowMetars = make([]string, 0)
|
||||
// curWindow = 0
|
||||
windowOffset = curWindow
|
||||
} else { // If it's not "START", then it's a tick count.
|
||||
i, err := strconv.ParseInt(linesplit[0], 10, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid tick: '%s'\n\n\n%s\n", linesplit[0], buf)
|
||||
continue
|
||||
}
|
||||
|
||||
// Window number in current session.
|
||||
wnum := int64(i / (5 * 60 * 1000000000))
|
||||
// fmt.Printf("%d\n", curWindow)
|
||||
if wnum + windowOffset != curWindow { // Switched over.
|
||||
curWindow = wnum + windowOffset
|
||||
beforeLastWindowMetars, ok := metarsByWindow[curWindow - 2]
|
||||
lastWindowMetars, ok2 := metarsByWindow[curWindow - 1]
|
||||
if ok && ok2 {
|
||||
// fmt.Printf("%v\n\n\nheyy\n\n%v\n", beforeLastWindowMetars, lastWindowMetars)
|
||||
qos[curWindow - 1] = metar_qos_one_period(beforeLastWindowMetars, lastWindowMetars)
|
||||
fmt.Printf("qos=%f\n", qos[curWindow - 1])
|
||||
delete(metarsByWindow, curWindow - 2)
|
||||
delete(metarsByWindow, curWindow - 1)
|
||||
}
|
||||
}
|
||||
metarsByWindow[curWindow] = append_metars(linesplit[1], metarsByWindow[curWindow])
|
||||
}
|
||||
}
|
||||
|
||||
// Make graph.
|
||||
p, err := plot.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
p.Title.Text = "Stratux FIS-B QoS vs. Time"
|
||||
p.X.Label.Text = "5 min intervals"
|
||||
p.Y.Label.Text = "QoS"
|
||||
|
||||
// Loop through an ordered list of the periods, so that the line connects the right dots.
|
||||
var keys []int
|
||||
for k := range qos {
|
||||
keys = append(keys, int(k))
|
||||
}
|
||||
sort.Ints(keys)
|
||||
|
||||
pts := make(plotter.XYs, len(qos))
|
||||
i := 0
|
||||
for _,k := range keys {
|
||||
v := qos[int64(k)]
|
||||
fmt.Printf("%d, %f\n", k, v)
|
||||
pts[i].X = float64(k)
|
||||
pts[i].Y = v
|
||||
i++
|
||||
}
|
||||
|
||||
err = plotutil.AddLinePoints(p, "UAT", pts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := p.Save(4 * vg.Inch, 4 * vg.Inch, "qos.png"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
package uatparse
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
UPLINK_BLOCK_DATA_BITS = 576
|
||||
UPLINK_BLOCK_BITS = (UPLINK_BLOCK_DATA_BITS + 160)
|
||||
UPLINK_BLOCK_DATA_BYTES = (UPLINK_BLOCK_DATA_BITS / 8)
|
||||
UPLINK_BLOCK_BYTES = (UPLINK_BLOCK_BITS / 8)
|
||||
|
||||
UPLINK_FRAME_BLOCKS = 6
|
||||
UPLINK_FRAME_DATA_BITS = (UPLINK_FRAME_BLOCKS * UPLINK_BLOCK_DATA_BITS)
|
||||
UPLINK_FRAME_BITS = (UPLINK_FRAME_BLOCKS * UPLINK_BLOCK_BITS)
|
||||
UPLINK_FRAME_DATA_BYTES = (UPLINK_FRAME_DATA_BITS / 8)
|
||||
UPLINK_FRAME_BYTES = (UPLINK_FRAME_BITS / 8)
|
||||
|
||||
// assume 6 byte frames: 2 header bytes, 4 byte payload
|
||||
// (TIS-B heartbeat with one address, or empty FIS-B APDU)
|
||||
UPLINK_MAX_INFO_FRAMES = (424 / 6)
|
||||
|
||||
dlac_alpha = "\x03ABCDEFGHIJKLMNOPQRSTUVWXYZ\x1A\t\x1E\n| !\"#$%&'()*+,-./0123456789:;<=>?"
|
||||
)
|
||||
|
||||
func dlac_decode(data []byte, data_len uint32) string {
|
||||
step := 0
|
||||
tab := false
|
||||
ret := ""
|
||||
for i := uint32(0); i < data_len; i++ {
|
||||
var ch uint32
|
||||
switch step {
|
||||
case 0:
|
||||
ch = uint32(data[i+0]) >> 2
|
||||
case 1:
|
||||
ch = ((uint32(data[i-1]) & 0x03) << 4) | (uint32(data[i+0]) >> 4)
|
||||
case 2:
|
||||
ch = ((uint32(data[i-1]) & 0x0f) << 2) | (uint32(data[i+0]) >> 6)
|
||||
i = i - 1
|
||||
case 3:
|
||||
ch = uint32(data[i+0]) & 0x3f
|
||||
}
|
||||
if tab {
|
||||
for ch > 0 {
|
||||
ret += " "
|
||||
ch--
|
||||
}
|
||||
tab = false
|
||||
} else if ch == 28 { // tab
|
||||
tab = true
|
||||
} else {
|
||||
ret += string(dlac_alpha[ch])
|
||||
}
|
||||
step = (step + 1) % 4
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func decodeInfoFrame(frame []byte, frame_start int, frame_len uint32, frame_type uint32) []string {
|
||||
data := frame[frame_start : frame_start+int(frame_len)]
|
||||
|
||||
if frame_type != 0 {
|
||||
return []string{} // Not FIS-B.
|
||||
}
|
||||
if frame_len < 4 {
|
||||
return []string{} // Too short for FIS-B.
|
||||
}
|
||||
|
||||
t_opt := ((uint32(data[1]) & 0x01) << 1) | (uint32(data[2]) >> 7)
|
||||
product_id := ((uint32(data[0]) & 0x1f) << 6) | (uint32(data[1]) >> 2)
|
||||
|
||||
if product_id != 413 { // FIXME.
|
||||
return []string{}
|
||||
}
|
||||
|
||||
if t_opt != 0 { //FIXME.
|
||||
// fmt.Printf("don't know time format %d\n", t_opt)
|
||||
// panic("time format")
|
||||
return []string{}
|
||||
}
|
||||
|
||||
/* fisb_hours := (uint32(data[2]) & 0x7c) >> 2
|
||||
fisb_minutes := ((uint32(data[2]) & 0x03) << 4) | (uint32(data[3]) >> 4)
|
||||
*/
|
||||
fisb_length := frame_len - 4
|
||||
fisb_data := data[4:]
|
||||
|
||||
p := dlac_decode(fisb_data, fisb_length)
|
||||
ret := make([]string, 0)
|
||||
for {
|
||||
pos := strings.Index(p, "\x1E")
|
||||
if pos == -1 {
|
||||
pos = strings.Index(p, "\x03")
|
||||
if pos == -1 {
|
||||
ret = append(ret, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
ret = append(ret, p[:pos])
|
||||
p = p[pos+1:]
|
||||
}
|
||||
return ret
|
||||
|
||||
// logger.Printf("pos=%d,len=%d,t_opt=%d,product_id=%d, time=%d:%d\n", frame_start, frame_len, t_opt, product_id, fisb_hours, fisb_minutes)
|
||||
}
|
||||
|
||||
func DecodeUplink(frame []byte) []string {
|
||||
// position_valid := (uint32(frame[5]) & 0x01) != 0
|
||||
raw_lat := (uint32(frame[0]) << 15) | (uint32(frame[1]) << 7) | (uint32(frame[2]) >> 1)
|
||||
|
||||
raw_lon := ((uint32(frame[2]) & 0x01) << 23) | (uint32(frame[3]) << 15) | (uint32(frame[4]) << 7) | (uint32(frame[5]) >> 1)
|
||||
lat := float64(raw_lat) * 360.0 / 16777216.0
|
||||
lon := float64(raw_lon) * 360.0 / 16777216.0
|
||||
|
||||
if lat > 90 {
|
||||
lat = lat - 180
|
||||
}
|
||||
if lon > 180 {
|
||||
lon = lon - 360
|
||||
}
|
||||
|
||||
// utc_coupled := (uint32(frame[6]) & 0x80) != 0
|
||||
app_data_valid := (uint32(frame[6]) & 0x20) != 0
|
||||
// slot_id := uint32(frame[6]) & 0x1f
|
||||
// tisb_site_id := uint32(frame[7]) >> 4
|
||||
|
||||
// logger.Printf("position_valid=%t, %.04f, %.04f, %t, %t, %d, %d\n", position_valid, lat, lon, utc_coupled, app_data_valid, slot_id, tisb_site_id)
|
||||
|
||||
ret := make([]string, 0)
|
||||
if !app_data_valid {
|
||||
return ret // Not sure when this even happens?
|
||||
}
|
||||
|
||||
app_data := frame[8:432]
|
||||
num_info_frames := 0
|
||||
pos := 0
|
||||
total_len := len(app_data)
|
||||
for (num_info_frames < UPLINK_MAX_INFO_FRAMES) && (pos+2 <= total_len) {
|
||||
data := app_data[pos:]
|
||||
frame_length := (uint32(data[0]) << 1) | (uint32(data[1]) >> 7)
|
||||
frame_type := uint32(data[1]) & 0x0f
|
||||
if pos+int(frame_length) > total_len {
|
||||
break // Overrun?
|
||||
}
|
||||
if frame_length == 0 && frame_type == 0 {
|
||||
break // No more frames.
|
||||
}
|
||||
pos = pos + 2
|
||||
infoFrameText := decodeInfoFrame(app_data, pos, frame_length, frame_type)
|
||||
if len(infoFrameText) > 0 {
|
||||
for _, v := range infoFrameText {
|
||||
ret = append(ret, v)
|
||||
}
|
||||
}
|
||||
pos = pos + int(frame_length)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/*
|
||||
Parse out the message from the "dump978" output format.
|
||||
*/
|
||||
|
||||
func ParseInput(buf string) ([]byte, error) {
|
||||
buf = strings.Trim(buf, "\r\n") // Remove newlines.
|
||||
x := strings.Split(buf, ";") // We want to discard everything before the first ';'.
|
||||
|
||||
s := x[0]
|
||||
|
||||
// Only want "long" uplink messages.
|
||||
if (len(s) - 1)%2 != 0 || (len(s)-1)/2 != UPLINK_FRAME_DATA_BYTES {
|
||||
return []byte{}, errors.New(fmt.Sprintf("parseInput: short read (%d).", len(s)))
|
||||
}
|
||||
|
||||
if s[0] != '+' { // Only want + ("Uplink") messages currently. - (Downlink) or messages that start with other are discarded.
|
||||
return []byte{}, errors.New("parseInput: expecting uplink frames.")
|
||||
}
|
||||
|
||||
s = s[1:]
|
||||
|
||||
// Convert the hex string into a byte array.
|
||||
frame := make([]byte, UPLINK_FRAME_DATA_BYTES)
|
||||
hex.Decode(frame, []byte(s))
|
||||
|
||||
return frame, nil
|
||||
}
|
Ładowanie…
Reference in New Issue