Showing 9 changed files with 364 additions and 0 deletions
+45
Nasal/core.nas
... ...
@@ -102,6 +102,51 @@ var deviceClass = {
102 102
             data.timers.map.singleShot = 1;
103 103
             data.timers.map.start();
104 104
         }
105
+        if (getprop('/instrumentation/tcas/serviceable') != nil) {
106
+            m.data.tcas = 0;
107
+            if (!contains(data.timers, 'tcas')) {
108
+                data.timers.tcas = maketimer ( 1,
109
+                    func {
110
+                        var tcas_dirty = [];
111
+                        var _range = 6;
112
+                        var _minLevel = -1;
113
+                        foreach (var ac; props.globals.getNode("/ai/models").getChildren("multiplayer")) {
114
+                            if (ac.getValue("valid")) {
115
+                                var range = ac.getNode("radar/range-nm").getValue();
116
+                                if (range != nil) {
117
+                                    if (debug.isnan(range) == 0) {
118
+                                        if ( (range > 0) and (range <= _range) ) {
119
+                                            var nTcasThreat = ac.getNode("tcas/threat-level");
120
+                                            if (nTcasThreat != nil) {
121
+                                                var level = nTcasThreat.getValue();
122
+                                                if (level > _minLevel) {
123
+                                                    var lat     = ac.getNode("position/latitude-deg").getValue();
124
+                                                    var lon     = ac.getNode("position/longitude-deg").getValue();
125
+                                                    var aAlt    = ac.getNode("position/altitude-ft").getValue();
126
+                                                    var vs      = ac.getNode("velocities/vertical-speed-fps").getValue();
127
+                                                    var alt     = math.floor(((aAlt - data.alt) / 100) + 0.5);
128
+                                                    if (debug.isnan(lat) == 0 and
129
+                                                        debug.isnan(lon) == 0 and
130
+                                                        debug.isnan(vs)  == 0 and
131
+                                                        debug.isnan(alt) == 0) {
132
+                                                            append(tcas_dirty, {
133
+                                                                lat: lat,
134
+                                                                lon: lon,
135
+                                                                vs: vs,
136
+                                                                alt: alt,
137
+                                                                level: level,
138
+                                                            });
139
+                                                    } } } } } } } }
140
+                        data.tcas = tcas_dirty;
141
+                    }
142
+                );
143
+                data.timers.tcas.start();
144
+            }
145
+        }
146
+        else {
147
+            delete(m.softkeys.bindings.PFD.INSET, 'TRAFFIC');
148
+            delete(m.softkeys.bindings.MFD.MAP,   'TRAFFIC');
149
+        }
105 150
         m.display.showInitProgress();
106 151
 
107 152
         setprop('/instrumentation/zkv1000/' ~ m.name ~ '/status', 1);
+3
Nasal/display.nas
... ...
@@ -106,6 +106,7 @@ var displayClass = {
106 106
                     'WindData', 'WindData-OPTN1', 'WindData-OPTN2', 'WindData-OPTN1-HDG', 'WindData-OPTN2-symbol', 'WindData-OPTN2-headwind', 'WindData-OPTN2-crosswind', 'WindData-NODATA',
107 107
                     'AOA', 'AOA-needle', 'AOA-text', 'AOA-approach',
108 108
                     'MFD-navbox',
109
+                    'Traffic',
109 110
 # }}}
110 111
                 );
111 112
                 append(groups.clip,
... ...
@@ -564,6 +565,8 @@ var displayClass = {
564 565
             .setRotation(-roll * D2R);
565 566
         me.screenElements['SlipSkid']
566 567
             .setTranslation(getprop("/instrumentation/slip-skid-ball/indicated-slip-skid") * 10, 0);
568
+        me.screenElements['Traffic']
569
+            .setVisible(size(data.tcas));
567 570
         settimer(func me.updateAI(), 0.1);
568 571
     },
569 572
 #}}}
+1
Nasal/map.nas
... ...
@@ -37,6 +37,7 @@ var mapClass = {
37 37
         m.layers.tiles = MapTiles.new(m.device, m.group);
38 38
         m.layers.route = MapRoute.new(m.device, m.group);
39 39
         m.layers.navaids = MapNavaids.new(m.device, m.group);
40
+        m.layers.tcas = MapTcas.new(m.device, m.group);
40 41
 
41 42
         m.mapOrientation = m.device.display.display.createGroup('MapOrientation')
42 43
             .setVisible(m.visibility);
+121
Nasal/maps/tcas.nas
... ...
@@ -0,0 +1,121 @@
1
+# vim: set foldmethod=marker foldmarker={{{,}}} :
2
+
3
+var TcasItemClass = {
4
+# get data from AI and multiplayers {{{
5
+    new : func(canvasGroup, index) {
6
+        var m = {parents : [TcasItemClass]};
7
+        m._group = canvasGroup.createChild("group", "TcasItem_" ~ index);
8
+
9
+        canvas.parsesvg(m._group, data.zkv1000_reldir ~ "Systems/tcas.svg");
10
+        m._can = {
11
+            Alt : [
12
+                m._group.getElementById("Alt_above").setVisible(0),
13
+                m._group.getElementById("Alt_below").setVisible(0)
14
+            ],
15
+            Arrow : [
16
+                m._group.getElementById("Arrow_climb").setVisible(0),
17
+                m._group.getElementById("Arrow_descent").setVisible(0)
18
+            ]
19
+        };
20
+        m._can.ThreadLevel = [];
21
+
22
+        for (var i=0; i<4; i+=1)
23
+            append(m._can.ThreadLevel,
24
+                    m._group.getElementById("Thread_Level_" ~ i).setVisible(0));
25
+
26
+        m._colors = [ '#00ffff', '#00ffff', '#ff7f2a', '#ff0000' ];
27
+
28
+        return m;
29
+    },
30
+
31
+    setData : func(lat, lon, alt, vs, level) {
32
+        me._group.setVisible(1);
33
+        me._group.setGeoPosition(lat, lon);
34
+        
35
+        if (alt == 0) {
36
+            me._can.Alt[0].setVisible(0);
37
+            me._can.Alt[1].setVisible(0);
38
+        }
39
+        else {
40
+            me._can.Alt[alt < 0]
41
+                .setText(sprintf("%+i", alt))
42
+                .set('fill', me._colors[level])
43
+                .setVisible(1);
44
+
45
+            me._can.Alt[alt > 0].setVisible(0);
46
+        }
47
+                
48
+        if (vs >= -3 and vs <= 3) {
49
+            me._can.Arrow[0].setVisible(0);
50
+            me._can.Arrow[1].setVisible(0);
51
+        }
52
+        else {
53
+            me._can.Arrow[vs < -3]
54
+                .set('fill', me._colors[level])
55
+                .set('stroke', me._colors[level])
56
+                .setVisible(1);
57
+
58
+            me._can.Arrow[vs > 3].setVisible(0);
59
+        }
60
+        
61
+        me._can.ThreadLevel[0].setVisible(level == 0);
62
+        me._can.ThreadLevel[1].setVisible(level == 1);
63
+        me._can.ThreadLevel[2].setVisible(level == 2);
64
+        me._can.ThreadLevel[3].setVisible(level == 3);
65
+        me._group.set("z-index", level + 1);
66
+    },
67
+};
68
+# }}} 
69
+
70
+var MapTcas = {
71
+# init TCAS layer and update {{{
72
+    new : func(device, group) {
73
+        var m = {parents:[MapTcas]}; 
74
+        if (getprop('/instrumentation/tcas/serviceable') == nil) {
75
+            m.update = func;
76
+            m.setVisible = func;
77
+        }
78
+        else {
79
+            m.device = device;
80
+            m.visibility = 0;
81
+            m.group = group.createChild('map', 'tcas')
82
+                .setTranslation(
83
+                    m.device.role == 'MFD' ? (m.device.data.mapview[0] + m.device.data.mapclip.left)/2 : 120,
84
+                    m.device.role == 'MFD' ? 400 : 600)
85
+                .setVisible(m.visibility);
86
+            m._item      = [];
87
+            m._itemIndex = 0;
88
+            m._itemCount = 0;
89
+# /instrumentation/tcas/inputs/mode
90
+            m.MODE      = ["Normal","Above","Unlimited","Below"];
91
+        }
92
+        return m;
93
+    },
94
+
95
+    update : func() {
96
+        if (me.device.data.tcas == 0)
97
+            return;
98
+        me.group._node.getNode('ref-lat', 1).setDoubleValue(data.lat);
99
+        me.group._node.getNode('ref-lon', 1).setDoubleValue(data.lon);
100
+        me.group._node
101
+            .getNode('range', 1).setDoubleValue(me.device.data['range-factor']);
102
+        me._itemIndex = 0;
103
+        foreach (var ac; data.tcas) {
104
+            if (me._itemIndex >= me._itemCount) {
105
+                append(me._item, TcasItemClass.new(me.group, me._itemIndex));
106
+                me._itemCount += 1;
107
+            }
108
+            me._item[me._itemIndex].setData(ac.lat, ac.lon, ac.alt, ac.vs, ac.level);
109
+            me._itemIndex += 1;
110
+        }
111
+                
112
+        for (; me._itemIndex < me._itemCount; me._itemIndex += 1) {
113
+            me._item[me._itemIndex]._group.setVisible(0);
114
+        }
115
+    },
116
+
117
+    setVisible : func (v) {
118
+        me.group.setVisible(v);
119
+    },
120
+};
121
+# }}} 
+20
Nasal/softkeys.nas
... ...
@@ -61,6 +61,15 @@ var softkeysClass = {
61 61
                     me.device.map.setVisible(0);
62 62
                     me.device.display.screenElements['PFD-Map-bg'].hide();
63 63
                 },
64
+                TRAFFIC: func {
65
+                    me.device.data.tcas = ! me.device.data.tcas;
66
+                    if (me.device.data.tcas)
67
+                        me.colored['INSETTRAFFIC'] = me.device.data.tcas;
68
+                    else
69
+                        delete(me.colored, 'INSETTRAFFIC');
70
+                    me.device.display.updateSoftKeys();
71
+                    me.device.map.layers.tcas.setVisible(me.device.data.tcas);
72
+                },
64 73
                 hook : func {
65 74
                     me.device.display.screenElements['PFD-Map-bg'].show();
66 75
                     me.device.map.setVisible(1);
... ...
@@ -492,6 +501,17 @@ var softkeysClass = {
492 501
                     me.device.display.updateSoftKeys();
493 502
                 },
494 503
             },
504
+            MAP: {
505
+                TRAFFIC: func {
506
+                    me.device.data.tcas = ! me.device.data.tcas;
507
+                    if (me.device.data.tcas)
508
+                        me.colored['MAPTRAFFIC'] = me.device.data.tcas;
509
+                    else
510
+                        delete(me.colored, 'MAPTRAFFIC');
511
+                    me.device.display.updateSoftKeys();
512
+                    me.device.map.layers.tcas.setVisible(me.device.data.tcas);
513
+                },
514
+            },
495 515
         },
496 516
     },
497 517
 };
+16
README.md
... ...
@@ -78,6 +78,7 @@ Please report bug at <seb.marque@free.fr>.
78 78
   * single-prop EIS: texts displayed, animations for fuel
79 79
   * ND and map display: synchronized tiles and navaids, range change, map heads up
80 80
   * rotation and zooming of online maps in-flight
81
+  * simple Traffic Alert Collision Avoidance System (TCAS)
81 82
 * ![][90%]
82 83
   * TMR/REF Timer ![][done], Vspeeds ![][done], minimums ![][pending] (don't understand the role of this)
83 84
   * angle of attack display (not sure about calculation): specific for each airplane (see Installation instructions below)
... ...
@@ -239,6 +240,21 @@ Actually there are only two "types of display": MFD or PFD, which is known by th
239 240
 Other devices as keyboard or non-display can also exists, as long as they don't have a `status` property...
240 241
 Not sur I'm clear on this point though :)
241 242
 
243
+## TCAS
244
+To enable TCAS you need to make it serviceable in your aircraft before the ZKV1000 being powered on, so the best is to set it in the aircraft configuration files.
245
+Actually it checks if the property `/instrumentation/tcas/serviceable` is set to boolean true value.
246
+
247
+        <tcas>
248
+          <name>tcas</name>
249
+          <number>0</number>
250
+          <serviceable type='bool'>true</serviceable>
251
+          <inputs>
252
+            <mode type="int">5</mode>
253
+          </inputs>
254
+        </tcas>
255
+
256
+Moreover it only shows alerts with an annunciation "TRAFFIC" on PFD, and shows icons on maps (MFD and INSET), no decision is taken, no relation with transponder and no evasion scenari and no evasion scenario.
257
+
242 258
 ## Map tiles origin
243 259
 By defaults the maps tiles come from `https://maps.wikimedia.org`, type `osm-intl` (please read [https://www.wikimedia.org/wiki/Maps]()), but you can choose your favorite one if you've got one. I've tested `opentopomap.org` and `thunderforest.com` (my favourite).
244 260
 You can tell the zkv1000 the tile server, type and eventually apikey by using `--prop:` option while starting FlightGear session:
+23
Systems/PFD.svg
... ...
@@ -7270,6 +7270,29 @@
7270 7270
            id="tspan5048"
7271 7271
            sodipodi:role="line">000°</tspan></text>
7272 7272
     </g>
7273
+    <g
7274
+       id="Traffic"
7275
+       inkscape:label="Traffic"
7276
+       transform="translate(198.51279,102.67903)">
7277
+      <rect
7278
+         y="-35.178177"
7279
+         x="50.959221"
7280
+         height="26.620489"
7281
+         width="108.00312"
7282
+         id="rect3397"
7283
+         style="opacity:1;fill:#ffff00;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
7284
+      <text
7285
+         id="text3395"
7286
+         y="-13.131604"
7287
+         x="56.509609"
7288
+         style="font-style:normal;font-weight:normal;font-size:24px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
7289
+         xml:space="preserve"><tspan
7290
+           style="fill:#000000;fill-opacity:1"
7291
+           y="-13.131604"
7292
+           x="56.509609"
7293
+           id="tspan3393"
7294
+           sodipodi:role="line">TRAFFIC</tspan></text>
7295
+    </g>
7273 7296
     <g
7274 7297
        id="SelectedALT"
7275 7298
        inkscape:label="SelectedALT"
+133
Systems/tcas.svg
... ...
@@ -0,0 +1,133 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+<svg
5
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
6
+   xmlns:cc="http://creativecommons.org/ns#"
7
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
+   xmlns:svg="http://www.w3.org/2000/svg"
9
+   xmlns="http://www.w3.org/2000/svg"
10
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
+   width="50"
13
+   height="64"
14
+   id="svg2"
15
+   version="1.1"
16
+   inkscape:version="0.48.4 r9939"
17
+   sodipodi:docname="tcas.svg">
18
+  <defs
19
+     id="defs4" />
20
+  <sodipodi:namedview
21
+     id="base"
22
+     pagecolor="#ffffff"
23
+     bordercolor="#666666"
24
+     borderopacity="1.0"
25
+     inkscape:pageopacity="0.0"
26
+     inkscape:pageshadow="2"
27
+     inkscape:zoom="11.2"
28
+     inkscape:cx="0.78607779"
29
+     inkscape:cy="33.324257"
30
+     inkscape:document-units="px"
31
+     inkscape:current-layer="layer1"
32
+     showgrid="false"
33
+     inkscape:snap-page="true"
34
+     inkscape:snap-global="true"
35
+     inkscape:snap-bbox="true"
36
+     showguides="true"
37
+     inkscape:guide-bbox="true"
38
+     inkscape:bbox-nodes="true"
39
+     inkscape:window-width="1920"
40
+     inkscape:window-height="1025"
41
+     inkscape:window-x="-2"
42
+     inkscape:window-y="-3"
43
+     inkscape:window-maximized="1">
44
+    <sodipodi:guide
45
+       orientation="1,0"
46
+       position="50,35.982143"
47
+       id="guide3001" />
48
+  </sodipodi:namedview>
49
+  <metadata
50
+     id="metadata7">
51
+    <rdf:RDF>
52
+      <cc:Work
53
+         rdf:about="">
54
+        <dc:format>image/svg+xml</dc:format>
55
+        <dc:type
56
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
57
+        <dc:title />
58
+      </cc:Work>
59
+    </rdf:RDF>
60
+  </metadata>
61
+  <g
62
+     inkscape:label="Ebene 1"
63
+     inkscape:groupmode="layer"
64
+     id="layer1"
65
+     transform="translate(-15,-1020)">
66
+    <text
67
+       xml:space="preserve"
68
+       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;font-family:Liberation Mono;-inkscape-font-specification:Liberation Mono"
69
+       x="38.328125"
70
+       y="1003.079"
71
+       id="Alt_above"
72
+       sodipodi:linespacing="125%"><tspan
73
+         sodipodi:role="line"
74
+         id="tspan2989"
75
+         x="38.328125"
76
+         y="1003.079">+30</tspan></text>
77
+    <text
78
+       sodipodi:linespacing="125%"
79
+       id="Alt_below"
80
+       y="1052.1428"
81
+       x="38.328125"
82
+       style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:end;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none;font-family:Liberation Mono;-inkscape-font-specification:Liberation Mono"
83
+       xml:space="preserve"><tspan
84
+         y="1052.1428"
85
+         x="38.328125"
86
+         id="tspan2993"
87
+         sodipodi:role="line">-30</tspan></text>
88
+    <path
89
+       style="color:#000000;fill:#e8e8d8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
90
+       d="m 41.3125,16.09375 -5.9375,10.25 4,0 0,21.5 3.84375,0 0,-21.5 4,0 -5.90625,-10.25 z"
91
+       transform="translate(0,988.36218)"
92
+       id="Arrow_climb"
93
+       inkscape:connector-curvature="0" />
94
+    <path
95
+       inkscape:connector-curvature="0"
96
+       id="Arrow_descent"
97
+       d="m 41.3125,1036.2059 -5.9375,-10.25 4,0 0,-21.5 3.84375,0 0,21.5 4,0 -5.90625,10.25 z"
98
+       style="color:#000000;fill:#e8e8d8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
99
+    <rect
100
+       transform="matrix(0.70710713,-0.70710643,0.70710713,0.70710643,0,0)"
101
+       style="color:#000000;fill:none;stroke:#00ffff;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
102
+       id="Thread_Level_0"
103
+       width="20.5061"
104
+       height="20.5061"
105
+       x="-720.4447"
106
+       y="722.56604" />
107
+    <rect
108
+       y="722.56604"
109
+       x="-720.4447"
110
+       height="20.5061"
111
+       width="20.5061"
112
+       id="Thread_Level_1"
113
+       style="color:#000000;fill:#00ffff;stroke:#00ffff;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
114
+       transform="matrix(0.70710713,-0.70710643,0.70710713,0.70710643,0,0)" />
115
+    <path
116
+       sodipodi:type="arc"
117
+       style="color:#000000;fill:#ff7f2a;fill-opacity:1;fill-rule:nonzero;stroke:#ff7f2a;stroke-width:2.32758594;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
118
+       id="Thread_Level_2"
119
+       sodipodi:cx="15.714286"
120
+       sodipodi:cy="32.125"
121
+       sodipodi:rx="11.25"
122
+       sodipodi:ry="11.25"
123
+       d="m 26.964286,32.125 c 0,6.213203 -5.036797,11.25 -11.25,11.25 -6.2132036,0 -11.2500001,-5.036797 -11.2500001,-11.25 0,-6.213203 5.0367965,-11.25 11.2500001,-11.25 6.213203,0 11.25,5.036797 11.25,11.25 z"
124
+       transform="matrix(1.2888897,0,0,1.2888884,-4.2539588,978.95663)" />
125
+    <rect
126
+       style="color:#000000;fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
127
+       id="Thread_Level_3"
128
+       width="32"
129
+       height="32"
130
+       x="-1.6947017e-07"
131
+       y="1004.3622" />
132
+  </g>
133
+</svg>
+2
zkv1000.nas
... ...
@@ -9,6 +9,7 @@ files_to_load = [
9 9
     'maps/route.nas',
10 10
     'maps/navaids.nas',
11 11
     'maps/tiles.nas',
12
+    'maps/tcas.nas',
12 13
     'map.nas',     # moves the maps
13 14
     'display.nas',
14 15
     'menu.nas',    # manage windows
... ...
@@ -34,6 +35,7 @@ var data = { # set of data common to all devices
34 35
     lat : 0,
35 36
     lon : 0,
36 37
     aoa : 0,
38
+    tcas: [],
37 39
     timers : {
38 40
         '20Hz': maketimer (
39 41
             0.05,