## FLARM ## Version 08/2021 ## by Benedikt Wolf (D-ECHO) ## References: ## [1] https://flarm.com/wp-content/uploads/man/FLARM_OperatingManual_E.pdf (FLARM Technology and traditional/main instrument) ## [2] https://swiss-bat.ch/.cm4all/iproc.php/flarm/Handbuch_V3%2B_FW571_DV100-a-EN.pdf?cdp=a (for v3 display, the one described there additionally has a numerical distance indicator) # Initialize necessary properties var flarm_base = props.globals.initNode("/instrumentation/flarm"); var play_newcontact = flarm_base.initNode("new-contact", 0, "BOOL"); var play_warn = flarm_base.initNode("warn", 0, "INT"); # two warning levels: 0 = off; 1 = warning level 1; 2 = warning level 2 var receive_flag = flarm_base.initNode("receive", 0, "BOOL"); var ub_leds = [ flarm_base.initNode("ub-LED[0]", 0, "BOOL"), flarm_base.initNode("ub-LED[1]", 0, "BOOL"), flarm_base.initNode("ub-LED[2]", 0, "BOOL"), flarm_base.initNode("ub-LED[3]", 0, "BOOL"), ]; var leds_green = []; var leds_red = []; for( var i = 0; i <= 11; i = i + 1 ){ append(leds_green, flarm_base.initNode("LED["~i~"]", 0, "BOOL")); append(leds_red, flarm_base.initNode("LED-red["~i~"]", 0, "BOOL")); } var volts = props.globals.initNode("systems/electrical/outputs/flarm", 0.0, "DOUBLE"); var track = props.globals.getNode("/orientation/track-deg"); var ai_models = props.globals.getNode("/ai/models"); var elapsed_sec = props.globals.getNode("/sim/time/elapsed-sec"); # Initialize Arrays to internally store targets and warnings var targets = []; var warnings = []; var targets_tracked = []; # Initialize internal variables var max_dist = 4; # according to [1] typically 3-5km, depending on installation of antenna var running = 0; #Set properties for(var f=0; f<=30; f=f+1){ append(targets, nil); append(warnings, nil); append(targets_tracked, 0); } # Helper function to return relative bearing towards target var relative = func (brg, heading) { brg = brg - heading; return geo.normdeg(brg); } # Helper function to play sound for new contact var new_contact = func () { #Sound message for new contact play_newcontact.setBoolValue( !play_newcontact.getBoolValue() ); } # Target class #var target1 = Target.new(n,scnd); var Target = { new : func(n,scnd){ m = { parents : [Target] }; m.id=n; m.lat = ai_models.getNode("multiplayer["~n~"]/position/latitude-deg"); m.lon = ai_models.getNode("multiplayer["~n~"]/position/longitude-deg"); m.alt = ai_models.getNode("multiplayer["~n~"]/position/altitude-ft"); m.pos = geo.Coord.new().set_latlon( m.lat.getDoubleValue(), m.lon.getDoubleValue(), m.alt.getDoubleValue() ); m.second=0.0; var ac = geo.aircraft_position(); m.last_dist = m.pos.direct_distance_to( ac ); new_contact(); return m; }, update_data : func(){ me.pos.set_latlon( me.lat.getDoubleValue(), me.lon.getDoubleValue(), me.alt.getDoubleValue() ); }, update_LED : func( scnd ) { var ac = geo.aircraft_position(); #Time difference var delta_time = scnd - me.second; me.second = scnd; var actual_dist_now = me.pos.direct_distance_to( ac ); #Delta Distance var delta_dist = ( me.last_dist - actual_dist_now ) / delta_time; #(Theoretical) time to collision if( delta_dist == 0 ){ ttc=999; }else{ var ttc = actual_dist_now / delta_dist; } if( ttc <= 0 ){ ttc = 999; } var LED = [0,0,0,0,0,0,0,0,0,0,0,0]; var bearing = ac.course_to( me.pos ); var relative_bearing = relative( bearing, track.getDoubleValue() ); var alt_diff = math.abs( ( me.pos.alt() * FT2M ) - ac.alt() ); #Altitude difference in meters if( ttc < 6 and alt_diff < 150){ #Warn 1: all LEDs red warnings[me.id]=2; forindex(var key; LED){ LED[key]=2; } }else if(ttc<14 and alt_diff < 300){ #Warn 2: corresponding LED red warnings[me.id]=1; LED[int(relative_bearing/30+1)-1] = 2; }else{ #Normal: corresponding LED green warnings[me.id]=0; LED[int(relative_bearing/30+1)-1] = 1; } me.last_dist=actual_dist_now; return LED; }, update_ub : func(){ var ac = geo.aircraft_position(); var alt_diff = ( me.pos.alt() * FT2M ) - ac.alt(); #Altitude difference in meters var distance = ac.distance_to(me.pos); var angle = ( math.atan( alt_diff/distance ) )*R2D; return angle; }, get_distance : func() { return me.pos.alt(); }, }; setlistener("/sim/signals/fdm-initialized", func{ phase1_timer = maketimer( 0.2, flarm_start_phase2 ); phase2_timer = maketimer( 5, flarm_start_phase3 ); phase3_timer = maketimer( 2, flarm_start_phase4 ); startup_loop_timer = maketimer( 0, startup_loop ); phase1_timer.singleShot = 1; phase2_timer.singleShot = 1; phase3_timer.singleShot = 1; }); var update_FLARM = func{ for(var f=0; f<=30; f=f+1){ if(getprop("/ai/models/multiplayer["~f~"]/position/latitude-deg") != nil){ var temp_pos = geo.Coord.set_latlon( getprop("/ai/models/multiplayer["~f~"]/position/latitude-deg"), getprop("/ai/models/multiplayer["~f~"]/position/longitude-deg"), getprop("/ai/models/multiplayer["~f~"]/position/altitude-ft")); #Check whether in range and target not already existing var distance_km = temp_pos.distance_to(geo.aircraft_position())/1000; if(distance_kmmax_dist and targets_tracked[f] == 1){ #Target existing, but has moved meanwhile out of range targets[f] = nil; targets_tracked[f] = 0; } } else if ( targets_tracked[f] == 1){ #Target existing, but has meanwhile logged out targets[f]=nil; targets_tracked[f] = 0; } } receive = 0; forindex(var key; targets){ if(targets[key] != nil){ targets[key].update_data(); receive=1; } } #Check LEDs #12 LEDS, each cover 30 degrees var stored_distance=9999; var used_angle=nil; var LEDs=[0,0,0,0,0,0,0,0,0,0,0,0]; forindex(var key; targets){ if(targets[key]!=nil){ var LED=targets[key].update_LED( elapsed_sec.getDoubleValue() ); #Get the value each time again because it should be precisely the current time forindex(var f; LED){ if(LED[f]==1){ LEDs[f]=1; }else if(LED[f]==2){ LEDs[f]=2; } } var angle=targets[key].update_ub(); var distance=targets[key].get_distance(); if(distance 14){ ub_leds[0].setBoolValue(1); ub_leds[1].setBoolValue(0); ub_leds[2].setBoolValue(0); ub_leds[3].setBoolValue(0); }else if(used_angle > 0){ ub_leds[0].setBoolValue(0); ub_leds[1].setBoolValue(1); ub_leds[2].setBoolValue(0); ub_leds[3].setBoolValue(0); }else if(used_angle < -14){ ub_leds[0].setBoolValue(0); ub_leds[1].setBoolValue(0); ub_leds[2].setBoolValue(0); ub_leds[3].setBoolValue(1); }else if(used_angle < 0){ ub_leds[0].setBoolValue(0); ub_leds[1].setBoolValue(0); ub_leds[2].setBoolValue(1); ub_leds[3].setBoolValue(0); }else{ ub_leds[0].setBoolValue(0); ub_leds[1].setBoolValue(0); ub_leds[2].setBoolValue(0); ub_leds[3].setBoolValue(0); } }else{ ub_leds[0].setBoolValue(0); ub_leds[1].setBoolValue(0); ub_leds[2].setBoolValue(0); ub_leds[3].setBoolValue(0); } forindex(var key; LEDs){ if(LEDs[key]<=1){ leds_green[key].setBoolValue(LEDs[key]); leds_red[key].setBoolValue(0); }else if(LEDs[key]==2){ leds_green[key].setBoolValue(0); leds_red[key].setBoolValue(1); } } #Check Warning sounds warn=0; forindex(var key; warnings){ if(warnings[key]==2 and warn<2){ warn=2; }else if(warnings[key]==1 and warn<1){ warn=1; } } play_warn.setValue(warn); if ( volts.getDoubleValue() > 9){ receive_flag.setBoolValue(receive); if( running == 0 ){ running = 1; } } else { running = 0; foreach(var led; leds_green){ led.setBoolValue(0); } foreach(var led; leds_red){ led.setBoolValue(0); } foreach(var led; ub_leds){ led.setBoolValue(0); } receive_flag.setBoolValue(0); flarm_update.stop(); } } var flarm_update = maketimer( 1, func() { update_FLARM(); } ); # Startup as described in [1], p.6 var phase1_timer = nil; var phase2_timer = nil; var phase3_timer = nil; var startup_loop_timer = nil; var starting = 0; var leds_startup = { green: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], red: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }; var startup_loop = func { if( !starting ){ startup_loop_timer.stop(); } if( volts.getDoubleValue() > 9 ){ forindex( var key; leds_startup.green ){ leds_green[key].setBoolValue( leds_startup.green[ key ] ); leds_red[key].setBoolValue( leds_startup.red[ key ] ); } } else { foreach( var el; leds_startup.green ){ el = 0; } foreach( var el; leds_startup.red ){ el = 0; } forindex(var key; leds_green){ leds_green[key].setBoolValue(0); leds_red[key].setBoolValue(0); } starting = 0; } } flarm_start_phase1 = func () { if( volts.getDoubleValue() <= 9 ){ return; } # 1. Short beep, all LEDs light up forindex(var key; leds_startup.green ){ leds_startup.green[ key ] = 1; leds_startup.red[ key ] = 1; } play_warn.setIntValue(1); phase1_timer.restart(0.2); } flarm_start_phase2 = func () { if( volts.getDoubleValue() <= 9 ){ return; } # beep and LEDs off except to show hardware version (here: show green LEDs 0 and 1) play_warn.setIntValue(0); forindex(var key; leds_startup.green){ if( key > 1 ){ leds_startup.green[key] = 0; } leds_startup.red[key] = 0; } phase2_timer.restart(5); } flarm_start_phase3 = func () { if( volts.getDoubleValue() <= 9 ){ return; } # Show firmware version ( emit green LEDs 7 and 8 as well as 2 and 3 ) leds_startup.green[0]= 0; leds_startup.green[1]= 0; leds_startup.green[2]= 1; leds_startup.green[3]= 1; leds_startup.green[7]= 1; leds_startup.green[8]= 1; phase3_timer.restart(2); } flarm_start_phase4 = func () { if( volts.getDoubleValue() <= 9 ){ return; } # Go to normal operation starting = 0; running = 1; flarm_update.restart(1); } setlistener(volts, func{ if( running == 0 and starting == 0 and volts.getDoubleValue() > 9) { starting = 1; flarm_start_phase1(); startup_loop_timer.restart( 0 ); } });