Nasal CDU Framework: Difference between revisions

From FlightGear wiki
Jump to navigation Jump to search
(Switch to {{fgdata url}} to fix the broken Gitorious link.)
 
(9 intermediate revisions by one other user not shown)
Line 1: Line 1:
{{WIP|[[User:Hcc23|Hcc23]] is working on this. Find him in the FG IRC channel to discuss this page.}}
This page is a rather technical description of the Nasal code for the framework used to implement a Boeing style [[CDU]].
This page is a rather technical description of the Nasal code for the framework used to implement a Boeing style [[CDU]].


{{WIP|[[User:Hcc23|Hcc23]] is working on this. Find him in the FG IRC channel to discuss this page.}}
'''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 {{fgdata url|Aircraft/Instruments-3d/cdu}} should not present any major surprises. [[User:Hcc23|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 <tt>C++</tt>
 
* '''Static:''' Stuff in this section is mainly identified by being made directly as part of the <tt>hash</tt> (as opposed to being ''tagged on'' by the the <tt>new</tt> 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...


'''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. [[User:Hcc23|Hcc23]] 10:07, 7 May 2011 (EDT)
* '''Public:''' Elements in here are created from inside the <tt>new</tt> 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 <tt>new</tt> 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.


= Basic Classes =
'''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.) [[User:Hcc23|Hcc23]] 14:45, 9 May 2011 (EDT)


== Line ==
== Line ==
Line 84: Line 96:
     func me.activate(),
     func me.activate(),
     (Field) me.home
     (Field) me.home
 
 
     # "Private"
     # "Private"
     scalar me.parent,
     scalar me.parent,
Line 95: Line 107:


== BasePage ==
== 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.
<code>
  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,
   
  }; 
</code>


== CDU ==
== 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.
<code>
  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,
  };
</code>
= 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.
<code>
  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),
  };
</code>
'''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 <tt>DELETE</tt> 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 <tt>pm_trigger</tt>.
'''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.
<code>
  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,
  };
</code>
==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 <tt>vec</tt> is adds them at the front, '''altering the original vector'''. (Implemented in <tt>helping_functions.nas</tt>.)
== 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 <tt>helping_functions.nas</tt>.)
== 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 <tt>helping_functions.nas</tt>.)
== 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 <tt>helping_functions.nas</tt>.)
== 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 <tt>timer.nas</tt>.
== 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>
        <!-- UPDATE the name of the button object to match you case -->
  <object-name>Btn.clr</object-name>
  <action>
  <button>0</button>
  <repeatable>false</repeatable> 
  <binding>
  <command>nasal</command>
<nowiki><!-- UPDATE the namespace --></nowiki>
  <script><![CDATA[NAMESPACE.b_clr.pressed();]]></script>
  </binding>
  <mod-up>
  <binding>
  <command>nasal</command>
  <nowiki><!-- UPDATE the namespace --></nowiki>
  <script><![CDATA[NAMESPACE.b_clr.released();]]></script>
  </binding>
  </mod-up>
  </action>
  </animation>


= Supportive Infrastructure =
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.

Latest revision as of 10:42, 9 March 2016

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://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.