diff --git a/main/gen_gdl90.go b/main/gen_gdl90.go index 032693ed..d6fc3d15 100755 --- a/main/gen_gdl90.go +++ b/main/gen_gdl90.go @@ -757,7 +757,7 @@ type WeatherMessage struct { } // Send update to connected websockets. -func registerADSBTextMessageReceived(msg string) { +func registerADSBTextMessageReceived(msg string, uatMsg *uatparse.UATMsg) { x := strings.Split(msg, " ") if len(x) < 5 { return @@ -777,17 +777,14 @@ func registerADSBTextMessageReceived(msg string) { if x[0] == "PIREP" { globalStatus.UAT_PIREP_total++ } - wm.Type = x[0] wm.Location = x[1] wm.Time = x[2] wm.Data = strings.Join(x[3:], " ") wm.LocaltimeReceived = stratuxClock.Time - wmJSON, _ := json.Marshal(&wm) - // Send to weatherUpdate channel for any connected clients. - weatherUpdate.Send(wmJSON) + weatherUpdate.SendJSON(wm) } func UpdateUATStats(ProductID uint32) { @@ -900,11 +897,12 @@ func parseInput(buf string) ([]byte, uint16) { for _, f := range uatMsg.Frames { thisMsg.Products = append(thisMsg.Products, f.Product_id) UpdateUATStats(f.Product_id) + weatherRawUpdate.SendJSON(f) } // Get all of the text reports. textReports, _ := uatMsg.GetTextReports() for _, r := range textReports { - registerADSBTextMessageReceived(r) + registerADSBTextMessageReceived(r, uatMsg) } thisMsg.uatMsg = uatMsg } diff --git a/main/managementinterface.go b/main/managementinterface.go old mode 100644 new mode 100755 index d576c8f3..77045130 --- a/main/managementinterface.go +++ b/main/managementinterface.go @@ -35,6 +35,31 @@ type SettingMessage struct { // Weather updates channel. var weatherUpdate *uibroadcaster var trafficUpdate *uibroadcaster +var gdl90Update *uibroadcaster + +func handleGDL90WS(conn *websocket.Conn) { + // Subscribe the socket to receive updates. + gdl90Update.AddSocket(conn) + + // Connection closes when function returns. Since uibroadcast is writing and we don't need to read anything (for now), just keep it busy. + for { + buf := make([]byte, 1024) + _, err := conn.Read(buf) + if err != nil { + break + } + if buf[0] != 0 { // Dummy. + continue + } + time.Sleep(1 * time.Second) + } +} + +// Situation updates channel. +var situationUpdate *uibroadcaster + +// Raw weather (UATFrame packet stream) update channel. +var weatherRawUpdate *uibroadcaster /* The /weather websocket starts off by sending the current buffer of weather messages, then sends updates as they are received. @@ -57,6 +82,36 @@ func handleWeatherWS(conn *websocket.Conn) { } } +func handleJsonIo(conn *websocket.Conn) { + trafficMutex.Lock() + for _, traf := range traffic { + if !traf.Position_valid { // Don't send unless a valid position exists. + continue + } + trafficJSON, _ := json.Marshal(&traf) + conn.Write(trafficJSON) + } + // Subscribe the socket to receive updates. + trafficUpdate.AddSocket(conn) + weatherRawUpdate.AddSocket(conn) + situationUpdate.AddSocket(conn) + + trafficMutex.Unlock() + + // Connection closes when function returns. Since uibroadcast is writing and we don't need to read anything (for now), just keep it busy. + for { + buf := make([]byte, 1024) + _, err := conn.Read(buf) + if err != nil { + break + } + if buf[0] != 0 { // Dummy. + continue + } + time.Sleep(1 * time.Second) + } +} + // Works just as weather updates do. func handleTrafficWS(conn *websocket.Conn) { @@ -484,11 +539,20 @@ func viewLogs(w http.ResponseWriter, r *http.Request) { func managementInterface() { weatherUpdate = NewUIBroadcaster() trafficUpdate = NewUIBroadcaster() + situationUpdate = NewUIBroadcaster() + weatherRawUpdate = NewUIBroadcaster() + gdl90Update = NewUIBroadcaster() http.HandleFunc("/", defaultServer) http.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log")))) http.HandleFunc("/view_logs/", viewLogs) + http.HandleFunc("/gdl90", + func(w http.ResponseWriter, req *http.Request) { + s := websocket.Server{ + Handler: websocket.Handler(handleGDL90WS)} + s.ServeHTTP(w, req) + }) http.HandleFunc("/status", func(w http.ResponseWriter, req *http.Request) { s := websocket.Server{ @@ -514,6 +578,13 @@ func managementInterface() { s.ServeHTTP(w, req) }) + http.HandleFunc("/jsonio", + func(w http.ResponseWriter, req *http.Request) { + s := websocket.Server{ + Handler: websocket.Handler(handleJsonIo)} + s.ServeHTTP(w, req) + }) + http.HandleFunc("/getStatus", handleStatusRequest) http.HandleFunc("/getSituation", handleSituationRequest) http.HandleFunc("/getTowers", handleTowersRequest) diff --git a/main/network.go b/main/network.go old mode 100755 new mode 100644 index ce41dd71..28917000 --- a/main/network.go +++ b/main/network.go @@ -143,6 +143,7 @@ func sendToAllConnectedClients(msg networkMessage) { if (msg.msgType & NETWORK_GDL90_STANDARD) != 0 { // It's a GDL90 message. Send to serial output channel (which may or may not cause something to happen). serialOutputChan <- msg.msg + networkGDL90Chan <- msg.msg } netMutex.Lock() @@ -192,6 +193,14 @@ func sendToAllConnectedClients(msg networkMessage) { } var serialOutputChan chan []byte +var networkGDL90Chan chan []byte + +func networkOutWatcher() { + for { + ch := <-networkGDL90Chan + gdl90Update.SendJSON(ch) + } +} // Monitor serial output channel, send to serial port. func serialOutWatcher() { @@ -603,6 +612,7 @@ func ffMonitor() { func initNetwork() { messageQueue = make(chan networkMessage, 1024) // Buffered channel, 1024 messages. serialOutputChan = make(chan []byte, 1024) // Buffered channel, 1024 GDL90 messages. + networkGDL90Chan = make(chan []byte, 1024) outSockets = make(map[string]networkConnection) pingResponse = make(map[string]time.Time) netMutex = &sync.Mutex{} @@ -612,4 +622,5 @@ func initNetwork() { go sleepMonitor() go networkStatsCounter() go serialOutWatcher() + go networkOutWatcher() } diff --git a/main/ry835ai.go b/main/ry835ai.go index 465af148..ab5c7ebb 100644 --- a/main/ry835ai.go +++ b/main/ry835ai.go @@ -847,6 +847,15 @@ func calculateNACp(accuracy float32) uint8 { return ret } +/* + registerSituationUpdate(). + Called whenever there is a change in mySituation. +*/ +func registerSituationUpdate() { + logSituation() + situationUpdate.SendJSON(mySituation) +} + /* processNMEALine parses NMEA-0183 formatted strings against several message types. @@ -863,7 +872,7 @@ func processNMEALine(l string) (sentenceUsed bool) { defer func() { if sentenceUsed || globalSettings.DEBUG { - logSituation() + registerSituationUpdate() } mySituation.mu_GPS.Unlock() }() diff --git a/main/traffic.go b/main/traffic.go old mode 100644 new mode 100755 index 26addc3d..61a187ac --- a/main/traffic.go +++ b/main/traffic.go @@ -207,8 +207,7 @@ func sendTrafficUpdates() { traffic[icao] = ti // write the updated ti back to the map //log.Printf("Traffic age of %X is %f seconds\n",icao,ti.Age) if ti.Age > 2 { // if nothing polls an inactive ti, it won't push to the webUI, and its Age won't update. - tiJSON, _ := json.Marshal(&ti) - trafficUpdate.Send(tiJSON) + trafficUpdate.SendJSON(ti) } if ti.Position_valid && ti.Age < 6 { // ... but don't pass stale data to the EFB. TO-DO: Coast old traffic? Need to determine how FF, WingX, etc deal with stale targets. logTraffic(ti) // only add to the SQLite log if it's not stale @@ -235,8 +234,7 @@ func registerTrafficUpdate(ti TrafficInfo) { return } */ // Send all traffic to the websocket and let JS sort it out. This will provide user indication of why they see 1000 ES messages and no traffic. - tiJSON, _ := json.Marshal(&ti) - trafficUpdate.Send(tiJSON) + trafficUpdate.SendJSON(ti) } func makeTrafficReportMsg(ti TrafficInfo) []byte { diff --git a/main/uibroadcast.go b/main/uibroadcast.go index 500e2afa..b7d45ab1 100644 --- a/main/uibroadcast.go +++ b/main/uibroadcast.go @@ -11,6 +11,7 @@ package main import ( + "encoding/json" "golang.org/x/net/websocket" "sync" "time" @@ -36,6 +37,11 @@ func (u *uibroadcaster) Send(msg []byte) { u.messages <- msg } +func (u *uibroadcaster) SendJSON(i interface{}) { + j, _ := json.Marshal(&i) + u.Send(j) +} + func (u *uibroadcaster) AddSocket(sock *websocket.Conn) { u.sockets_mu.Lock() u.sockets = append(u.sockets, sock) diff --git a/uatparse/nexrad.go b/uatparse/nexrad.go new file mode 100644 index 00000000..453955aa --- /dev/null +++ b/uatparse/nexrad.go @@ -0,0 +1,147 @@ +package uatparse + +import () + +const ( + BLOCK_WIDTH = float64(48.0 / 60.0) + WIDE_BLOCK_WIDTH = float64(96.0 / 60.0) + BLOCK_HEIGHT = float64(4.0 / 60.0) + BLOCK_THRESHOLD = 405000 + BLOCKS_PER_RING = 450 +) + +type NEXRADBlock struct { + Radar_Type uint32 + Scale int + LatNorth float64 + LonWest float64 + Height float64 + Width float64 + Intensity []uint16 // Really only 4-bit values, but using this as a hack for the JSON encoding. +} + +func block_location(block_num int, ns_flag bool, scale_factor int) (float64, float64, float64, float64) { + var realScale float64 + if scale_factor == 1 { + realScale = float64(5.0) + } else if scale_factor == 2 { + realScale = float64(9.0) + } else { + realScale = float64(1.0) + } + + if block_num >= BLOCK_THRESHOLD { + block_num = block_num & ^1 + } + + raw_lat := float64(BLOCK_HEIGHT * float64(int(float64(block_num)/float64(BLOCKS_PER_RING)))) + raw_lon := float64(block_num%BLOCKS_PER_RING) * BLOCK_WIDTH + + var lonSize float64 + if block_num >= BLOCK_THRESHOLD { + lonSize = WIDE_BLOCK_WIDTH * realScale + } else { + lonSize = BLOCK_WIDTH * realScale + } + + latSize := BLOCK_HEIGHT * realScale + + if ns_flag { // Southern hemisphere. + raw_lat = 0 - raw_lat + } else { + raw_lat = raw_lat + BLOCK_HEIGHT + } + + if raw_lon > 180.0 { + raw_lon = raw_lon - 360.0 + } + + return raw_lat, raw_lon, latSize, lonSize + +} + +func (f *UATFrame) decodeNexradFrame() { + if len(f.FISB_data) < 4 { // Short read. + return + } + + rle_flag := (uint32(f.FISB_data[0]) & 0x80) != 0 + ns_flag := (uint32(f.FISB_data[0]) & 0x40) != 0 + block_num := ((int(f.FISB_data[0]) & 0x0f) << 16) | (int(f.FISB_data[1]) << 8) | (int(f.FISB_data[2])) + scale_factor := (int(f.FISB_data[0]) & 0x30) >> 4 + + if rle_flag { // Single bin, RLE encoded. + lat, lon, h, w := block_location(block_num, ns_flag, scale_factor) + var tmp NEXRADBlock + tmp.Radar_Type = f.Product_id + tmp.Scale = scale_factor + tmp.LatNorth = lat + tmp.LonWest = lon + tmp.Height = h + tmp.Width = w + tmp.Intensity = make([]uint16, 0) + + intensityData := f.FISB_data[3:] + for _, v := range intensityData { + intensity := uint16(v) & 0x7 + runlength := (uint16(v) >> 3) + 1 + for runlength > 0 { + tmp.Intensity = append(tmp.Intensity, intensity) + runlength-- + } + } + f.NEXRAD = []NEXRADBlock{tmp} + } else { + var row_start int + var row_size int + if block_num >= 405000 { + row_start = block_num - ((block_num - 405000) % 225) + row_size = 225 + } else { + row_start = block_num - (block_num % 450) + row_size = 450 + } + + row_offset := block_num - row_start + + L := int(f.FISB_data[3] & 15) + + if len(f.FISB_data) < L+3 { // Short read. + return + } + + for i := 0; i < L; i++ { + var bb int + if i == 0 { + bb = (int(f.FISB_data[3]) & 0xF0) | 0x08 + } else { + bb = int(f.FISB_data[i+3]) + } + + for j := 0; j < 8; j++ { + if bb&(1<
{{weather.location}} - {{weather.type}} - {{weather.time}} + +
{{weather.type}}
+
+   + {{weather.time}}
{{weather.data}} @@ -62,8 +65,11 @@
{{weather.location}} - {{weather.type}} - {{weather.time}} + +
{{weather.type}}
+
+   + {{weather.time}}
{{weather.data}}