Howto:Coding a Boeing CDU
IMPORTANT: Some, and possibly most, of the features/ideas discussed here are likely to be affected, and possibly even deprecated, by the ongoing work on providing a property tree-based 2D drawing API accessible from Nasal using the new Canvas system available since FlightGear 2.80 (08/2012). Please see: Canvas MCDU Framework for further information
You are advised not to start working on anything directly related to this without first discussing/coordinating your ideas with other FlightGear contributors using the FlightGear developers mailing list or the Canvas subforum . Anything related to Canvas Core Development should be discussed first of all with TheTom and Zakalawe. Nasal-space frameworks are being maintained by Philosopher and Hooray currently. talk page. |
This article or section contains out-of-date information
Please help improve this article by updating it. There may be additional information on the talk page. |
Gijs has started an effort to bring a generic Boeing CDU to FlightGear. This page tries to describe the coding for this effort.
The Sources
The CDU is aimed to reside in $FGDATA/Aircraft/Instruments-3d/cdu/.
Graphics:
- boeing_Cdu.ac: This is a AC3D file, describing the 3D outline of the CDU model.
- boeing_brown.png: A texture bitmap.
- boeing_grey.png: A texture bitmap.
XML:
- boeing_cdu.xml: This is the main XML file, providing the direct interface between the 3D model and the code. This file also defines the keypress interactions and the positioning of the text to be displayed in the screen.
- display_matrix_text.xml: An OSGText "model", used as a baseline for the texts on the screen. Compare to README.osgtext.
Nasal: The Nasal part is composed of two sections, the framework (which a CDU content coder hopefully would not have to touch) ...
- cdu_page_framework.nas: This is the main file of the CDU framework. It defines the main classes used for the CDU.
- display_matrix.nas: This file contains the Nasal relevant for the dispaly matrix. This code might be useful for other complex screen-based instruments.
- helping_functions.nas: This file contains functions which I found useful for my project, but which are totally generic and could be used for other stuff as well.
- timer.nas: This file contains a rudimentary timer class and an example of how to incorporate this with a generic class for an instrument button, allowing the button to call different Nasal scripts, depending on how long it has been pressed.
- README: Well, a readme.
... and the actual content implementation
- b777_cdu.nas: This is the main Nasal script file to be edited by the user of the CDU Nasal framework.
- b777_cdu_page_XXXXXX.nas: The content of the individual pages of the CDU.
Loading a CDU into an Aircraft
Here is how the CDU is loaded into the Boeing 777-200:
In 777-22ER-set.xml the Nasal node is expanded as follows:
<instrumentation>
...
<cdu>
<serviceable type="bool">true</serviceable>
</cdu>
...
<instrumentation>
...
<nasal>
...
<cdu>
<nowiki><!-- GENERIC --></nowiki>
<nowiki><!--
Every CDU needs these files in order to make use of the CDU Framework.
It should not be necessary to alter these files in order to "just" make
some CDU pages.
Think of these files as "header files" or stuff that one uses to
"inherit" from.
--></nowiki>
<file>Aircraft/Instruments-3d/cdu/Nasal/framework/helping_functions.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/framework/display_matrix.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/framework/timer.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/framework/cdu_page_framework.nas</file>
<nowiki><!-- SPECIFIC --></nowiki>
<nowiki><!--
These files here provide the actual content of a CDU and a person
who wants to create CDU pages needs to change, add, and alter these files.
Think of these files as where the actuall instantiation happens.
--></nowiki>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_menu.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_index.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_ident.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_pos.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_approach.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_navData.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_navRadio.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_perf.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_thrustLim.nas</file>
<file>Aircraft/Instruments-3d/cdu/Nasal/b777/b777_cdu_page_takeoff.nas</file>
</cdu>
</nasal>
Note the difference in the framework files and the actual instance used for this aircraft. To code pages for the CDU, one would normally not have to mess with the framework files. Hcc23
The 3D model is loaded from within $FGDATA/Aircraft/777-200/Models/pedestal.xml and the Nasal node is expanded as follows to accomplish that:
<model>
<name>CDU1</name>
<path>Aircraft/Instruments-3d/cdu/boeing_cdu.xml</path>
<offsets>
<x-m>-0.423</x-m>
<y-m>-0.175</y-m>
<z-m>0.559</z-m>
<pitch-deg>-60</pitch-deg>
</offsets>
<overlay>
<texture>boeing_brown.png</texture>
<material>
<emission>
<red>0.1</red>
<green>0.1</green>
<blue>0.1</blue>
</emission>
</material>
</overlay>
</model>
<animation>
<type>scale</type>
<object-name>CDU1</object-name>
<x-offset>0.9</x-offset>
<y-offset>0.8</y-offset>
<z-offset>0.9</z-offset>
<nowiki><center>
<x-m>-0.423</x-m>
<y-m>-0.175</y-m>
<z-m>0.559</z-m>
</center></nowiki>
</animation>
Note that this code loads one CDU, the B777-200 has three of them, all loaded in the same way.
3D Graphics
The 3D model resides in the file $FGDATA/Models/Cockpit/Instruments-3d/boeing_cdu.ac. This is a AC3D file format, but it should be importable by Blender.
The model also specifies the model internal coordinate system. It is indicated in the pictures through the corresponding axis: X (red), Y (green), and Z (blue). The origin of this coordinate system seems to coincide with the geometric center of the back plane of the CDU, i.e. the YZ-plane is coplanar with the back of the CDU. The unit of the coordinate system is the SI unit Meter.
The 3D model has has a name assigned to each button. Via XML bindings these can be detected, so when a certain button is clicked, something happens. Currently not all buttons are functional and there is no real way of telling which will do something (other then pressing them and observing the reaction or reading the source code...)
The Screen of the CDU
With respect to the model coordinate frame, the screen is the rectangle given by:
Top edge: z= 0.111, x=0.004 Lower edge: z= 0.019, x=0.004 Left edge: y=-0.051, x=0.004 Right edge: y= 0.051, x=0.004
The CDU Screen
One of the major problems of the CDU is the representation of the actual screen. According to the Honeywell manual, the CDU utilized by Boeing has a fixed setup which could be captured by a matrix-like approach, i.e. a fixed number of rows and columns (each element holding one character) and an assigned format of that cell.
This setup is replicated within the Nasal Display Matrix Framework, which has been developed for this CDU.
Details of the (LCD) Screen
The LCD has 14 lines with a total of 24 characters per line. The page format is formatted as follows:
- Title Field: The top line of the display area. Id identifies the subject or title of the data displayed on the page in view. It also identifies page number and the number of pages in series, e.g., 1/2 identifies a page as the first in a series of two pages.
- Left Field: 6 pairs of lines, 11 characters per line. It extends from the left side of the screen to the center. The pilot has access to one line of each pair through a LSK (Line-Select Key) on the left side of the CDU. A line pair is made up of a label and a data line.
- Right Field: This field is similar to the left field, extending from the center of the screen to the right side. Pilot access is available by a LSK on the right side.
- Scratchpad: The bottom line of the display screen. This line displays alphanumeric data or messages. Data can be entered with the alphanumeric keys or the LSK, or by the FMC. Scratchpad entry cannot, normally, be made with a FMC message in the scratchpad. Scratchpad entries are independent of page selection, and remain until cleared even when page changes occur. Scratchpad data entries and erases affect only the associated CDU. Messages can appear and be erased on both CDUs simultaneously.
For more details on the implementation of the display matrix, read the page on the Nasal Display Matrix Framework.
The Code Framework
This is a rough overview on the internal framework of the CDU code. A more detailed descriptions can be found at the Nasal CDU Framework page.
The CDU framework is implemented following some standard object-oriented ideas, completely implemented in Nasal. All variables are instantiated using var so that the global namespace should not be tainted.
The idea for the framework stems from the fact that a CDU is based upon the concept of pages, some of which could occupy several screens. This, in conjunction with the idea of the fields (stipulated from the screen layout of the CDU) resulted in the following framework:
CDU | |-BasePage | | | |-SubPage | | | | | |-Field | | | |-Line | | | |-Line | | | | | |-Field | | | |-Line | | | |-Line | | | | | |-... | | | |-SubPage | | |-Field | | |-Field | | |-... | | | |-... | |-BasePage | | | |-SubPage | | | |-Field | |-Field | |-... | |-...
Well, you get the idea....
These four classes (BasePage, SubPage, Field, Line) are defined in cdu_framework.nas. The constructors are generally called .new(), all members are created within the constructors, all static parts are implemented directly on the hash level.
Beside these four classes the class ScreenText (defined in display_matrix.nas) is used to house the actual text to be printed.
All text is then kept internal to Nasal within the before-mentioned structure. The text is then also kept in the FlightGear property tree so that an interface to osgtext can be established via boeing_cdu.xml.
NOTE: There is a DEBUG variable set in the CDU framework. Setting this variable to non-zero enables a bunch of console printouts, implemented via DEBUG>debug_level?print('....'):; Furthermore, when calling a constructor, the parameter ptp (for path-to-property) can be set, enabling some member variables of the classes to register themselves in the property tree, allowing an easier debugging during runtime. Hcc23 15:20, 8 April 2011 (EDT)
Examples
Maybe looking at the actual code is the easiest way of understanding how the framework works. (I also tried to be fairly explicit in my in-code comments, so looking at the sources might actually be helpful, too. Hcc23 15:46, 8 April 2011 (EDT))
As the framework is separate from the instantiation, let's focus on that as that is the part a user (i.e. content coder) should care most about. The initially relevant file for the Boeing 777-200 is b777_cdu.nas
Preliminaries
Initially some settings can/should be changed in b777_cdu.nas. Most importantly these are the PROP and DEBUG variables.
# # The variables PROP and DEBUG have already been defined in the # cdu_page_framework but can be redifined here in order to # customize the desired behaviour. # var PROP = "/instrumentation/cdu"; # generic path to the properties of this CDU # DEBUG = 0; # NO messages # DEBUG = 1; # ERROR messages # DEBUG = 2; # ERROR, WARNING messages # DEBUG = 3; # ERROR, WARNING, NOTE messages var DEBUG = 2;
PROP determines where the CDU stores stuff in the property tree, DEBUG sets the level of verbosity of the console output. The default for DEBUG is 1, only printing errors. For content coding 2 might be more suitable. 3 and 4 really print a lot of stuff and should only be used when searching for bugs or other things that don't work as expected.
In order to be more generic, it might be helpful to precheck for the FDM an aircraft is using. (This is only done here in order to reduce the number of getprop calls to /sim/flight-model.)
# determine the flight dynamics model # options are: # "yasim" # "jsb" var FDM = getprop("/sim/flight-model");
Furthermore it might be helpful to start from a clean sheet:
# reset the display matrix in the property tree to "zero". resetDisplayMatrix();
Starting with Base- and SubPages
It all starts in b777_cdu.nas as well. Use this file to create the Base- and corresponding SubPages before the content is created. Though technically possible to create the pages not here (but in the corresponding page's file), the detached approach is helpful as it prevents some issued with the home-fields (more on that later).
############################# # BASE PAGES and SUB PAGES ############################# # # As some pages might need access to others pages "home" field, it is # necessary to create all the base pages here so that the home fields # are already in place when some pages want to use it. # var page_menu = BasePage.new("MENU",PROP~"/page_menu"); var page_menu_default = SubPage.new(page_menu,"MENU",page_menu.ptp~"/sub_default"); KeyBinding["MENU"] = page_menu.activate;
This section creates the menu base page of the CDU. The constructor for base pages is this one:
new: func(name,ptp=nil) {...};
What this essentially does is creating a container to house sub pages. The name element is used as the title of the page (but this could be overwritten in a sub page), the ptp, if set, registers the base page at that path. (However, if the DEBUG variable is set, the the pages are registered by their internal ID in a DEBUG path underneath the CDU.)
When a base page constructor is called, it also creates a home-field. This function can be used to make the corresponding base page the active page, i.e. display that page. As you can see above, the corresponding method of the MENU page is assigned to the related button of the CDU. KeyBinding is defined in cdu_framework.nas, for a list of the available buttons see boeing_cdu.xml.
As outlined in the framework, each base page houses sub pages, which in turn house the fields (which hold the actual content). So we need to start by making a sub page:
# create the subpages var page_menu_default = SubPage.new(page_menu,"MENU",page_menu.ptp~"/sub_default");
The constructor for a sub page looks like this:
new: func(parent_base_page,name=nil,ptp=nil) {...};
The elements are fairly straight forward:
- parent_base_page: parent node of this sub page (which is a base page). Note that this is not the name of that page, but the actual page, i.e. put down the name of the variable you made earlier. Here, this is a sub page of page_menu, i.e. the MENU page, which was made earlier.
- name: similar to the name of a base page, it is used as the title entry of the sub page. If a sub page has a name (which it doesn't need to have), the name of the sub page overwrites the name of the base page, such allowing for slightly different titles on each "screen" of a CDU "page". Note, however, that the display of the page count is taken care of, i.e. you do not have to put that into the title.
- ptp: same as with the base page. Also identical, the registration is changed to registration by ID when the DEBUG variable is non-zero.
NOTE: For the time beeing, the debug registering procedure is permanent. Hcc23 17:20, 9 May 2011 (EDT)
Filling a SubPage (and the corresponding Fields)
To continue the elaborate example for the menu page, here the code that makes this page:
####################### # DETAILS PAGE MENU ####################### # # TODO: # * pressing 1L only goes to the ident page on initialization of the FMC/CDU. # If the CDU has been in use before, it goes back to whatever page was displayed before # CDU.M_NOTE('Starting to make details for page '~'MENU'); # create some properties setprop(page_menu_default.ptp~"/data"~"/efis_ctl","ON"); setprop(page_menu_default.ptp~"/data"~"/efis_dsp_ctl","OFF"); # create the fields var field_fmc = Field.new("",ScreenText.new("<FMC <ACT>",ScreenText["WHITE"]),page_ident.activate); var toggle_efisCtl = toggle_OnOff_func(page_menu_default.ptp~"/data"~"/efis_ctl"); var field_efisCtl = Field.new(ScreenText.new("EFIS CTL",ScreenText["white"]),[ ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_ctl",toggle_OnOff_off]), ScreenText.new("<>",ScreenText["WHITE"]), ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_ctl",toggle_OnOff_on]), ScreenText.new(">",ScreenText["WHITE"]), ],toggle_efisCtl); var field_efis = Field.new("",ScreenText.new("EFIS>",ScreenText["WHITE"])); var toggle_efisDspCtl = toggle_OnOff_func(page_menu_default.ptp~"/data"~"/efis_dsp_ctl"); var field_efisDspCtl = Field.new(ScreenText.new("DSP CTL",ScreenText["white"]),[ ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_dsp_ctl",toggle_OnOff_off]), ScreenText.new("<>",ScreenText["WHITE"]), ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_dsp_ctl",toggle_OnOff_on]), ScreenText.new(">",ScreenText["WHITE"]), ],toggle_efisDspCtl); var field_maintInfo = Field.new(ScreenText.new("MAINT INFO",ScreenText["white"]),ScreenText.new("DISPLAY>",ScreenText["WHITE"])); var field_memory = Field.new("",ScreenText.new("MEMORY>",ScreenText["WHITE"])); # register the fields in the subpage page_menu_default.register_field("1L",field_fmc); page_menu_default.register_field("1R",field_efisCtl); page_menu_default.register_field("2R",field_efis); page_menu_default.register_field("3R",field_efisDspCtl); page_menu_default.register_field("5R",field_maintInfo); page_menu_default.register_field("6R",field_memory); #activate the sub page (and display it consequentially) #DEBUG?page_menu_default.activate():; # activate the MENU page as the initial page of the CDU page_menu.activate();
This code section starts to fill in the details of the page MENU. It starts by setting up some properties that will later on be used to hold data for a toggle field.
NOTE: Theoretically the CDU as such should not hold this data, as this is all FMS stuff - which the CDU only displays. But in lieu of knowing whether (or where) such a content would/should live, I created it within the CDU.Hcc23 17:20, 9 May 2011 (EDT)
After a sub page has been made, one needs to make fields to actually hold content:
# create the fields var field_fmc = Field.new("",ScreenText.new("<FMC <ACT>",ScreenText["WHITE"]),page_ident.activate);
This part creates a field, the field constructor is:
new: func(label,data,key_action=nil,ptp=nil) {...};
This constructor is a little more complex than the previous ones:
- label: the entry for the label line (the top line) of the field.
- data: the entry for data line (the lower line) of the field.
- key_action: as fields have an line select key associated with them, key_action, a function element, determines what happens when the LSK next to the data line is pressed.
- ptp: registering the field. As for base and sub pages, a non-zero DEBUG changes the ptp.
In order to make the construction of a field a little more convenient, some very limited type of constructor overloading is incorporated. Both lines could be initialized with either an element of type ScreenText.new() (defined in display_matrix.nas), i.e. a hash, or it can be initialized with a vector of ScreenText elements, i.e. with [ScreenText.new() ScreenText.new() ...], or it could be initialized with just a string, i.e. "ABCD". In the last case, the provided string will be used to internally create a ScreenText.new(input_string,ScreenText["white"]) from it.
Later in the code this field is then registered to the corresponding subpage via
# register the fields in the subpage page_menu_default.register_field("1L",field_fmc);
This part of the code puts this field into the top-left position in the sub page. Each sub page has two columns (L and R) of six elements (1-6) each. The columns are 1-indexed.
#activate the page (and display it consequentially) page_menu_default.activate();
This part activates this sup page. SubPages as well as BasePages can be activated. The later then displays the currently acitve subpage of that base page, the former directly shows this page.
Toggle Fields
The MENU page has some fields that simply show an entry which can be toggled, say between "on" and "off". Here is the code that did that:
var toggle_efisCtl = toggle_OnOff_func(page_menu_default.ptp~"/data"~"/efis_ctl"); var field_efisCtl = Field.new(ScreenText.new("EFIS CTL",ScreenText["white"]),[ ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_ctl",toggle_OnOff_off]), ScreenText.new("<>",ScreenText["WHITE"]), ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_ctl",toggle_OnOff_on]), ScreenText.new(">",ScreenText["WHITE"]), ],toggle_efisCtl);
Somewhere at the bottom of b777_cdu.nas there are some little helper functions that make this code easier to replicate (at least for the "ON/OFF" case).
So what happens is two-fold:
- a functionis made that is bound to the line select key of that field. toggle_OnOff_func creates toggle_efisCtl for that purpose and toggle_efisCtl is made to alter between "ON" and "OFF", depending on the value of page_menu_default.ptp~"/data"~"/efis_ctl".
- a field is made (field_efisCtl) that houses the data and it's formatting.
The field makes use of several things:
- the label line of the field is a simple ScreenText, always displaying EFIS CTL in small white characters.
- the data line of the field is represented using an vector of ScreenTexts, allowing for individual formatting of the different parts.
- the ScreenText used in the label makes use of an advanced formatting vector wich allows the content to be bound to the decision value page_menu_default.ptp~"/data"~"/efis_ctl".
What effectively happens is that ["%s",page_menu_default.ptp~"/data"~"/efis_ctl",toggle_OnOff_off] creates a ScreeText output that
- is dependetn on a property, page_menu_default.ptp~"/data"~"/efis_ctl" in this case
- the value of that property is then modified by the funciotn toggle_OnOff_off
- the output of that function is then formatted according to "%s", i.e. simply print.
Internally the following is happening:
ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_ctl",toggle_OnOff_off])
is equivalent to
ScreenText.new(sprintf("%s",toggle_OnOff_off(getprop(page_menu_default.ptp~"/data"~"/efis_ctl")));
Furthermore, the toggle function changes the style parameter of the ScreenText to alter between small font white characters (ScreenText["white"]) and large font green characters (ScreenText["GREEN"]).
More on this can be found on the Nasal Display Matrix Framework page.
The Property Tree
The CDU also has some properties in the general FlightGear Property Tree.
In general it is located in /instrumentation/cdu/
.
A Brief History of the Boeing CDU in FlightGear
Find some more information on the CDU at these pages: