Showing 5 changed files with 177 additions and 3 deletions
+83
Nasal/annunciations.nas
... ...
@@ -0,0 +1,83 @@
1
+var annunciationsClass = {
2
+    new: func {
3
+        var m = { parents : [ annunciationsClass ] };
4
+        m.registered = {}; # triggers identified by message
5
+        m.active  = [];    # currently active warnings and alerts, sorted by emergency level
6
+        m.devices = [];    # PFD displays
7
+
8
+        foreach (var d; keys(flightdeck))
9
+            if (flightdeck[d].role == 'PFD')
10
+                append(m.devices, d);
11
+
12
+        foreach (var warnings; alerts.getChildren('warnings'))
13
+            foreach (var warning; warnings.getChildren('warning'))
14
+                m.register(warning);
15
+
16
+        data.timers.annunciations = maketimer(1.0, func call(m.activate, [], m));
17
+        data.timers.annunciations.start();
18
+
19
+        return m;
20
+    },
21
+
22
+    register: func (node) {
23
+        me.registered[node.getValue('message')] = {
24
+            trigger: compile(node.getValue('script')),
25
+            node: node,
26
+        };
27
+    },
28
+
29
+    del: func (message) {
30
+        if (contains(me.registered, message))
31
+            delete(me.registered, message);
32
+        if (contains(me.active, message))
33
+            delete(me.active, message);
34
+    },
35
+
36
+    activate: func {
37
+        size(me.registered) or return;
38
+
39
+        var score = {};
40
+
41
+        foreach (var a; keys(me.registered))
42
+            if (me.registered[a].trigger())
43
+                score[me.registered[a].node.getValue('message')] = me.registered[a].node.getValue('level');
44
+
45
+        if (size(score) > 1) {
46
+            var sorted_scores = sort(keys(score), func (a, b) {
47
+                if (score[a] <= score[b])
48
+                    return 1; # greatest first
49
+                else
50
+                    return -1;
51
+            });
52
+        }
53
+        else
54
+            var sorted_scores = keys(score);
55
+
56
+        me.active = sorted_scores;
57
+
58
+        var levels_bg = [ 'lightgrey', 'white', 'red'    ];
59
+        var levels_fg = [ 'black'    , 'black', 'yellow' ];
60
+
61
+        if (size(me.active)) {
62
+            var level = score[sorted_scores[0]];
63
+            if (level > 2) level = 2;
64
+
65
+            foreach (var d; me.devices) {
66
+                flightdeck[d].display.screenElements['SoftKey11-bg']
67
+                    .setColorFill(flightdeck[d].display.colors[levels_bg[level]]);
68
+                flightdeck[d].display.screenElements['SoftKey11-text']
69
+                    .setColor(flightdeck[d].display.colors[levels_fg[level]]);
70
+                flightdeck[d].display.updateSoftKeys();
71
+            }
72
+        }
73
+        else {
74
+            foreach (var d; me.devices) {
75
+                flightdeck[d].display.screenElements['SoftKey11-bg']
76
+                    .setColorFill(flightdeck[d].display.colors.black);
77
+                flightdeck[d].display.screenElements['SoftKey11-text']
78
+                    .setColor(flightdeck[d].display.colors.lightgrey);
79
+                flightdeck[d].display.updateSoftKeys();
80
+            }
81
+        }
82
+    },
83
+};
+3
Nasal/core.nas
... ...
@@ -309,6 +309,9 @@ var powerOn = func {
309 309
     if (! contains(autopilot, 'parents'))
310 310
         autopilot = APClass.new();
311 311
 
312
+    if (! contains(annunciations, 'parents'))
313
+        annunciations = annunciationsClass.new();
314
+
312 315
     if (! contains(data.timers, 'listeners')) {
313 316
         data.timers.listeners = maketimer(1, setListeners);
314 317
         data.timers.listeners.singleShot = 1;
+34
Nasal/softkeys.nas
... ...
@@ -57,6 +57,40 @@ var softkeysClass = {
57 57
 
58 58
     bindings : {
59 59
         PFD : {
60
+            ALERTS: func {
61
+                id = 'ALERTS';
62
+                if (!contains(me.device.windows.state, id)) {
63
+                    var obj_infos = [
64
+                        { text: sprintf('%i ALERT%s', size(annunciations.active), size(annunciations.active) > 1 ? 'S' : ''), type: 'title' },
65
+                        { type: 'separator' }
66
+                    ];
67
+                    var firstEntry = 1;
68
+                    var levels = [ 'INFO', 'WARNING', 'ALERT' ];
69
+                    forindex (var order; annunciations.active) {
70
+                        var level = annunciations.registered[annunciations.active[order]].node.getValue('level');
71
+                        if (level > 2) level = 2;
72
+                        append(obj_infos, {
73
+                            text: sprintf('%02i - %-7s ', order + 1, levels[level]),
74
+                            type: firstEntry ? 'selected' : 'editable',
75
+                            scrollgroup: order,
76
+                        });
77
+                        append(obj_infos, {
78
+                            text: annunciations.registered[annunciations.active[order]].node.getValue('message'),
79
+                            type: 'normal|end-of-line',
80
+                            scrollgroup: order,
81
+                        });
82
+                        firstEntry = 0;
83
+                    }
84
+                    me.device.knobs.FmsInner = me.device.knobs.NavigateMenu;
85
+                    me.device.knobs.FmsOuter = me.device.knobs.NavigateMenu;
86
+                    me.device.windows.draw( id, obj_infos, {lines: 4, columns: 2, rows: 1} );
87
+                }
88
+                else {
89
+                    me.device.knobs.FmsInner = func;
90
+                    me.device.knobs.FmsOuter = func;
91
+                    me.device.buttons.ClearTMRREF();
92
+                }
93
+            },
60 94
             INSET: {
61 95
                 OFF: func {
62 96
                     me.device.map.setVisible(0);
+54 -2
README.md
... ...
@@ -58,13 +58,14 @@ Please report bug at <zkv1000@seb.lautre.net>.
58 58
   * Screens are fully animated, with softkeys and menus, on map (PFD inset and MFD) background is configurable with the preferred one, navaids and [route](https://seb.lautre.net/git/seb/zkv1000/issues/6) are displayed. Traffic and topography layers are available for display only on MFD screen (not in PFD inset as the window is too small).
59 59
   * There is also per-aircraft settings storage for preferred units (speed, distance, etc.).
60 60
   * Flightplan management is delegated to the more usable FG interface lauched from inside the zkv100, but it is possible to use DirectTo on selected navaid, and OBS mode with GPS.
61
-  * Checklists are managed, including separation between emergency and normal checklists (and aircraft agnostic, should be compatible with most systems).
61
+  * Checklists are managed, including separation between emergency and normal checklists (and aircraft agnostic, should be compatible with most systems). Moreover a alerting system allow to show specific alerts for your aircraft, just register a new alert check and PFDs will warn the pilot about.
62 62
   * ... and many more ! :)
63 63
 * ![][90%]
64 64
   * route displayed on map: legs ![][done], current and next leg ![][done], OBS ![][done], TOC/TOD ![][ongoing]
65 65
   * XPDR: emergency code depending of the country (eg.: 1200 for US, 7700 for Europe), should be set in settings
66 66
 * ![][80%]
67 67
   * Flight plans catalog (display on map doesn't work each time...)
68
+  * Alerts: voice description of the alert ![][ongoing]
68 69
 * ![][60%]
69 70
   * NOT TESTED: add the posssibility to only use Nasal part of the instrument. This in case of a cockpit which already includes needed objects and animations for screens, knobs and buttons in its config files
70 71
 * ![][50%]
... ...
@@ -79,7 +80,6 @@ Please report bug at <zkv1000@seb.lautre.net>.
79 80
   * make possible to integrate other autopilot systems than the one integrated
80 81
   * make booting animation visible
81 82
   * CDI/GPS: scale depending of the flight phase
82
-  * Alerts
83 83
   * Weather map
84 84
   * replace the use of `clip` by a better system in map display (think also about INSET)
85 85
   * VS guidance
... ...
@@ -206,6 +206,58 @@ You can specify a directory where to find the flightplans you save. Defaults to
206 206
 Only the flightplans set for a departure from your position will be shown, no matter of the name of the file (even extension), each will be parsed to find the ones corresponding to your departure location.
207 207
 The list will show only 6 flightplans, but it's scrollable so you can handle much more.
208 208
 
209
+### Alerting system
210
+You can set some alerts of your choice. Note that there are 3 levels:
211
+
212
+* `0`: INFO
213
+* `1`: WARNING
214
+* `2`: ALERT
215
+
216
+Alerts are identified by the displayed message, so you can't provide two alerts with the same alert message. The higher level is shown first in list displayed on PFD (press `ALERT` softkey), all levels should be integers at least if less than 2. The tests are executed each second.
217
+
218
+To come: a voice alert if needed and configured (but I'm facing issue of multiple alerts at the same time, temporisation is ongoing).
219
+
220
+To register your alerts you have two ways:
221
+#### set the alerts in `/instrumentation/zkv1000/alerts/warnings`
222
+This way they are registered at start
223
+
224
+        <warnings>
225
+          <warning>
226
+            <script><![CDATA[
227
+              getprop('/foo/bar/baz/value') and getprop('/foo/bar/baz/value[1]') > 5;
228
+            ]]></script>
229
+            <message>TEST 1</message>
230
+            <!-- <speaker/> TO USE THE MESSAGE AS VOICE -->
231
+            <level>0</level>
232
+          </warning>
233
+          <warning>
234
+            <script><![CDATA[
235
+              getprop('/foo/bar/baz/value[3]') == 'foobar' or getprop('/foo/bar/baz/value[2]') < 10;
236
+            ]]></script>
237
+            <message>TEST 2</message>
238
+            <level>1</level>
239
+            <!-- <speaker>this is only for testing</speaker> TO USE A SPECIFIC VOICE MESSAGE -->
240
+          </warning>
241
+          <warning>
242
+            <script><![CDATA[
243
+              return getprop('/foo/bar/baz/value[4]') == 5 or getprop('/foo/bar/baz/value[5]');
244
+            ]]></script>
245
+            <message>TEST 3</message>
246
+            <level>2</level>
247
+          </warning>
248
+        </warnings>
249
+
250
+#### or use the integrated API
251
+Actually not yet tested, but you can register new alerts somewhere in your Nasal code with the following:
252
+
253
+        zkv1000.annunciations.register(props.globals.new({
254
+            message: "a message",
255
+            level: 20,
256
+            script: "if (something) return 1; else return 0;"
257
+        }));
258
+
259
+It also exists a `zkv1000.annunciations.del(message)` to delete a warning from the register list, so it won't be tested anymore.
260
+
209 261
 ## 3D models
210 262
 In the definition of your flightdeck (here are the values for the installation in the Lancair 235 in which I develop the device)
211 263
 put it everywhere you want to. Note that the path `Aircraft/Instruments-3d/zkv1000` is dependant on the path where the zkv1000 is installed, this can be somewhere like `Aircraft/My-Nice-Aircraft/arbitrary/dirs/zkv1000-or-not`as mentionned earlier in this section.
+3 -1
zkv1000.nas
... ...
@@ -16,17 +16,19 @@ files_to_load = [
16 16
     'menu.nas',    # manage windows
17 17
     'core.nas',    # the scheduler and switch on button
18 18
     'afcs.nas',    # Automatic Flight Control System
19
+    'annunciations.nas',  # in flight tests
19 20
 ];
20 21
 #    'routes.nas',  # manages flightplans and routes
21 22
 #    'display.nas', # display messages and popups
22 23
 #    'infos.nas',   # get informations from environment
23
-#    'alerts.nas',  # in flight tests
24 24
 #    'mud.nas',     # displays simple embedded GUI (Multi-Use Display)
25 25
 
26 26
 var flightdeck = {};
27 27
 
28 28
 var autopilot = {};
29 29
 
30
+var annunciations = {};
31
+
30 32
 var units = {
31 33
     speed : {
32 34
         from_kt  : 1,