diff --git a/main/gps.go b/main/gps.go index df6afbe1..9228e194 100755 --- a/main/gps.go +++ b/main/gps.go @@ -99,6 +99,8 @@ type SituationData struct { AHRSGLoadMax float64 AHRSLastAttitudeTime time.Time AHRSStatus uint8 + RadarLimits int + RadarRange int } /* diff --git a/main/managementinterface.go b/main/managementinterface.go index 5997e9a7..706a6b79 100644 --- a/main/managementinterface.go +++ b/main/managementinterface.go @@ -39,6 +39,7 @@ type SettingMessage struct { // Weather updates channel. var weatherUpdate *uibroadcaster var trafficUpdate *uibroadcaster +var radarUpdate *uibroadcaster var gdl90Update *uibroadcaster func handleGDL90WS(conn *websocket.Conn) { @@ -97,6 +98,7 @@ func handleJsonIo(conn *websocket.Conn) { } // Subscribe the socket to receive updates. trafficUpdate.AddSocket(conn) + radarUpdate.AddSocket(conn) weatherRawUpdate.AddSocket(conn) situationUpdate.AddSocket(conn) @@ -145,6 +147,35 @@ func handleTrafficWS(conn *websocket.Conn) { } } +func handleRadarWS(conn *websocket.Conn) { + log.Printf("RadarWS client connected.\n") + 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. + radarUpdate.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) + } +} + + func handleStatusWS(conn *websocket.Conn) { // log.Printf("Web client connected.\n") @@ -313,6 +344,12 @@ func handleSettingsSetRequest(w http.ResponseWriter, r *http.Request) { } case "PPM": globalSettings.PPM = int(val.(float64)) + case "RadarLimits": + mySituation.RadarLimits = int(val.(float64)) + log.Printf("handleSettingsSetRequest RadarLimit:%d\n", mySituation.RadarLimits) + case "RadarRange": + mySituation.RadarRange = int(val.(float64)) + log.Printf("handleSettingsSetRequest RadarRange:%d\n", mySituation.RadarRange) case "Baud": if serialOut, ok := globalSettings.SerialOutputs["/dev/serialout0"]; ok { //FIXME: Only one device for now. newBaud := int(val.(float64)) @@ -795,6 +832,7 @@ func viewLogs(w http.ResponseWriter, r *http.Request) { func managementInterface() { weatherUpdate = NewUIBroadcaster() trafficUpdate = NewUIBroadcaster() + radarUpdate = NewUIBroadcaster() situationUpdate = NewUIBroadcaster() weatherRawUpdate = NewUIBroadcaster() gdl90Update = NewUIBroadcaster() @@ -833,6 +871,13 @@ func managementInterface() { Handler: websocket.Handler(handleTrafficWS)} s.ServeHTTP(w, req) }) + http.HandleFunc("/radar", + func(w http.ResponseWriter, req *http.Request) { + s := websocket.Server{ + Handler: websocket.Handler(handleRadarWS)} + s.ServeHTTP(w, req) + }) + http.HandleFunc("/jsonio", func(w http.ResponseWriter, req *http.Request) { diff --git a/main/sdr.go b/main/sdr.go index 16550a6a..5afd9adc 100644 --- a/main/sdr.go +++ b/main/sdr.go @@ -608,11 +608,11 @@ func configDevices(count int, esEnabled, uatEnabled, flarmEnabled bool) { // dongles are set to the same stratux id and the unconsumed, // non-anonymous, dongle makes it to this loop. for i, s := range unusedIDs { - if uatEnabled && !globalStatus.UATRadio_connected && UATDev == nil && !rES.hasID(s) && !rFLARM.hasID(s) { + if uatEnabled && !globalStatus.UATRadio_connected && UATDev == nil && !rES.hasID(s) { createUATDev(i, s, false) - } else if esEnabled && ESDev == nil && !rUAT.hasID(s) && !rFLARM.hasID(s) { + } else if esEnabled && ESDev == nil && !rUAT.hasID(s) { createESDev(i, s, false) - } else if flarmEnabled && FLARMDev == nil { + } else if flarmEnabled && FLARMDev == nil && !rFLARM.hasID(s) { createFLARMDev(i, s, false) } } @@ -702,8 +702,8 @@ func sdrWatcher() { atomic.StoreUint32(&globalStatus.Devices, uint32(interfaceCount)) // support up to two dongles - if count > 3 { - count = 3 + if count > 2 { + count = 2 } if count == prevCount && prevESEnabled == esEnabled && prevUATEnabled == uatEnabled && prevFLARMEnabled == flarmEnabled { diff --git a/main/traffic.go b/main/traffic.go index b735adf8..88edad36 100644 --- a/main/traffic.go +++ b/main/traffic.go @@ -223,6 +223,20 @@ func sendTrafficUpdates() { //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. trafficUpdate.SendJSON(ti) + var currAlt float32 + currAlt = mySituation.BaroPressureAltitude + if currAlt == 99999 { // no valid BaroAlt, take GPS instead, better than nothing + currAlt = mySituation.GPSAltitudeMSL + } + if float32(ti.Alt) <= currAlt+float32(mySituation.RadarLimits)*1.3 { //take 30% more to see moving outs + // altitude lower than upper boundary + if float32(ti.Alt) >= currAlt-float32(mySituation.RadarLimits)*1.3 { + // altitude higher than upper boundary + if !ti.BearingDist_valid || ti.Distance= currAlt-float32(mySituation.RadarLimits)*1.3 { + // altitude higher than upper boundary + if !ti.BearingDist_valid || ti.Distance= (DisplayRadius*0.75) ) { // implement hysteresis, play tone again only if 3/4 of DisplayRadius outside @@ -210,7 +217,7 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { doUpdate = 1; if ( distradius <=(DisplayRadius/2) ) { if (!traffic.alarms) traffic.alarms = 0; - if ( speechOn && (traffic.alarms =0 ) { alpha = Math.PI - Math.atan(distx/disty); @@ -222,10 +229,10 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { if ( alpha<0) alpha +=360; var oclock = Math.round(alpha/30); if (oclock <= 0 ) oclock += 12; - console.log("Distx %d Disty %d GPSCourse %f alpha-Course %f oclock %f\n", distx, disty, GPSCourse, alpha, oclock); + //console.log("Distx %d Disty %d GPSCourse %f alpha-Course %f oclock %f\n", distx, disty, GPSCourse, alpha, oclock); speaktraffic(altDiff, oclock); } - if (traffic.alarms <=MaxAlarms ) sound_alert.play(); // play alarmtone max 5 times + if ((traffic.alarms 0 ) { + if (new_traffic.timeVal >0 && timestamp) { timeLack = timestamp - new_traffic.timeVal; } - new_traffic.timeVal = timestamp; - new_traffic.time = utcTimeString(timestamp); - new_traffic.signal = obj.SignalLevel; - new_traffic.ema = expMovingAverage(new_traffic.ema, new_traffic.signal, timeLack); - new_traffic.lat = obj.Lat; new_traffic.lon = obj.Lng; var n = Math.round(obj.Alt / 25) * 25; @@ -309,8 +311,8 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { new_traffic.vspeed = Math.round(obj.Vvel / 100) * 100 - new_traffic.age = obj.Age; - new_traffic.ageLastAlt = obj.AgeLastAlt; + new_traffic.Last_seen = Date.parse(obj.Last_seen); + new_traffic.Last_alt = Date.parse(obj.Last_alt); new_traffic.dist = (obj.Distance/1852); new_traffic.tail = obj.Tail; //registration No } @@ -362,16 +364,16 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { } if ((invalidIdx < 0) && (!message.Position_valid)) { // new aircraft without position - if ( altDiffValid && (Math.abs(altDiff) <= (AltDiffThreshold + storageDiff)) ) { - setAircraft(message, new_traffic); //store in any case, since EMA needs history of dB + if ( altDiffValid && (Math.abs(altDiff) <= AltDiffThreshold ) ) { + setAircraft(message, new_traffic); checkCollisionVector(new_traffic); $scope.data_list_invalid.unshift(new_traffic); // add to start of invalid array. } // else not added in list, since not relevant } // Handle the negative cases of those above - where an aircraft moves from "valid" to "invalid" or vice-versa. - if ((validIdx >= 0) && (!message.Position_valid)) { //known valid aircraft now with invalid position - // Position is not valid any more. Remove from "valid" table. + if ((validIdx >= 0) && !message.Position_valid ) { + // Position is not valid any more or outside Threshold. Remove from "valid" table. if ( $scope.data_list[validIdx].planeimg ) { $scope.data_list[validIdx].planeimg.remove().forget(); // remove plane image $scope.data_list[validIdx].planetext.remove().forget(); // remove plane image @@ -403,7 +405,7 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { return; // we are getting called once after clicking away from the status page if (($scope.socket === undefined) || ($scope.socket === null)) { - socket = new WebSocket(URL_TRAFFIC_WS); + socket = new WebSocket(URL_RADAR_WS); $scope.socket = socket; // store socket in scope for enter/exit usage sit_socket = new WebSocket(URL_GPS_WS); // socket for situation $scope.sit_socket = sit_socket; @@ -465,6 +467,7 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { var tempUptimeClock = new Date(Date.parse(globalStatus.UptimeClock)); var uptimeClockString = tempUptimeClock.toUTCString(); $scope.UptimeClock = uptimeClockString; + $scope.StratuxClock = Date.parse(globalStatus.UptimeClock); var tempLocalClock = new Date; $scope.LocalClock = tempLocalClock.toUTCString(); @@ -487,12 +490,14 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { // perform cleanup every 10 seconds var clearStaleTraffic = $interval(function () { - // remove stale aircraft = anything more than 20 seconds without a position update + // remove stale aircraft = anything more than x seconds without a position update + var cutoff = 59; + var cutTime = $scope.StratuxClock-cutoff*1000; // Clean up "valid position" table. for (var i = $scope.data_list.length; i > 0; i--) { - if ($scope.data_list[i - 1].age >= cutoff) { + if ($scope.data_list[i - 1].Last_seen < cutTime) { if ( $scope.data_list[i-1].planeimg ) { $scope.data_list[i-1].planeimg.remove().forget(); // remove plane image $scope.data_list[i-1].planetext.remove().forget(); // remove plane image @@ -509,13 +514,15 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { // Clean up "invalid position" table. for (var i = $scope.data_list_invalid.length; i > 0; i--) { - if (($scope.data_list_invalid[i - 1].age >= cutoff) || ($scope.data_list_invalid[i - 1].ageLastAlt >= cutoff)) { + //if (($scope.data_list_invalid[i - 1].timeVal < cutTime) || ($scope.data_list_invalid[i - 1].ageLastAlt < cutTime)) { + if ($scope.data_list_invalid[i - 1].Last_alt < cutTime) { if ( $scope.data_list_invalid[i-1].circ ) { // is displayed $scope.data_list_invalid[i-1].circ.remove().forget(); } $scope.data_list_invalid.splice(i - 1, 1); } } + radar.update(); }, (1000 * 10), 0, false); @@ -533,7 +540,7 @@ function RadarCtrl($rootScope, $scope, $state, $http, $interval) { $interval.cancel(clearStaleTraffic); }; - radar = new RadarRenderer ("radar_display",$scope); + radar = new RadarRenderer ("radar_display",$scope,$http); // Traffic Controller tasks connect($scope); // connect - opens a socket and listens for messages @@ -553,6 +560,11 @@ function clearRadarTraces ($scope) { } } } + for (var i = $scope.data_list_invalid.length; i > 0; i--) { //clear circles + if ( $scope.data_list_invalid[i-1].circ ) { // is displayed + $scope.data_list_invalid[i-1].circ.remove().forget(); + } + } } function requestFullScreen(el) { @@ -574,7 +586,63 @@ function cancelFullScreen(el) { } } -function RadarRenderer(locationId,$scope) { +function displaySoundStatus(speech, soundMode) { + switch (soundMode) { + case 0: + if ( synth ) { + var utterOn = new SpeechSynthesisUtterance("Beep and Speech on"); + utterOn.lang="en-US"; + synth.speak(utterOn); + } + speech.get(0).removeClass('zoom').addClass('zoomInvert'); + speech.get(1).removeClass('tSmall').addClass('tSmallInvert').text('BpSp').cx(16).cy(0); + break; + case 1: + if ( synth ) { + var utterOn = new SpeechSynthesisUtterance("Beep only"); + utterOn.lang="en-US"; + synth.speak(utterOn); + } + speech.get(0).removeClass('zoom').addClass('zoomInvert'); + speech.get(1).removeClass('tSmall').addClass('tSmallInvert').text('Beep').cx(16).cy(0); + break; + case 2: + if ( synth ) { + var utterOn = new SpeechSynthesisUtterance("Speech only"); + utterOn.lang="en-US"; + synth.speak(utterOn); + } + speech.get(0).removeClass('zoom').addClass('zoomInvert'); + speech.get(1).removeClass('tSmall').addClass('tSmallInvert').text('Spch').cx(16).cy(0); + break; + default: + if ( synth ) { + var utterOn = new SpeechSynthesisUtterance("Sound off"); + utterOn.lang="en-US"; + synth.speak(utterOn); + } + speech.get(0).removeClass('zoomInvert').addClass('zoom'); + speech.get(1).removeClass('tSmallInvert').addClass('tSmall').text('SnOff').cx(18).cy(0); + } +} + +function communicateLimits (threshold,radarrange,$http) { //tell raspi the limits for callback + var newsettings = { + "RadarLimits": threshold, + "RadarRange": radarrange + }; + msg = angular.toJson(newsettings); + // Simple POST request example (note: response is asynchronous) + $http.post(URL_SETTINGS_SET, msg). + then(function (response) { + // do nothing + }, function (response) { + // do nothing + }); +} + +function RadarRenderer(locationId,$scope,$http) { + this.$scope = $scope; this.width = -1; this.height = -1; @@ -582,8 +650,8 @@ function RadarRenderer(locationId,$scope) { this.canvas = document.getElementById(this.locationId); this.resize(); - AltDiffThreshold = altDiff[altindex]; - DisplayRadius = zoom[zoomfactor]; + AltDiffThreshold = 20; + DisplayRadius = 10; // Draw the radar using the svg.js library var radarAll = SVG(this.locationId).viewbox(-201, -201, 402, 302).group().addClass('radar'); @@ -593,6 +661,8 @@ function RadarRenderer(locationId,$scope) { card.circle(200).cx(0).cy(0); this.displayText = radarAll.text(DisplayRadius+' nm').addClass('textOutside').x(-200).cy(-158); //not rotated this.altText = radarAll.text('\xB1'+AltDiffThreshold+'00ft').addClass('textOutsideRight').x(200).cy(-158); //not rotated + communicateLimits(100*AltDiffThreshold,DisplayRadius,$http); // initially sent Thresholds + this.fl = radarAll.text("FL"+Math.round(BaroAltitude/100)).addClass('textSmall').move(7,5); card.text("N").addClass('textDir').center(0,-190); card.text("S").addClass('textDir').center(0,190); card.text("W").addClass('textDir').center(-190,0); @@ -607,70 +677,113 @@ function RadarRenderer(locationId,$scope) { zoomin.text('Ra-').cx(12).cy(2).addClass('textZoom'); zoomin.on('click', function () { var animateTime= 200; - if (zoomfactor > 0 ) { - zoomfactor--; - } else { - animateTime = 20; + var newval = DisplayRadius; + switch (DisplayRadius) { + case 40: + newval = 20; + break; + case 20: + newval = 10; + break; + case 10: + newval = 5; + break; + case 5: + newval = 2; + break; + default: // keep 2 + animateTime = 20; } - DisplayRadius = zoom[zoomfactor]; + communicateLimits(100*AltDiffThreshold,newval,$http); zoomin.animate(animateTime).rotate(90, 0, 0); - this.displayText.text(DisplayRadius+' nm'); - //update(); zoomin.animate(animateTime).rotate(0, 0, 0); - clearRadarTraces($scope); }, this); var zoomout = radarAll.group().cx(-177).cy(-190).addClass('zoom'); zoomout.circle(45).cx(0).cy(0).addClass('zoom'); zoomout.text('Ra+').cx(12).cy(2).addClass('textZoom'); zoomout.on('click', function () { - var animateTime= 200; - if (zoomfactor < (zoom.length-1) ) { - zoomfactor++; - } else { - animateTime = 20; + var animateTime = 200; + var newval = DisplayRadius; + switch (DisplayRadius) { + case 2: + newval = 5; + break; + case 5: + newval = 10; + break; + case 10: + newval = 20; + break; + case 20: + newval = 40; + break; + default: // keep 40 + animateTime = 20; } - DisplayRadius = zoom[zoomfactor]; + communicateLimits(100*AltDiffThreshold,newval,$http); zoomout.animate(animateTime).rotate(90, 0, 0); - this.displayText.text(DisplayRadius+' nm'); zoomout.animate(animateTime).rotate(0, 0, 0); - clearRadarTraces($scope); }, this); var altmore = radarAll.group().cx(120).cy(-190).addClass('zoom'); altmore.circle(45).cx(0).cy(0).addClass('zoom'); altmore.text('Alt+').cx(12).cy(2).addClass('textZoom'); altmore.on('click', function () { + var newval = AltDiffThreshold; var animateTime= 200; - if (altindex < (altDiff.length-1) ) { - altindex++; - } else { - animateTime = 20; - } - AltDiffThreshold = altDiff[altindex]; + switch (AltDiffThreshold) { + case 5: + newval = 10; + break; + case 10: + newval = 20; + break; + case 20: + newval = 50; + break; + case 50: + newval = 100; + break; + case 100: + newval = 500; + break; + default: + animateTime = 20; + } + communicateLimits(100*newval,DisplayRadius,$http); altmore.animate(animateTime).rotate(90, 0, 0); - this.altText.text('\xB1'+AltDiffThreshold+'00ft'); - //update(); altmore.animate(animateTime).rotate(0, 0, 0); - clearRadarTraces($scope); }, this); var altless = radarAll.group().cx(177).cy(-190).addClass('zoom'); altless.circle(45).cx(0).cy(0).addClass('zoom'); altless.text('Alt-').cx(12).cy(2).addClass('textZoom'); altless.on('click', function () { + var newval = AltDiffThreshold; var animateTime= 200; - if (altindex > 0 ) { - altindex--; - } else { - animateTime = 20; - } - AltDiffThreshold = altDiff[altindex]; + switch (AltDiffThreshold) { + case 500: + newval = 100; + break; + case 100: + newval = 50; + break; + case 50: + newval = 20; + break; + case 20: + newval = 10; + break; + case 10: + newval = 5; + break; + default: //5 stays 5 + animateTime = 20; + } + communicateLimits(100*newval,DisplayRadius,$http); altless.animate(animateTime).rotate(90, 0, 0); - //update(); - this.altText.text('\xB1'+AltDiffThreshold+'00ft'); altless.animate(animateTime).rotate(0, 0, 0); - clearRadarTraces($scope); }, this); var fullscreen = radarAll.group().cx(185).cy(-125).addClass('zoom'); @@ -692,30 +805,28 @@ function RadarRenderer(locationId,$scope) { }, this); - var speech = radarAll.group().cx(-185).cy(-125).addClass('zoom'); + var speech = radarAll.group().cx(-185).cy(-125); speech.rect(40,35).radius(10).cx(0).cy(0).addClass('zoom'); - speech.text('Spk').cx(12).cy(2).addClass('textZoom'); + speech.text('Undef').cx(16).cy(0).addClass('tSmall'); synth = window.speechSynthesis; + if (!synth) soundType=1; // speech function not working, default now beep + displaySoundStatus(speech,soundType); speech.on('click', function () { - if (!synth) return; // speech function not working - if ( ! speechOn ) { - var utterOn = new SpeechSynthesisUtterance("Speech on"); - utterOn.lang="en-US"; - utterOn.rate=1.1; - speech.get(0).removeClass('zoom').addClass('zoomInvert'); - speech.get(1).removeClass('textZoom').addClass('textZoomInvert'); - synth.speak(utterOn); - speechOn = true; - } else { - var utterOff = new SpeechSynthesisUtterance("Speech off"); - utterOff.lang="en-US"; - utterOff.rate=1.1; - speech.get(0).removeClass('zoomInvert').addClass('zoom'); - speech.get(1).removeClass('textZoomInvert').addClass('textZoom'); - synth.speak(utterOff); - speechOn = false; + switch (soundType) { + case 0: //speech and beep + soundType = 1; // beep only + break; + case 1: //beep only + if (synth) { soundType=2; } else { soundType=3 }; + break; + case 2: //speech only + soundType = 3; //Sound off + break; + default: + soundType = 0; //speech and beep } + displaySoundStatus(speech,soundType); }, this); @@ -740,8 +851,23 @@ RadarRenderer.prototype = { }, update: function () { - if (this.fl) this.fl.remove(); - this.rScreen.rotate(-GPSCourse,0,0); // rotate conforming to GPSCourse - this.fl = this.allScreen.text("FL"+Math.round(BaroAltitude/100)).addClass('textSmall').move(7,5); + if ( BaroAltitude != OldBaroAltitude ) { + this.fl.text("FL"+Math.round(BaroAltitude/100)); // just update text + OldBaroAltitude = BaroAltitude; + } + if ( AltDiffThreshold != OldAltDiffThreshold ) { + this.altText.text('\xB1'+AltDiffThreshold+'00ft'); + clearRadarTraces(this.$scope); + OldAltDiffThreshold = AltDiffThreshold; + } + if ( DisplayRadius != OldDisplayRadius ) { + this.displayText.text(DisplayRadius+' nm'); + clearRadarTraces(this.$scope); + OldDisplayRadius = DisplayRadius; + } + if ( GPSCourse != OldGPSCourse ) { + this.rScreen.rotate(-GPSCourse,0,0); // rotate conforming to GPSCourse + OldGPSCourse = GPSCourse; + } } }; diff --git a/web/plates/js/status.js b/web/plates/js/status.js index 43729e9c..bd75eb7c 100644 --- a/web/plates/js/status.js +++ b/web/plates/js/status.js @@ -37,7 +37,7 @@ function StatusCtrl($rootScope, $scope, $state, $http, $interval) { }; socket.onmessage = function (msg) { - console.log('Received status update.') + //console.log('Received status update.') var status = JSON.parse(msg.data) // Update Status diff --git a/web/plates/js/traffic.js b/web/plates/js/traffic.js index 5c570629..e1882826 100644 --- a/web/plates/js/traffic.js +++ b/web/plates/js/traffic.js @@ -119,7 +119,7 @@ function TrafficCtrl($rootScope, $scope, $state, $http, $interval) { socket.onmessage = function (msg) { - console.log('Received traffic update.') + //console.log('Received traffic update.') var message = JSON.parse(msg.data); $scope.raw_data = angular.toJson(msg.data, true); @@ -238,4 +238,4 @@ function TrafficCtrl($rootScope, $scope, $state, $http, $interval) { // Traffic Controller tasks connect($scope); // connect - opens a socket and listens for messages -}; \ No newline at end of file +}; diff --git a/web/plates/radar-help.html b/web/plates/radar-help.html index fb89fc8f..8f8930c8 100644 --- a/web/plates/radar-help.html +++ b/web/plates/radar-help.html @@ -6,6 +6,6 @@

An aircraft is removed from the radar, if it is outside the range, or if there is no transmission received within the last 60 seconds.

The aircraft in the middle is a symbol for your own aircraft. Right below the own aircraft the current flight-level is displayed. This level is received from barometric measurement, if your stratux is equipped with it. If not, the flight level is depending on the GPS altitude. Be aware that in this case the altitude difference to other aircraft may be wrong because their position reports are based on barometric measurement. A barometric sensor is therefore absolutely recommended!

A short beep sound is played whenever an aircraft is within the inner circle range.

-

"Spk" toogles speech output, if your web browser supports it. If switched on, a warning e.g. "Traffic at 10 o'clock plus 500 feet" is spoken, whenever an aircraft enters the inner circle.

+

"BpSp/Beep/Spch/SnOff" toogles between different sound outputs. Default is Beep (5x) and a spoken warning, alternatively only Beep, only Speech or silence. If speech is switched on, a warning e.g. "Traffic at 10 o'clock plus 500 feet" is spoken, whenever an aircraft enters the inner circle.

"F/S" toggles full screen mode, which displays the radar in maximum screen size (landscape format recommended).