Updated Nordstern: Added a variometer.
[fg:toms-fgdata.git] / Aircraft / Nordstern / Systems / Nordstern.nas
1 ###############################################################################
2 ##
3 ## Zeppelin LZ 121 "Nordstern" airship for FlightGear.
4 ##
5 ##  Copyright (C) 2010 - 2011  Anders Gidenstam  (anders(at)gidenstam.org)
6 ##  This file is licensed under the GPL license v2 or later.
7 ##
8 ###############################################################################
9
10 ###############################################################################
11 # External API
12 #
13 # autoWeightOff()
14 # printWOW()
15 # shiftTrimBallast(direction, amount)
16 # releaseBallast(location, amount)
17 # refillQuickReleaseBallast(location)
18 # setForwardGasValves()
19 # setAftGasValves()
20 # about()
21 #
22
23 # Constants
24 var FORWARD = -1;
25 var AFT     = -2;
26 var FORE_BALLAST = -1;
27 var AFT_BALLAST = -2;
28 var QUICK_RELEASE_BAG_CAPACITY = 220.5; # lb
29 var TRIM_BAG_CAPACITY = 1102.3; # lb
30
31 ###############################################################################
32 var static_trim_p = "/fdm/jsbsim/fcs/static-trim-cmd-norm";
33 var weight_on_gear_p = "/fdm/jsbsim/forces/fbz-gear-lbs";
34
35 var trim_ballast_p =
36     [
37      "/fdm/jsbsim/inertia/pointmass-weight-lbs[4]",
38      "/fdm/jsbsim/inertia/pointmass-weight-lbs[5]",
39      "/fdm/jsbsim/inertia/pointmass-weight-lbs[6]",
40      "/fdm/jsbsim/inertia/pointmass-weight-lbs[7]",
41      "/fdm/jsbsim/inertia/pointmass-weight-lbs[8]",
42      "/fdm/jsbsim/inertia/pointmass-weight-lbs[9]",
43      "/fdm/jsbsim/inertia/pointmass-weight-lbs[10]",
44      "/fdm/jsbsim/inertia/pointmass-weight-lbs[11]"
45     ];
46 var fore_ballast_p =
47     [
48      "/fdm/jsbsim/inertia/pointmass-weight-lbs[12]",
49      "/fdm/jsbsim/inertia/pointmass-weight-lbs[13]",
50      "/fdm/jsbsim/inertia/pointmass-weight-lbs[14]",
51      "/fdm/jsbsim/inertia/pointmass-weight-lbs[15]"
52     ];
53 var aft_ballast_p =
54     [
55      "/fdm/jsbsim/inertia/pointmass-weight-lbs[0]",
56      "/fdm/jsbsim/inertia/pointmass-weight-lbs[1]",
57      "/fdm/jsbsim/inertia/pointmass-weight-lbs[2]",
58      "/fdm/jsbsim/inertia/pointmass-weight-lbs[3]"
59     ];
60
61 var printWOW = func {
62     gui.popupTip("Current weight on gear " ~
63                  -getprop(weight_on_gear_p) ~ " lbs.");
64 }
65
66 # Weight off to neutral
67 var autoWeighoff = func {
68     var lift = getprop("/fdm/jsbsim/static-condition/net-lift-lbs");
69     var n = size(trim_ballast_p);
70         
71     print("Nordstern: Auto weigh off from " ~ (-lift) ~
72           " lb heavy to neutral.");
73
74     foreach(var p; trim_ballast_p) {
75         var v = getprop(p) + lift/n;
76         interpolate(p,
77                     (v > 0 ? v : 0),
78                     0.5);
79         #n -= 1;
80         #lift -= (v > 0 ? v : 0);
81     }
82 }
83
84 var initial_weighoff = func {
85     # Set initial static condition.
86     # Finding the right static condition at initialization time is tricky.
87     autoWeighoff();
88     settimer(autoWeighoff, 0.25);
89     settimer(autoWeighoff, 1.0);
90     # Fill up the envelope if not at pressure already. A bit of a hack.
91 #    settimer(func {
92 #        setprop("/fdm/jsbsim/buoyant_forces/gas-cell/contents-mol",
93 #                2.0 *
94 #                getprop("/fdm/jsbsim/buoyant_forces/gas-cell/contents-mol"));
95 #    }, 0.8);
96 }
97
98 var mp_mast_carriers =
99     ["Aircraft/ZLT-NT/Models/GroundCrew/scania-mast-truck.xml"];
100
101 var init_all = func(reinit=0) {
102     setprop(static_trim_p, 0.5);
103     initial_weighoff();
104
105 #    fake_electrical();
106     # Disable the autopilot menu.
107     gui.menuEnable("autopilot", 0);
108
109     if (!reinit) {
110         # Hobbs counters.
111 #        aircraft.timer.new("/sim/time/hobbs/engine[0]", 73).start();
112 #        aircraft.timer.new("/sim/time/hobbs/engine[0]", 73).start();
113 #        aircraft.timer.new("/sim/time/hobbs/engine[0]", 73).start();
114         # Livery support.
115 #        aircraft.livery.init("Aircraft/ZLT-NT/Models/Liveries")
116     }
117
118     # Timed initialization.
119     settimer(func {
120         # Add some AI moorings.
121         mooring.add_ai_mooring(props.globals.getNode("/ai/models/carrier[0]"),
122                                160.0);
123         mooring.add_ai_mooring(props.globals.getNode("/ai/models/carrier[1]"),
124                                160.0);
125         mooring.add_ai_mooring(props.globals.getNode("/ai/models/carrier[2]"),
126                                160.0);
127         setlistener(props.globals.getNode("/ai/models/model-added", 1),
128                     func (path) {
129                         var node = props.globals.getNode(path.getValue());
130                         if (nil == node.getNode("sim/model/path")) return;
131                         var model = node.getNode("sim/model/path").getValue();
132                         foreach (var c; mp_mast_carriers) {
133                             if (model == c) {
134                                 settimer(func {
135                                   mooring.add_ai_mooring
136                                     (node,
137                                      "sim/model/mast-truck/mast-head-height-m");
138                                   print("Added: " ~ path.getValue());
139                                 }, 0.0);
140                                 return;
141                             }
142                         }
143                     });
144         setlistener(props.globals.getNode("/ai/models/model-removed"),
145                     func (path) {
146                         var node = props.globals.getNode(path.getValue());
147                         mooring.remove_ai_mooring(node);
148                         #print("Removed: " ~ path.getValue());
149                     });
150     }, 1.0);
151     print("Nordstern Systems ... Check");
152 }
153
154 setlistener("/sim/signals/fdm-initialized", func {
155     init_all();
156     setlistener("/sim/signals/reinit", func(reinit) {
157         if (!reinit.getValue()) {
158             init_all(reinit=1);
159         }
160     });
161 });
162
163 ###############################################################################
164 # Ballast controls
165
166 var shiftTrimBallast = func(direction, amount) {
167     var from = nil;
168     var to = nil;
169     if (direction == FORWARD) {
170         from = [trim_ballast_p[0], trim_ballast_p[1]];
171         to   = [trim_ballast_p[6], trim_ballast_p[7]];
172     } elsif (direction == AFT) {
173         from = [trim_ballast_p[6], trim_ballast_p[7]];
174         to   = [trim_ballast_p[0], trim_ballast_p[1]];
175
176     } else {
177         printlog("warn",
178                  "Nordstern.shiftTrimBallast(" ~ direction ~ ", " ~ amount ~
179                  "): Invalid direction.");
180         return;
181     }
182     foreach (var p; from) {
183         SmoothTransfer.new(p, to[0],
184                            2.0,
185                            0.25*amount);
186         SmoothTransfer.new(p, to[1],
187                            2.0,
188                            0.25*amount);
189     }
190 }
191
192 # Release one or more quick-release ballast units.
193 var releaseBallast = func(location, amount) {
194     var units = nil;
195     if (location == FORE_BALLAST) {
196         units = fore_ballast_p;
197     } elsif (location == AFT_BALLAST) {
198         units = aft_ballast_p;
199     } else {
200         printlog("warn",
201                  "Nordstern.releaseBallast(" ~ location ~ ", " ~ amount ~
202                  "): Invalid ballast location.");
203         return;
204     }
205     foreach (var p; units) {
206         if (!amount) return;
207         if (getprop(p) > 0.0) {
208             interpolate(p, 0.0, 1.0);
209             amount -= 1;
210         }
211     }
212 }
213
214 # Refills empty "ballasthosen" from the trim ballast bags.
215 var refillQuickReleaseBallast = func(location) {
216     var units = nil;
217     if (location == FORE_BALLAST) {
218         units = fore_ballast_p;
219     } elsif (location == AFT_BALLAST) {
220         units = aft_ballast_p;
221     } else {
222         printlog("warn",
223                  "Nordstern.refillQuickReleaseBallast(" ~ location ~
224                  ", " ~ amount ~
225                  "): Invalid ballast location.");
226         return;
227     }
228     var n = size(trim_ballast_p);
229     foreach (var qb; units) {
230         var v = getprop(qb);
231         if (v < QUICK_RELEASE_BAG_CAPACITY) {
232             foreach(var tb; trim_ballast_p) {
233                 SmoothTransfer.new(tb, qb,
234                                    2.0,
235                                    (QUICK_RELEASE_BAG_CAPACITY - v)/n);
236             }
237         }
238     }
239 }
240
241 ###############################################################################
242 # Gas valve controls
243 var gascell        = "/fdm/jsbsim/buoyant_forces/gas-cell";
244
245 var setForwardGasValves = func (v) {
246     setprop(gascell ~ "[12]/valve_open", v);
247     setprop(gascell ~ "[11]/valve_open", v);
248 }
249
250 var setAftGasValves = func (v) {
251     setprop(gascell ~ "[1]/valve_open", v);
252     setprop(gascell ~ "[0]/valve_open", v);
253 }
254
255 ###############################################################################
256 # Utility functions
257
258 # Set up aTransfer fluid between two properties without losing or creating any.
259 #  amount [lb]
260 #  rate   [lb/sec]
261 var SmoothTransfer = {
262     # Set up a smooth value transfer between two properties.
263     #  from, to       : property paths or property nodes
264     #  rate           : units/sec. MUST be positive.
265     #  amount         : the amount to transfer. MUST be positive.
266     #  stop_at_zero   : stop if the from property reaches 0.0.
267     new: func(from, to, rate, amount=-1, stop_at_zero=1) {
268         var m = { parents: [SmoothTransfer] };
269         m.from         = aircraft.makeNode(from);
270         m.to           = aircraft.makeNode(to);
271         m.left         = amount;
272         m.rate         = rate;
273         m.stop_at_zero = stop_at_zero;
274         m.last_time    = getprop("/sim/time/elapsed-sec");
275         settimer(func{ m._loop_(); }, 0.0);
276         return m;
277     },
278     stop: func {
279         me.left = 0.0;
280     },
281     _loop_: func() {
282         var t = getprop("/sim/time/elapsed-sec");
283         var a = me.rate * (t - me.last_time);
284         a = (a < me.left) ? a : me.left;
285         if (me.stop_at_zero) {
286             var f = me.from.getValue();
287             a = (a < f) ? a : f;
288         }
289         me.from.setValue(f - a);
290         me.to.setValue(me.to.getValue() + a);
291         me.left -= a;
292         me.last_time = t;
293         if (me.left > 0.0) {
294             settimer(func{ me._loop_(); }, 0.0);
295         }
296     }
297 };
298
299
300 ###############################################################################
301 # Debug display - stand in instrumentation.
302 var debug_display_view_handler = {
303     init : func {
304         if (contains(me, "left")) return;
305
306         me.left  = screen.display.new(20, 10);
307         me.right = screen.display.new(-250, 20);
308         # Static condition
309         me.left.add("/fdm/jsbsim/static-condition/net-lift-lbs");
310         me.left.add("/fdm/jsbsim/atmosphere/T-R");
311         me.left.add("/fdm/jsbsim/buoyant_forces/gas-cell[1]/temp-R",
312                     "/fdm/jsbsim/buoyant_forces/gas-cell[10]/temp-R");
313         # C.G.
314         me.left.add("/fdm/jsbsim/inertia/cg-x-in");
315         me.left.add(static_trim_p);
316         me.left.add("/fdm/jsbsim/mooring/total-distance-ft");
317         # Pitch moments
318         me.left.add("/fdm/jsbsim/moments/m-buoyancy-lbsft",
319                     "/fdm/jsbsim/moments/m-aero-lbsft",
320                     "/fdm/jsbsim/moments/m-total-lbsft");
321        
322         # Flight information
323         me.right.add("orientation/pitch-deg");
324         me.right.add("surface-positions/elevator-pos-norm");
325         me.right.add("surface-positions/rudder-pos-norm");
326         me.right.add("instrumentation/altimeter/indicated-altitude-ft");
327         me.right.add("instrumentation/altimeter/indicated-altitude-ft");
328         me.right.add("instrumentation/airspeed-indicator/indicated-speed-kt");
329         # Engines
330         me.right.add("/engines/engine[0]/rpm",
331                      "/engines/engine[1]/rpm",
332                      "/engines/engine[2]/rpm",
333                      "/engines/engine[3]/rpm");
334
335         me.shown = 1;
336         me.stop();
337     },
338     start  : func {
339         if (!me.shown) {
340             me.left.toggle();
341             me.right.toggle();
342         }
343         me.shown = 1;
344     },
345     stop   : func {
346         if (me.shown) {
347             me.left.toggle();
348             me.right.toggle();
349         }
350         me.shown = 0;
351     }
352 };
353
354 # Install the debug display for some views.
355 setlistener("/sim/signals/fdm-initialized", func {
356     view.manager.register("Watch Officer View", debug_display_view_handler);
357     view.manager.register("Rudder Man View", debug_display_view_handler);
358     view.manager.register("Elevator Man View", debug_display_view_handler);
359     print("Debug instrumentation ... check");
360 });
361
362 ###############################################################################
363 # fake part of the electrical system.
364 var fake_electrical = func {
365 #    setprop("systems/electrical/ac-volts", 24);
366 #    setprop("systems/electrical/volts", 24);
367
368 #    setprop("/systems/electrical/outputs/comm[0]", 24.0);
369 #    setprop("/systems/electrical/outputs/comm[1]", 24.0);
370 #    setprop("/systems/electrical/outputs/nav[0]", 24.0);
371 #    setprop("/systems/electrical/outputs/nav[1]", 24.0);
372 #    setprop("/systems/electrical/outputs/dme", 24.0);
373 #    setprop("/systems/electrical/outputs/adf", 24.0);
374 #    setprop("/systems/electrical/outputs/transponder", 24.0);
375 #    setprop("/systems/electrical/outputs/instrument-lights", 24.0);
376 #    setprop("/systems/electrical/outputs/gps", 24.0);
377 #    setprop("/systems/electrical/outputs/efis", 24.0);
378
379 #    setprop("/instrumentation/clock/flight-meter-hour",0);
380
381     var beacon_switch =
382         props.globals.initNode("controls/lighting/beacon", 1, "BOOL");
383     var beacon = aircraft.light.new("sim/model/lights/beacon",
384                                     [0.05, 1.2],
385                                     "/controls/lighting/beacon");
386     var strobe_switch =
387         props.globals.initNode("controls/lighting/strobe", 1, "BOOL");
388     var strobe = aircraft.light.new("sim/model/lights/strobe",
389                                     [0.05, 3],
390                                     "/controls/lighting/strobe");
391 }
392 ###############################################################################
393
394 ###########################################################################
395 ## MP integration of user's fixed local mooring locations.
396 ## NOTE: Should this be here or elsewhere?
397 settimer(func { mp_network_init(1); }, 0.1);
398
399 ###############################################################################
400 # About dialog.
401
402 var ABOUT_DLG = 0;
403
404 var dialog = {
405 #################################################################
406     init : func (x = nil, y = nil) {
407         me.x = x;
408         me.y = y;
409         me.bg = [0, 0, 0, 0.3];    # background color
410         me.fg = [[1.0, 1.0, 1.0, 1.0]]; 
411         #
412         # "private"
413         me.title = "About";
414         me.dialog = nil;
415         me.namenode = props.Node.new({"dialog-name" : me.title });
416     },
417 #################################################################
418     create : func {
419         if (me.dialog != nil)
420             me.close();
421
422         me.dialog = gui.Widget.new();
423         me.dialog.set("name", me.title);
424         if (me.x != nil)
425             me.dialog.set("x", me.x);
426         if (me.y != nil)
427             me.dialog.set("y", me.y);
428
429         me.dialog.set("layout", "vbox");
430         me.dialog.set("default-padding", 0);
431
432         var titlebar = me.dialog.addChild("group");
433         titlebar.set("layout", "hbox");
434         titlebar.addChild("empty").set("stretch", 1);
435         titlebar.addChild("text").set
436             ("label",
437              "About");
438         var w = titlebar.addChild("button");
439         w.set("pref-width", 16);
440         w.set("pref-height", 16);
441         w.set("legend", "");
442         w.set("default", 0);
443         w.set("key", "esc");
444         w.setBinding("nasal", "ZLTNT.dialog.destroy(); ");
445         w.setBinding("dialog-close");
446         me.dialog.addChild("hrule");
447
448         var content = me.dialog.addChild("group");
449         content.set("layout", "vbox");
450         content.set("halign", "center");
451         content.set("default-padding", 5);
452         props.globals.initNode("sim/about/text",
453              "Zeppelin LZ 121 \"Nordstern\" airship for FlightGear\n" ~
454              "Copyright (C) 2010 - 2011  Anders Gidenstam\n\n" ~
455              "FlightGear flight simulator\n" ~
456              "Copyright (C) 1996 - 2011  http://www.flightgear.org\n\n" ~
457              "This is free software, and you are welcome to\n" ~
458              "redistribute it under certain conditions.\n" ~
459              "See the GNU GENERAL PUBLIC LICENSE Version 2 for the details.",
460              "STRING");
461         var text = content.addChild("textbox");
462         text.set("halign", "fill");
463         #text.set("slider", 20);
464         text.set("pref-width", 400);
465         text.set("pref-height", 300);
466         text.set("editable", 0);
467         text.set("property", "sim/about/text");
468
469         #me.dialog.addChild("hrule");
470
471         fgcommand("dialog-new", me.dialog.prop());
472         fgcommand("dialog-show", me.namenode);
473     },
474 #################################################################
475     close : func {
476         fgcommand("dialog-close", me.namenode);
477     },
478 #################################################################
479     destroy : func {
480         ABOUT_DLG = 0;
481         me.close();
482         delete(gui.dialog, "\"" ~ me.title ~ "\"");
483     },
484 #################################################################
485     show : func {
486         if (!ABOUT_DLG) {
487             ABOUT_DLG = 1;
488             me.init(400, getprop("/sim/startup/ysize") - 500);
489             me.create();
490         }
491     }
492 };
493 ###############################################################################
494
495 # Popup the about dialog.
496 var about = func {
497     dialog.show();
498 }