419 lines
12 KiB
Text
419 lines
12 KiB
Text
## FLARM
|
|
## Version 09/2022
|
|
## 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");
|
|
|
|
var version = 202209; # used for backwards compatibility (e.g. Salus Combined Instrument)
|
|
|
|
# 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 Target = {
|
|
new : func(n, type, scnd){
|
|
m = { parents : [Target] };
|
|
m.id=n;
|
|
m.prop_path = ai_models.getNode(type ~ "[" ~n~ "]" );
|
|
m.lat = m.prop_path.getNode("position/latitude-deg");
|
|
m.lon = m.prop_path.getNode("position/longitude-deg");
|
|
m.alt = m.prop_path.getNode("position/altitude-ft");
|
|
m.pos = geo.Coord.new().set_latlon( m.lat.getDoubleValue(),
|
|
m.lon.getDoubleValue(),
|
|
m.alt.getDoubleValue() );
|
|
m.hdg = m.prop_path.getNode("orientation/true-heading-deg");
|
|
m.vario = m.prop_path.getNode("velocities/vertical-speed-fps");
|
|
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{
|
|
# Check MP first, AI afterwards
|
|
var type = "multiplayer";
|
|
for(var f = 0; f < 15; f += 1){
|
|
if(getprop("/ai/models/"~ type ~"[" ~f~ "]/position/latitude-deg") != nil){
|
|
var temp_pos = geo.Coord.set_latlon( getprop("/ai/models/"~ type ~"[" ~f~ "]/position/latitude-deg"),
|
|
getprop("/ai/models/"~ type ~"[" ~f~ "]/position/longitude-deg"),
|
|
getprop("/ai/models/"~ type ~"[" ~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_km < max_dist and targets_tracked[f] == 0){
|
|
#Now generate a target
|
|
targets[f]=Target.new( f, type, elapsed_sec.getDoubleValue() );
|
|
targets_tracked[f] = 1;
|
|
}else if( distance_km > max_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;
|
|
}
|
|
}
|
|
type = "aircraft";
|
|
for(var f = 0; f < 15; f += 1){
|
|
if(getprop("/ai/models/"~ type ~"[" ~f~ "]/position/latitude-deg") != nil){
|
|
var temp_pos = geo.Coord.set_latlon( getprop("/ai/models/"~ type ~"[" ~f~ "]/position/latitude-deg"),
|
|
getprop("/ai/models/"~ type ~"[" ~f~ "]/position/longitude-deg"),
|
|
getprop("/ai/models/"~ type ~"[" ~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_km < max_dist and targets_tracked[ f+15 ] == 0){
|
|
#Now generate a target
|
|
targets[ f+15 ]=Target.new( f, type, elapsed_sec.getDoubleValue() );
|
|
targets_tracked[ f+15 ] = 1;
|
|
}else if( distance_km > max_dist and targets_tracked[ f+15 ] == 1){
|
|
#Target existing, but has moved meanwhile out of range
|
|
targets[ f+15 ] = nil;
|
|
targets_tracked[ f+15 ] = 0;
|
|
}
|
|
} else if ( targets_tracked[ f+15 ] == 1){
|
|
#Target existing, but has meanwhile logged out
|
|
targets[ f+15 ]=nil;
|
|
targets_tracked[ f+15 ] = 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<stored_distance){
|
|
used_angle=angle;
|
|
stored_distance=distance;
|
|
}
|
|
|
|
}
|
|
}
|
|
if(used_angle!=nil){
|
|
if(used_angle > 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(); } );
|
|
flarm_update.simulatedTime = 1;
|
|
|
|
# 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 );
|
|
}
|
|
});
|