Canvas MCDU framework: Difference between revisions

From FlightGear wiki
Jump to navigation Jump to search
mNo edit summary
mNo edit summary
(One intermediate revision by one other user not shown)
Line 1: Line 1:
{{Template:Canvas Navigation}}
{{Template:Canvas Navigation}}
{{See also|Canvas EFIS Framework}}


I am currently experimenting with building a framework for implementing a generic framework usable for every type of textual display (also see [[A standard CDU framework for FlightGear]]). The following code drives the AMU for an C-130J as shown in the video:
I am currently experimenting with building a framework for implementing a generic framework usable for every type of textual display (also see [[A standard CDU framework for FlightGear]]). The following code drives the AMU for an C-130J as shown in the video:
Line 6: Line 9:
The framework should easy to use for different types of displays, so if you use it please provide feedback!
The framework should easy to use for different types of displays, so if you use it please provide feedback!


<syntaxhighlight lang="php">
<syntaxhighlight lang="nasal">
# ==============================================================================
# ==============================================================================
# Avionics management unit
# Avionics management unit

Revision as of 17:33, 10 February 2020



I am currently experimenting with building a framework for implementing a generic framework usable for every type of textual display (also see A standard CDU framework for FlightGear). The following code drives the AMU for an C-130J as shown in the video:

The framework should easy to use for different types of displays, so if you use it please provide feedback!

# ==============================================================================
# Avionics management unit
# ==============================================================================

var Screen = {
  canvas_settings: {
    "name": "AMU",
    "size": [512, 512],
    "view": [480, 320],
    "mipmapping": 1,
  },
  new: func(placement)
  {
    var m = {
      parents: [Screen],
      canvas: canvas.new(Screen.canvas_settings),
      text_style: {
        'font': "LiberationFonts/LiberationMono-Bold.ttf",
        'character-size': 34,
        'character-aspect-ratio': 1.044
      }
    };

    m.canvas.addPlacement(placement);
    m.canvas.setColorBackground(0, 0.05, 0);
    m.root = m.canvas.createGroup();

    m.title = m.root.createChild("text");
    m.title._node.setValues(m.text_style);
    m.title.setColor(0,1,0);
    m.title.setAlignment("center-center");

    # Left and right text rows
    m.l = {
      x: 8.25,
      align: "left-center",
      rows: setsize([], 4)
    };
    m.r = {
      x: 471.75,
      align: "right-center",
      rows: setsize([], 4)
    };

    for(var i = 0; i < 4; i += 1)
    {
      var y = 16 + (2 * i + 1) * 32;
      m.l.rows[i] = {
        y: y,
        texts: []
      };
      m.r.rows[i] = {
        y: y + 32,
        texts: []
      };
    }
    
    return m;
  },
  setRow: func(side, row_index, config)
  {
    # config can either be just the title as single string or an array with two
    # elements where the first element is the title and the second parameter is
    # passed to the callback function for hardware key presses

    var row_config = config[side ~ row_index];
    var label = row_config;
    var property = nil;

    if( typeof(row_config) == 'hash' )
    {
      label = row_config['label'];
      action = row_config['action'];

      if( action != nil )
      {
        var fgcmd = action['fgcmd'];
        if( fgcmd != nil )
        {
          var args = action['args'];
          if( typeof(args) == 'hash' )
            property = args['property'];  
          action = func { fgcommand(fgcmd, props.Node.new(args)) };
        }

        me.commands[side ~ row_index] = action;
      }
    }

    if( typeof(label) == 'scalar' )
      label = [label];

    var row = me[side].rows[row_index - 1];
    var num_texts_old = size(row.texts);
    var num_texts_new = label != nil ? size(label) : 0;

    # remove unneeded texts
    for(var i = num_texts_new; i < num_texts_old; i += 1)
      row.texts[i].del();
    setsize(row.texts, num_texts_new);

    # update/create new texts
    var offset = 0;
    var i_start = (side == 'l') ? 0 : num_texts_new - 1;
    var i_end = (side == 'l') ? num_texts_new : -1;
    var dir = (side == 'l') ? 1 : -1;
    for(var i = i_start; i != i_end; i += dir)
    {
      var el_text = row.texts[i];
      if( el_text == nil )
      {
        el_text = me.root.createChild("text");
        el_text.setAlignment(me[side].align);
        el_text.setColor(0, 1, 0);
        el_text.setColorFill(0, 1, 0);
        el_text._node.setValues(me.text_style);
        row.texts[i] = el_text;
      }
      
      el_text.setDrawMode(1);
      el_text.setTranslation(me[side].x + offset, row.y);
      el_text.setColor(0, 1, 0);
      
      var text = label[i];
      if( typeof(text) == 'vector' )
      {
        (func {
          var text = el_text;
          var prop = property;
          var fun = label[i][1];

          var listener = setlistener(prop, func(p) {
            fun(text, p.getValue())
          }, 1, 0);

          if( me.listener[side ~ row_index] == nil )
            me.listener[side ~ row_index] = [listener];
          else
            append(me.listener[side ~ row_index], listener);
        })();

        text = text[0];
      }
      offset += dir * size(text) * 30.75/1.59;
      el_text.setText(text);
      el_text.update();

#      offset += dir * size(text) * 30.75/1.59;
    }
  },
  setCallback: func(callback)
  {
    me.callback = callback;
  },
  setPage: func(config)
  {
    me.title.setText(config['title']);
    # move to full character position (move half character width on odd length)
    me.title.setTranslation(241 - math.mod(size(config['title']), 2) * 0.5 * 30.75/1.59, 16);
    me.title.update();
    
    me.commands = {};
    
    if( me['listener'] != nil )
    {
      foreach(var name; keys(me.listener))
      {
        foreach(var listener; me.listener[name])
          removelistener(listener);
      }
    }
    me.listener = {};

    for(var i = 1; i <= 4; i += 1)
    {
      me.setRow('l', i, config);
      me.setRow('r', i, config);
    }
  },
  onKeyPress: func(key)
  {
    if( typeof(me.callback) != 'func' )
      return;

    var type = substr(key, 0, 3);
    var name = substr(key, 4);

    if( type != "LSK" )
      return;

    var cmd = me.commands[name];
    if( typeof(cmd) == 'func' )
      cmd();
    else if( cmd != nil )
      me.callback(cmd);
  }
};

var AMU = {
  new: func()
  {
    debug.dump("Initializing AMU...");

    var m = {
      parents: [AMU],
      screen_left: Screen.new({"node": "AMU Screen.l"}),
      screen_right: Screen.new({"node": "AMU Screen.r"}),
    };
    
    m.screen_left.setCallback(func(cmd){ m.onCmd(cmd); });
    m.screen_right.setCallback(func(cmd){ m.onCmd(cmd); });
    
    var setStyle = func(text, active)
    {
      text.setPadding(5);
      text.setDrawMode(active ? 5 : 1);
      if( active )
        text.setColor(0, 0.05, 0);
      else
        text.setColor(0, 1, 0);
      text.setPadding(3);
      text.update();
    };

    var propertyCycle = func(prop, values, texts)
    {
      var row = {
        label: [],
        action: {
          fgcmd: 'property-cycle',
          args: {
            property: prop,
            value: values
          }
        }
      };

      foreach(var text; texts)
      {
        if( typeof(text) == 'vector' )
        {
          # capture value in closure and add function which
          # changes style upon comparison with the given value
          (func {
            var val = text[1];
            text[1] = func(t,v) setStyle(t, v == val);
          })();
        }

        append(row.label, text);
      }

      return row;
    };

    var page_hdd_pos = {
      'title': "HDD POS",
      'l1': "HDD 1",
      'l2': "HDD 2",
      'l3': "HDD 3",
      'l4': "HDD 4",
      'r1': "DEFAULTS>"
    };

    m.pages = {
      'main-menu':
      {
        'left':
        {
          'title': "MAIN MENU",
          'l1': {label: "<PFD", action: {'page': 'pfd'}},
          'l2': {label: "<ENGINE", action: {'page': 'engine'}},
          'l3': "<CAPS",
          'r1': {label: "NAV-RADAR>", action: {'page': 'nav-radar'}},
          'r2': {label: "SYS STATUS>", action: {'page': 'sys-status'}},
          'r3': "DIG MAP>",
          'r4': "TAWS>"
        },
        'right':
        {
          'title': "MAIN MENU",
          'l1': "<NAV SELECT",
          'l2': "<ACAWS",
          'l3': "<DIAGNOSTICS",
          'l4': "<PREFLIGHT",
          'r1': "DEFAULTS>",
          'r3': "LIGHTING>",
          'r4': "GCAS AND STALL>"
        },
      },
      'pfd':
      {
        'left':
        {
          'title': "PFD",
          'l1': propertyCycle('/instrumentation/pfd[0]/is_pilot', [0,1], [["PILOT",1]," / ",["COPILOT",0]]),
          'l2': propertyCycle('/instrumentation/pfd[0]/baro_unit', ['in','mb'],["BARO ",["IN",'in']," / ",["MB",'mb']]),
          'l3': propertyCycle('/instrumentation/pfd[0]/north', ['mag','true','grid'], [["MAG",'mag']," / ",["TRUE",'true']," / ",["GRID",'grid']]),
          'l4': propertyCycle('/instrumentation/pfd[0]/fd_source', [0,1], ["FD SOURCE ",["P", 1]," / ",["CP",0]]),
          'r1': propertyCycle('/instrumentation/pfd[0]/att_ref_imu', [1,2], ["ATT REF IMU ",["1",1]," / ",["2",2]]),
          'r2': propertyCycle('/instrumentation/pfd[0]/cadc_src', [1,2], ["CADC ",["1",1]," / ",["2",2]]),
          'r3': propertyCycle('/instrumentation/pfd[0]/rad_alt_src', [1,2], ["RAD ALT ", ["1", 1], " / ", ["2", 2]]),
          'r4': {label: "MAIN MENU>", action: {'page': 'main-menu'}}
        },
        'right':
        {
          'title': "HDD POS",
          'l1': "HDD 1",
          'l2': "HDD 2",
          'r1': "DEFAULTS>"
        }
      },
      'engine':
      {
        'left':
        {
          'title': "ENGINE",
          'l1': "<ENG DIAGNOSTICS",
          'l2': "<PROP SYNC",
          'l3': "EMS DATA DOWNLOAD",
          'l4': "EMS EVENT RECORD",
          'r3': "HDD POS>",
          'r4': {label: "MAIN MENU>", action: {'page': 'main-menu'}}
        },
        'right': page_hdd_pos
      },
      'sys-status':
      {
        'left':
        {
          'title': "SYS STATUS DISPLAY",
          'r3': "HDD POS>",
          'r4': {label: "MAIN MENU>", action: {'page': 'main-menu'}}
        },
        'right': page_hdd_pos
      },
      'nav-radar':
      {
        'left':
        {
          'title': "NAV-RADAR DISPLAY",
          'l1': propertyCycle('/instrumentation/nav[0]/full', [0,1], [["FULL",1]," / ",["PART",0]]),
          'l2': propertyCycle('/instrumentation/nav[0]/center', [0,1], [["CENTER",1]," / ",["OFFSET",0]]),
          'l3': propertyCycle('/instrumentation/nav[0]/north', ['mag','true','grid'], [["MAG",'mag']," / ",["TRUE",'true']," / ",["GRID",'grid']]),
          'l4': propertyCycle('/instrumentation/nav[0]/up', ['hdg','trk','n'], [["HDG",'hdg']," / ",["TK",'trk']," / ",["N",'n']]),
          'r1': "RANGE    2>",
          'r2': "OVERLAYS>",
          'r3': "HDD POS>",
          'r4': {label: "MAIN MENU>", action: {'page': 'main-menu'}}
        },
        'right': page_hdd_pos
      }
    };
    
    m.setPage('main-menu');
    
    var input = "/controls/instruments/AMU/input";
    setlistener(input, func(cmd) { m.onInput(cmd); });
    m.node_input = props.globals.getNode(input);

    return m;
  },
  setPage: func(id)
  {
    var page = me.pages[id];

    if( typeof(page) != 'hash' )
      return debug.dump("AMU: Unknown page: " ~ id);
      
    var left = page['left'];
    if( typeof(left) == 'hash' )
      me.screen_left.setPage(left);
      
    var right = page['right'];
    if( typeof(right) == 'hash' )
      me.screen_right.setPage(right);
  },
  onInput: func()
  {
    var input = me.node_input.getValue();
    me.node_input.setValue("");
    
    var prefix = substr(input, 0, 2);
    var cmd = substr(input, 2);
    
    if( prefix == "L-" )
      me.screen_left.onKeyPress(cmd);
    else if( prefix == "R-" )
      me.screen_right.onKeyPress(cmd);
  },
  onCmd: func(cmd)
  {
    if( typeof(cmd['page']) == 'scalar' )
      me.setPage(cmd['page']);
  }
};

var node_pfd = props.globals.getNode('/instrumentation/pfd[0]/', 1);
node_pfd.initNode('is_pilot', 1, 'BOOL');
node_pfd.initNode('baro_unit', 'mb');
node_pfd.initNode('north', 'true');
node_pfd.initNode('fd_source', 1, 'INT');
node_pfd.initNode('att_ref_imu', 1, 'INT');
node_pfd.initNode('cadc_src', 1, 'INT');
node_pfd.initNode('rad_alt_src', 1, 'INT');

var node_nav = props.globals.getNode('/instrumentation/nav[0]/', 1);
node_nav.initNode('full', 1, 'BOOL');
node_nav.initNode('center', 1, 'BOOL');
node_nav.initNode('north', 'true');
node_nav.initNode('up', 'trk');

setlistener("/nasal/canvas/loaded", func {
  var amu_pilot = AMU.new();
}, 1);