Nasal CDU Framework

From FlightGear wiki
Jump to navigation Jump to search
WIP.png 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://gitorious.org/fg/fgdata/trees/master/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 CDU. This section introduces the other elements the framework introduces in order to code a functional CDU.


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,
   func 	me.erase,
   
   # "Private"
   scalar 	me.pm_trigger,
   func 	me.plusminus(),
   func 	me.clear(pressedTime),
   func 	me.erase(),
 };


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.