Nasal CDU Framework
Work in progress This article or section will be worked on in the upcoming hours or days. Note: Hcc23 is working on this. Find him in the FG IRC channel to discuss this page. See history for the latest developments. |
This page is a rather technical description of the Nasal code for the framework used to implement a Boeing style CDU.
Note: Although this is meant as a documentation for the code, it obviously will (always) be (slightly) outdated. However, after reading through this page, the actual source code at https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Aircraft/Instruments-3d/cdu should not present any major surprises. Hcc23 10:07, 7 May 2011 (EDT)
Basic Classes
These are the core classes in the CDU framework. The documentation should show a brief comment on what each variable or function is intended to do, particularly the ones that are important for the creation of an actual, content-filled CDU page.
Although Nasal as such has no support for this, the internal stuff in a class is sectioned in three categories, borrowing language from C++
- Static: Stuff in this section is mainly identified by being made directly as part of the hash (as opposed to being tagged on by the the new constructor. Elements in here also follow a camelCase type of notation, whereas members use an underscore_naming_convetion. Please note that although meant to be comparable to statics, in hindsight a lot of them would be better placed in the member section Public...
- Public: Elements in here are created from inside the new constructor and follow an underscore_naming_convetion. In a perfect world these would only be accessing functions, as exposing variables is always prone for trouble when the time for a redesign comes... Elements in here should be the only ones used in the creation of a page - in a perfect world.
- Private: Elements in here are created from inside the new constructor and follow an underscore_naming_convetion. Although readable by everybody, access to these elements should be limited to functions in the static and public parts of the classes code. Particularly no page creation (user) code should need to access this information.
Note: I know that I violate these rules. Yes. But I have rewritten the framework to often to (at this point) care much about it - particularly as it seems to be working. Please, feel free to correct the Nasal code I have written (which might involve the pain of revisiting the page creation sections... Sorry for that.) Hcc23 14:45, 9 May 2011 (EDT)
Line
The line class represents data to be shown on a row of the CDU's display matrix. This does not mean that a line has to span a complete row of the CDU's display matrix.
var Line = {
# "Static"
vector byID,
func registerInPropTree(path),
func formatOutput(input_data),
func getScreenTextVector(),
func new(line_data,ptp),
# "Public"
scalar me.id,
scalar me.ptp,
vector me.used_properties,
vector me.line_data,
scalar me.line_string_length,
func me.enable(),
func me.disable(),
# "Private"
scalar me.active,
};
Field
A field holds two lines, the label and the data line, as well as information about what the associated line select key does.
var Field = {
# "Static"
vector byID,
func registerInPropTree(path=nil),
func new(label,data,key_action=nil,ptp=nil),
# "Public"
scalar me.id,
scalar me.ptp,
func me.get_label_line(),
func me.get_label_line(),
func me.used_properties(),
func me.lsk_binding(),
func me.enable_lsk(),
func me.disable_lsk(),
# "Private"
(Line) me.label_line,
(Line) me.data_line,
scalar me.lsk_active,
};
SubPage
A SubPage is actually the main element in the CDU framework. It holds the content via its fields and manages the updating of the CDU screen.
var SubPage = {
# "Static"
vector byID,
(Field) blankField,
func registerInPropTree(path=nil),
func updateField(field, side, lsk_index),
func displayFields(field_vector, side),
func displayPage(),
func new(parent_base_page,name=nil,ptp=nil),
# "Public"
scalar me.id,
scalar me.separator,
scalar me.ptp,
func me.register_field(field_pos,field),
func me.activate(),
(Field) me.home
# "Private"
scalar me.parent,
scalar me.title,
scalar me.status,
vector me.left_field,
vector me.right_field,
};
BasePage
A BasePage is exactly that: a base to hold sub pages. A base page most closely represents the concept of a page in a CDU.
var BasePage = {
# "Static"
vector byID,
func displayPage(),
func regiserInPropTree(path)
func new(name,ptp=nil),
# "Public"
scalar me.id,
scalar me.active_sub_page_id,
func me.activate(),
(Field) me.home,
scalar me.ptp,
# "Private"
vector me.sub_pages,
scalar me.title,
scalar me.status,
};
CDU
The CDU is the main class for the instrument. Each simulated CDU should have one and only one CDU instance associated to it. This is intended to support multiple CDUs showing different content in the future.
var CDU = {
# "Static"
func M_ERROR(message_string),
func M_WARNING(message_string),
func M_NOTE(message_string),
func M_HINT(message_string),
func updateDisplay(),
scalar activeBasePageID,
(ScratchPad) scratchPad,
(Timer) timer,
(ListenerCollection) listOfListeners,
};
Additional Infrastructure
The previous section was more concerned with the conceptually necessary pieces for a/the core CDU. This section introduces the other elements the framework introduces in order to code a functional CDU.
Besides from the additional pieces presented here, there also is a page on the Nasal Display Matrix Framework which, although developed for the CDU, might be useful for any kind of fixed row-column based text-type screen in a cockpit. (As this code is conceptually separate from the CDU, its documentation is in a separate wiki page.)
ScratchPad
The ScratchPad is the input/output line in the CDU(s). The scratch pas as such is kind of different from the rest of the CDU(s) as the scratch pad messages are truly static, i.e. they happen to be displayed on all CDUs in the cockpit, no matter what the individual CDU is otherwise showing.
var ScratchPad = {
# "Static"
vector messages,
scalar deleteString, # "DELETE"~""
func displayScratchPad(),
func new(),
# "Public"
scalar me.input_string,
# "Private"
scalar me.pm_trigger,
func me.plusminus(),
func me.clear(pressedTime),
};
messages This vector of strings is currently not used, but it can/should hold the messages the CDU (or other systems) want to display via the scrath pad of the CDU. The messages are printed in in FIFO fashion on the scratch pad, pressing the CLR key erases the currently displayed message (and should trigger the display of the next message if one is still in the vector). This functionality has not yet been tested and is just partially integrated.
deleteString This string, defaulting to DELETE is entered into the scratch pad when it is empty and the DEL key is pressed. Having this in the scratch This behavior has not yet been tested and is just partially integrated. This functionality has been tested and is working in fields that implemented the logic.
displayScratchPad() This function goes along the CDU.updateDisplay() function, however, it does not refresh the whole display but just the scratch pad line. Whenever the ScratchPad.inputString is altered and the current state should be displayed before another update to the display is called, calling this function will do that.
new(...) The constructor of the scratch pad. Should theoretically not be necessary to use this outside the framework, particularly a use whilst creating pages (i.e. content) should not be necessary.
inputString The string holding the current content of the scratch pad.
plusminus() and pm_trigger A function beeing used when the "+/-" key is pressed. The function keeps track of the current state via pm_trigger.
clear() This function is bound to the CLR key and erases the last character on the scratchpad (if CLR is presesd less than 1 s) or the whole scratch pad (if CLR is pressed longer than 1 s).
ListenerCollection
In order to update fields that are tied to a (potentially changing) property, fields that need a (potentially changing) property to compute their data can (and automatically will) register a property listener on the relevant properties whenever the relevant field is displayed. In order to unregister the listener for pages that are no longer displayed, the class CDU keeps a collection of the currently active listeners, such allowing the unregisterling for unneeded listeners.
var ListenerCollection = {
# "Static"
func registerInPropTree(path),
func add(prop, field_id, subpage_id, side, lsk_index),
func clearAll(),
func new(ptp=nil),
# "Private"
scalar me.ptp,
vector me.listener_list,
vector me.property_list,
};
keyPressEvent
In order to allow for a change in key effects, the CDU's 3D model's XML binds all key presses (and releases where appropriate) to a Nasal call to this function, giving the objects name (i.e. the keys name) as an argument. This name is then checked against the keys present in the KeyBinding hash (see below) and, if a match is found, that function is called.
This function also keeps track of the current status of the plusminus, as this key either introduces a new char into the ScratchPad or toggles the last one entered.
KeyBinding
This hash represents the current key binding. The CDU's Nasal code then overwrites these functions, effectively changing the effect of a key press. Note that the KeyBinding does not use the Button class's timing systems, but directly uses the a CDU.timer instance of the Timer class.
Helping Functions and Classes
Although written for the CDU framework, these functions are fairly general and might be useful for other code projects. Please feel free to reuse them.
prepend
var prepend = func(vec, elements...)
This function is the counterpart to the Nasal provided append. Instead of adding stuff to the end of the vector vec is adds them at the front, altering the original vector. (Implemented in helping_functions.nas.)
stack
var stack = func(elements...)
Instead of appending or prepending, this function simply stacks its argument vectors and returns a new vector, leaving the original function arguments unaltered. (Implemented in helping_functions.nas.)
latitude_to_string
var latitude_to_string = func(lat_deg)
Take a Nasal scalar as input variable and return something like N23°14.8 . Actually, as the "°" seems to have some trouble in Helvetica, is returns something like N23*14.8. (Implemented in helping_functions.nas.)
longitude_to_string
var longitude_to_string = func(lon_deg)
Take a Nasal scalar as input variable and return something like E123°14.8 . Actually, as the "°" seems to have some trouble in Helvetica, is returns something like E123*14.8. (Implemented in helping_functions.nas.)
Timer
A timer can be made as an instance of this class. Each timer provides the functions tic and toc, the later returning the delta time passed.
var timer1 = Timer.new(); # uses /sim/time/elapsed-sec as a time source var timer2 = Timer.new(non_default_time_source_variable); # provide an alternate source of time timer1.tic(); # start the internal timer on the default time source var passed_time = timer1.toc(); # stop and reset the timer, get back the passed time in seconds.
This class is implemented in timer.nas.
Button
As an example for the use of the before mentioned timer, this is a generic button class to be used for all kinds of GUI interfaces. Although not used in the CDU, it still might be useful for other projects that require buttons to do several different things - depending on how long it has been pushed.
var b_clr = Button.new("CLEAR"); # make a new button and give it a name, stored in Button.name b_clr.addButtonAction(func() {print("NO delay");} ); # zero delay is the default. This function will be executed if no other criteria are met b_clr.addButtonAction(func() {print("2s delay");},2 ); # this function will be executed if the button has been pressed longer than 2 seconds when released. b_clr.addButtonAction(func() {print("1s delay");},1 ); # this function will be executed if the button has been pressed longer than 1 seconds when released.
The corresponding XML code for your model.xml could look like this:
<animation> <type>pick</type> <object-name>Btn.clr</object-name> <action> <button>0</button> <repeatable>false</repeatable> <binding> <command>nasal</command> <!-- UPDATE the namespace --> <script><![CDATA[NAMESPACE.b_clr.pressed();]]></script> </binding> <mod-up> <binding> <command>nasal</command> <!-- UPDATE the namespace --> <script><![CDATA[NAMESPACE.b_clr.released();]]></script> </binding> </mod-up> </action> </animation>
The sequence in which functions are added does not matter, note how the example created the 2 s before the 1 s functions. As a result of this, the button has to be released to work, i.e. currently it is not possible to make the button do something if the button has been pressed longer than X s and still is pressed.