Skip to content

Converting CH Products Pedals to USB

This was a nice quick project.

IMG_20150119_232701

I had these CH Products Pro Pedals lying around for ages, and they were useless because they were gameport not USB and I haven’t had a computer with a gameport since about 2008. I found them again recently while doing something else, so in a spirit of procrastination I opened up the case and threw in a Teensy 2.0.

I don’t know how gameport wiring works. The pedals have three 100K potentiometers, for yaw and the two toe-brakes. They were wired up as rheostats, or variable resistors: two wires per pot, connecting to one end of the track and to the wiper. I don’t know how to interface with potentiometers wired up this way, so I added a third wire to each pot, connecting the other end of each track, so they’re now acting as potential dividers rather than as rheostats.

If you’ve done the PJRC Teensy tutorials, you’ll know how to wire up a potentiometer this way to a Teensy board. One end of each track connects to AREF. The other end connects to ground. The wipers each connect to a separate analogue input pin – remembering they’re numbered differently to the IO pins. I’ve read somewhere that pots in the range 1K-100K are suitable to use with Teensy boards as potential dividers. Lower resistance means higher current flow and therefore power consumption; higher resistance means more signal noise.

I used a Teensy 2.0, which gives 8-bit conversion, meaning there are 1024 steps between full-left and full-right yaw. If I’d used a Teensy 3 I’d have got something like 12-bit conversion, or approx 8000 steps between full-left and full-right. I’m not sure this would make much of a difference though; the tiny size of the Teensy 2.0 made it easier to fit inside the case.

I think the yaw pot is worn – when centred, the Teensy reads the yaw pot as being at 600/1024 when it ought to be 512/1024. I discovered this using serial-output while testing the Arduino sketch. The code will interpolate the yaw input so that 600 is neutral. Ideally I’d replace the yaw pot with a Hall sensor.

Reassembling the pedals was a bit of a puzzle! Sorry for the lack of photos…

This is the code running on the Teensy inside the pedal’s case:

// joystick axis mapping for CH Pedals with Teensy 2 Plain on board.

const int Led = 11;

const int LeftBrakePin = 0;
const int RightBrakePin = 1;
const int YawPin = 2;
const float YawCentre = 600;

void setup() {
  Serial.begin(9600);
  Joystick.X(1023);
  Joystick.Y(1023);
  Joystick.Z(YawCentre);  
}

void loop() {
  Joystick.X(analogRead(LeftBrakePin));
  Joystick.Y(analogRead(RightBrakePin));
  float yaw = analogRead(YawPin);
  if (yaw <= YawCentre) {
    // (x-x1)/(y-y1) = (x2-x1)/(y2-y1)
    // y = y1 + (x-x1)(y2-y1)/(x2-x1)
    Joystick.Z((yaw*512)/YawCentre);
  }
  else {
    Joystick.Z(512 + (yaw-YawCentre)*512/(1023-YawCentre));
  }
  Serial.print("X: ");
  Serial.print(analogRead(LeftBrakePin));
  Serial.print("\tY: ");
  Serial.print(analogRead(RightBrakePin));
  Serial.print("\tZ: ");
  Serial.println(yaw);
}

AOA indicator example

In response to some guy from the .org forums, who wants to build an AOA indicator. It’s dead easy. I’d always recommend going through PJRC’s Teensyduino tutorial before doing anything with X-Plane, as it is fun and teaches you most of what you need to know to build hardware for X-Plane.

1: Plug in your Teensy, launch the Arduino IDE, and load the ‘blink’ example sketch – as described in the tutorial. It’s always good to start by doing this; it confirms the board is working and that you can upload code to it, and gives you a blank slate.

2: Wire up some LEDs to the Teensy. Pick three arbitrary I/O pins – these are the ones which have a number on the pinout diagram (e.g. not “GND”, “+5V”, “AREF” etc). Wire them to ground using a resistor – again as described in the tutorial.

IMG_20150112_135353

I’m using a Teensy 2.0++ and arbitrarily picked pins 13, 15, and 17 for orange, green and red.

3: In the Teensy IDE, in the Tools menu, set USB Type to “Flight Sim Controls”. (This is necessary to communicate with X-Plane later on.) You’ll already have the right board type selected, as otherwise you wouldn’t be able to run the Blink example.

4: First we’ll make a sketch which just turns the LEDs on and off, to prove we can and that the physical stuff is wired up correctly.

/*
AOA Indexer example
*/

// Indexer LED pins: 
const int Amber = 13;
const int Green = 15;
const int Red = 17;

void setup() {
  pinMode(Amber, OUTPUT);
  pinMode(Green, OUTPUT);
  pinMode(Red, OUTPUT);
}

void loop() {
  // just test the LEDs light up OK.
  digitalWrite(Red, LOW);
  digitalWrite(Amber, HIGH);
  delay(1000);
  digitalWrite(Amber, LOW);
  digitalWrite(Green, HIGH);
  delay(1000);
  digitalWrite(Green, LOW);
  digitalWrite(Red, HIGH);
  delay(1000);
}

You should see merry little blinkenlights.

5: Let’s look around the datarefs for angle-of-attack information. The easiest way is to search the SDK dataref page, which turns up sim/flightmodel2/misc/AoA_angle_degrees and not much else. This is unfortunate – we’ll have to simulate the behaviour of the indexer on the Teensy. Ideally, X-Plane and its plugins determine exactly when the lights should be on or off (including other factors, such as electrical power, failures, is the system turned on etc) and all the Teensy needs to do is make the LEDs go on and off to match. But in this case, with only the AoA angle to go on, we’ll simulate the indexer in code on the Teensy.

6: This page about AoA indexers suggests the indicator should show green between 11 and 15 degrees, with amber for less than 11 and red for greater than 15. So now we’ll extend the code to do the same:

/*
AOA Indexer example

&quot;The angle of attack indexer is used during final approach to
landing. In order to land the aircraft correctly, your angle of
attack should be between 11 and 15 degrees. At these AOAs, the
green circle on the indexer will be lit up. If your AOA is below
11 degrees, the amber symbol will be lit up, and if it is above
15 degrees, the red symbol will be lit up (and you are getting
close to stalling).&quot;
*/

// Input dataref from X-Plane:
FlightSimFloat aoa_degrees;

// Indexer LED pins: 
const int Amber = 13;
const int Green = 15;
const int Red = 17;

void setup() {
  aoa_degrees = XPlaneRef(&quot;sim/flightmodel2/misc/AoA_angle_degrees&quot;);
  
  pinMode( Amber, OUTPUT );
  pinMode( Green, OUTPUT );
  pinMode( Red, OUTPUT );
}

void loop() {
  FlightSim.update();
  
  if( aoa_degrees &lt; 11 ) {
    digitalWrite( Amber, HIGH );
    digitalWrite( Green, LOW );
    digitalWrite( Red,   LOW );
  }

  if( 11 &lt;= aoa_degrees &amp;&amp; aoa_degrees &lt; 15 ) {
    digitalWrite( Amber, LOW );
    digitalWrite( Green, HIGH );
    digitalWrite( Red,   LOW );
  }

  if( 15 &lt;= aoa_degrees) {
    digitalWrite( Amber, LOW );
    digitalWrite( Green, LOW );
    digitalWrite( Red,   HIGH );
  }
}

This works! Flying in the XP10 F-4, it matches the AoA indicators next to the HUD. However,  when parked and with a tailwind, the lights flicker annoyingly as the AoA passes from -179 to +179 degrees. Probably the best thing to do is to get the airspeed dataref and only light the LEDs when the airspeed is above, say, 50kts, but here it’s much simpler to put a lower limit on the amber and an upper limit on the red lights:

/*
AOA Indexer example

&quot;The angle of attack indexer is used during final approach to
landing. In order to land the aircraft correctly, your angle of
attack should be between 11 and 15 degrees. At these AOAs, the
green circle on the indexer will be lit up. If your AOA is below
11 degrees, the amber symbol will be lit up, and if it is above
15 degrees, the red symbol will be lit up (and you are getting
close to stalling).&quot;
*/

// Input dataref from X-Plane:
FlightSimFloat aoa_degrees;

// Indexer LED pins: 
const int Amber = 13;
const int Green = 15;
const int Red = 17;

void setup() {
  aoa_degrees = XPlaneRef(&quot;sim/flightmodel2/misc/AoA_angle_degrees&quot;);
  
  pinMode( Amber, OUTPUT );
  pinMode( Green, OUTPUT );
  pinMode( Red, OUTPUT );
}

void loop() {
  FlightSim.update();
  
  if( -45 &lt;= aoa_degrees &amp;&amp; aoa_degrees &lt; 11 ) {
    digitalWrite( Amber, HIGH );
    digitalWrite( Green, LOW );
    digitalWrite( Red,   LOW );
  }

  if( 11 &lt;= aoa_degrees &amp;&amp; aoa_degrees &lt; 15 ) {
    digitalWrite( Amber, LOW );
    digitalWrite( Green, HIGH );
    digitalWrite( Red,   LOW );
  }

  if( 15 &lt;= aoa_degrees &amp;&amp; aoa_degrees &lt; 45 ) {
    digitalWrite( Amber, LOW );
    digitalWrite( Green, LOW );
    digitalWrite( Red,   HIGH );
  }
}

When you’ve got the blinkenlights working happily, the next step is to build some kind of enclosure and take this off the breadboard – but I’ll leave that as an exercise for the reader!

Encoders: coarse and fine control

Hi there!

Olli aka ‘Frumpy’ posted an interesting question on the X-Plane.org forums a little while ago:

I am doing an MCP, buttons, LEDs and now rotary encoders all work. Now comes the software part and this is certainly none of my favorites.

So the rotary encoders work. However, I want them to move at two different speeds: fast and slow spinning. Fast will do 4 steps ahead, slow only one. Think about setting a heading, you don’t want to spin the 20-click encoder 9 times to set a 180° heading change :)
This can be done with a timer I think.

{code snippet}

This should move the dataref by 1 once I touch the encoder. Next dataref change will depend on how fast I move it -> below (lets say) 30ms it will be a quick turn and move 4 steps, or slow turn then of course only one step :-)

I think this should help: http://playground.arduino.cc/Main/RotaryEncoderAcceleration
Still I look at it like a pig into a clockwork (that’s a German proverb ;-) ), I am not a programmer and certainly no C-guy. :-/

Thank you very much,

Olli

After an embarrassing amount of time where I began describing a system completely unlike what Olli described, I tested his idea and it works very well!

Here’s the code:

#include <Encoder.h>

/*
This code links a rotary encoder to the NAV1 OBS. If you turn the
encoder quickly, the bug changes quickly and you have fast control; if
you turn the encoder slowly, the bug changes slowly and you have fine
control.

This is an extension (and substantial rewrite) of the plain encoder-
reading code from before.
*/


// Input from hardware
Encoder demoEncoder(7, 8); // encoder on pins 7 and 8
short demoEncoderPrevious = 0;

elapsedMillis demoEncoderClickInterval = 0;

// Input/Output with simulator
FlightSimFloat demoDataref;


void setup() {
  demoDataref = XPlaneRef("sim/cockpit2/radios/actuators/nav1_obs_deg_mag_pilot");
}


void loop() {
  FlightSim.update();

  // divide by 4 to find how many clicks the encoder's been turned
  // (+ve clockwise, -ve anticlockwise, normally)
  short demoClicks = (demoEncoder.read() - demoEncoderPrevious) / 4;

  // when encoder 'clicks' into a new detent:
  if (demoClicks != 0) {

    // change in degrees from current stored value (can be <0 )
    float demoChange = 0;

    // Threshold between FINE and FAST control: 30ms per click
    if (demoEncoderClickInterval > 30) {
      // FINE/slow mode. Change by 0.25 degrees per click.
      demoChange = demoClicks * 0.25;
    } else {
      // FAST/coarse mode. Change by 5 degrees per click.
      demoChange = demoClicks * 5.0;
    }

    // new value = current value plus changes
    float demoNewValue = demoDataref + demoChange;

    // make sure new value is valid (i.e. when moving across 0deg/360deg)
    while (demoNewValue  <   0.0) demoNewValue += 360.0;
    while (demoNewValue >= 360.0) demoNewValue -= 360.0;

    // write validated new heading back to dataref
    demoDataref = demoNewValue;

    // reset encoder state and interval timer
    // (only when encoder has 'clicked' into a new detent!)
    demoEncoderPrevious = 0;
    demoEncoder.write(0);
    demoEncoderClickInterval = 0;
  }
}

It’s basically the same as the encoder code I wrote before except it has a timer (named ClickInterval) which is reset after each encoder click. The highlighted bit can be tinkered with to change the fast and slow values and the threshold between them, or replaced with a formula which gradually blends between coarse and fine control across a range of click intervals.

I’ll probably be using this for heading-bug-type controls going forwards. It’s much better than using two encoders, or one encoder and a shift button. Thanks Olli!

Ignition key example

In response to Justin on the X-Plane.org forums, who’s using a rotary switch to control the ignition key and asked for help with the code.

I’ll share the code then comment on it.

#include "Bounce.h"

// per X-Plane DataRef docs
enum IGN_KEY_POSITION {
  IGN_OFF = 0,
  IGN_LEFT = 1,
  IGN_RIGHT = 2,
  IGN_BOTH = 3,
  IGN_START = 4
};

// button input pins
const int Pin_IgnOff = 1;
const int Pin_IgnRight = 2;
const int Pin_IgnLeft = 5;
const int Pin_IgnBoth = 9;
const int Pin_IgnStart = 12;

Bounce ignOff = Bounce (Pin_IgnOff, 5);
Bounce ignRight = Bounce (Pin_IgnRight, 5);
Bounce ignLeft = Bounce (Pin_IgnLeft, 5);
Bounce ignBoth = Bounce (Pin_IgnBoth, 5);
Bounce ignStart = Bounce (Pin_IgnStart, 5);

// output dataref/commands
// the ignition key dataref does not engage the starter for some reason
// so we'll also use the normal engage_starter_1 command
FlightSimInteger ignPosition;
FlightSimCommand engineStart1;

void setup() {
  pinMode (Pin_IgnOff, INPUT_PULLUP);
  pinMode (Pin_IgnRight, INPUT_PULLUP);
  pinMode (Pin_IgnLeft, INPUT_PULLUP);
  pinMode (Pin_IgnBoth, INPUT_PULLUP);
  pinMode (Pin_IgnStart, INPUT_PULLUP);

  ignPosition = XPlaneRef("sim/cockpit2/engine/actuators/ignition_on[0]");
  engineStart1 = XPlaneRef("sim/starters/engage_starter_1");

  pinMode (LED_BUILTIN, OUTPUT);
}

void loop() {
  FlightSim.update();

  ignOff.update();
  ignRight.update();
  ignLeft.update();
  ignBoth.update();
  ignStart.update();

  // non-blocking input to update ignition key position
  if (ignOff.fallingEdge()) ignPosition = IGN_OFF;
  if (ignLeft.fallingEdge()) ignPosition = IGN_LEFT;
  if (ignRight.fallingEdge()) ignPosition = IGN_RIGHT;
  if (ignBoth.fallingEdge()) ignPosition = IGN_BOTH;
  // engage starter using command as well as by moving key
  if (ignStart.fallingEdge()) {
    ignPosition = IGN_START;
    engineStart1.begin();
    digitalWrite (LED_BUILTIN, HIGH);
  }
  if (ignStart.risingEdge()) {
    engineStart1.end();
    digitalWrite (LED_BUILTIN, LOW);
  }

// debug code for hardware test
//  if ( ignOff.read() == LOW ||
//       ignRight.read() == LOW ||
//       ignLeft.read() == LOW ||
//       ignBoth.read() == LOW ||
//       ignStart.read() == LOW
//       ) {
//    digitalWrite(LED_BUILTIN, HIGH);
//  } else {
//    digitalWrite(LED_BUILTIN, LOW);
//  }

}

First thing I did was look through the list of datarefs for anything that looked relevant to the ignition key.

http://www.xsquawkbox.net/xpsdk/docs/DataRefs.html
I found two candidates: sim/cockpit2/engine/actuators/ignition_key and sim/cockpit2/engine/actuators/ignition_on. They’re integer datarefs and the SDK website lists the meaning of their values: 0=off, 1=left etc.

Then, loading X-Plane and the default Cessna (which has a nice prominent ignition key to experiment on!), I opened DataRefEditor and typed ‘ignition’ into its filter box. A little bit of experimenting shows how they work: they’re almost identical. Write them a number between 0-4 and the key moves to that position. The difference is, if you move the key to ‘start’ using ignition_key it will go back to ‘both’ on the next frame, while if you use ignition_on it stays there. That’s the behaviour we want – we don’t want the key automatically moving anywhere, we want it to only move to where our hardware key tells it to go.

Next, I slapped some pushbuttons and a Teensy 3 onto a breadboard. (I don’t have any rotary switches at the moment!). It’s always useful to begin by writing code which simply tests your hardware, and you can see the remnant of that commented out at the bottom of the update function there – it made the onboard LED turn on when any of the buttons were pressed. You’d probably need to devise something different to test a rotary switch. I assume most rotary switches have a bunch of output pins which connect to a common pin as the switch is turned – this makes a rotary switch identical to a bunch of pushbuttons from an electrical perspective.

With the hardware tested, I added the FlightSimInteger (and FlightSim.update() – I’ve had enough of forgetting that line and spending ages wondering why the code doesn’t work :D ) The code will move the sim ignition switch when the hardware switch is moved. It won’t hold the sim switch to the hardware switch position (it’s not a ‘blocking’ input – remind me to write about that) so if you use some other way to turn the key (by clicking on it in the cockpit for example) the hardware won’t fight you and override the other input.

I found this would move the key nicely, but moving it to the ‘start’ position wasn’t engaging the starter motor. I have no idea why, and didn’t fancy spending all evening figuring it out – a nice quick solution was to add a FlightSimCommand object for the Engine 1 Starter Engage command, and have it active when the rotary switch is in the ‘Start’ position (or, in my case, when you’re pressing the pushbutton corresponding to the ‘Start’ position).

Just for fun, I made the onboard LED light as well when the starter’s engaged. Ideally you’d have a rotary switch like a car ignition where you have to hold the thing in the ‘Start’ position against a spring, but that kind of switch is likely hard to come by, so perhaps a warning LED can do instead. The LED can remind you if you’ve left the switch in the ‘Start’ position and forgotten about it.

And with that, the code appears complete – it appears to work on my desktop anyway :)

Avoiding magic numbers

Magic Numbers are literal numbers in your program, instead of a named variable. I often see them used for Teensy pin numbers. In principle they’re bad because they’re less descriptive than a named variable and harder to change. There’s at least three ways I’ve avoided using them in this short program:

  • using const int Pin_IgnOff = 0 etc for the pin numbers
  • using an to store the values we can feed to the ignition key dataref
  • using someCommand.begin() and someCommand.end() instead of someCommand = 1 and someCommand = 0

    Arguably these are all unnecessary refinements in a program this short – but it’s a good habit to get into, and pays dividends when you come back six months later to make a change, or (more usually) when this all becomes part of a much bigger project. For instance, if you’ve used named constants for your pin numbers, you only need to make a change in one place in the code when you connect your hardware to some different pins.

  • Storing dataref identifiers for Teensy 2.0

    There’s a problem I have with standard Teensyduino flight sim code. Here’s a slightly modified version of the GearPanel tutorial code:

    #include <Blah.h>
    
    ////////////////////////////////////////
    // Blah blah blah blah
    //
    const int blah = 45;
    Bounce blahblah = Bounce (blah, 5);
    
    ////////////////////////////////////////
    // Blah blah blah
    //
    const int blahblah = 10;
    const int blahblah = 11;
    const int blahblah = 12;
    
    const int blahblah = 20;
    const int blahblah = 19;
    const int blahblah = 18;
    
    ////////////////////////////////////////
    // Blah blah blah blah blah
    //
    // Blah blah blah
    FlightSimCommand blahblah;
    FlightSimCommand blahblah;
    
    // Landing gear actual position
    FlightSimFloat blahblahblah;
    FlightSimFloat gearDeployNose;
    FlightSimFloat blahblahblah;
    
    // Blahblah blah blah
    FlightSimInteger blahblahblah;
    
    // Blah blah blahblah blah
    FlightSimFloat blahblah;
    
    void setup() {
      
      //////////////////
      // Blah
      //
      pinMode (blahblahPin, INPUT_PULLUP);
      
      //////////////////
      // Blah
      //
      pinMode(blahblah, OUTPUT);
      pinMode(blahblah, OUTPUT);
      pinMode(blahblah, OUTPUT);
      
      pinMode(blahblah, OUTPUT);
      pinMode(blahblah, OUTPUT);
      pinMode(blahblah, OUTPUT);
      
      //////////////////
      // Blah
      //
      blahblah       = XPlaneRef("blah/blah/blah/blah");
      blahblah       = XPlaneRef("blah/blah/blah/blah");
      
      blahblahblah   = XPlaneRef("blah/blah/blah/blah");
      gearDeployNose = XPlaneRef("sim/flightmodel2/gear/deploy_ratio[0]");
      blahblahblah   = XPlaneRef("blah/blah/blah/blah");
      
      blahblahblah   = XPlaneRef("blah/blah/blah/blah");
      
      blahblah       = XPlaneRef("blah/blah/blah/blah");
    }
    
    void loop() {
      // blah blah blah etc
    }

    The problem is writing the DataRef identifier (for example sim/cockpit/radios/transponder_light) such a long way away from the FlightSimWotsit it will be assigned to. It gets really difficult to keep track of code like this.

    The FlightSimWotsit must be declared at global scope (ie, not inside a function), but the assignment must take place within a function, which is why we can’t write

    FlightSimFloat xpdrLight;
    xpdrLight = XPlaneRef("sim/cockpit/radios/transponder_light");
    
    void setup() {
      ...
    

    The next best thing is to store the identifier in a string or char array, created alongside the FlightSimClass at the top of the file.

    FlightSimFloat xpdrLight;
    const char xpdrLightIdent[] = "sim/cockpit/radios/transponder_light";
    
    void setup() {
      xpdrLight = XPlaneRef(xpdrLightIdent);
      pinMode(12, OUTPUT);
    }
    void loop() {
      FlightSim.update();
      digitalWrite(12, xpdrLight);
    }

    But if you try this, you’ll get a mysterious error: initializer fails to determine size of '__c'.

    This is because the Teensy 2.0 (and Teensy++ 2.0) processors have a ‘Harvard’ memory architecture, unlike nearly every other processor, which has ‘Von Neumann’ memory architecture. I don’t understand what that means, but I don’t need to: all I know is it’s possible to store constants in program memory, instead of in RAM, to save precious bytes. Paul’s done this with Teensyduino’s X-Plane identifiers, so the ident assignment operator (myFlightSimWotsit = XPlaneRef("some/x_plane/ident");) and the .assign() member function take pointers to program-memory chars, not ordinary ones. Such data is declared using the PROGMEM preprocessor macro.

    After a lot of head-scratching, help from Paul, and experimentation, I found the working pattern:

    FlightSimFloat xpdrLight;
    PROGMEM const char xpdrLightIdent[] = "sim/cockpit/radios/transponder_light";
    
    void setup() {
      xpdrLight.assign((const _XpRefStr_ *) xpdrLightIdent);
      pinMode(12,OUTPUT);
    }
    
    void loop() {
      FlightSim.update();
      digitalWrite(12,xpdrLight);
    }

    The PROGMEM statement stores the identifier in program memory, and the cast to _XpRefStr_ * allows us to give it to the .assign() member function later.

    (I haven’t figured out how to make this work with = XPlaneRef() syntax, but .assign() works well enough.)

    It’s possible to make the code easier to write using preprocessor #defines:

    #define DataRefIdent PROGMEM const char
    #define IdentRef (const _XpRefStr_ *)
    
    FlightSimFloat xpdrLight;
    DataRefIdent xpdrLightIdent[] = "sim/cockpit/radios/transponder_light";
    
    void setup() {
      xpdrLight.assign(IdentRef xpdrLightIdent);
      pinMode(12,OUTPUT);
    }
    
    void loop() {
      FlightSim.update();
      digitalWrite(12,xpdrLight);
    }

    And also we can now make classes to apply all the boilerplate for us, ‘hiding’ a FlightSimClass privately inside the class.

    #define DataRefIdent PROGMEM const char
    #define IdentRef (const _XpRefStr_ *)
    
    class SimpleLED {
    public:
      SimpleLED(const int & pin, const char * ident) {
        _pin = pin;
        _ident = ident;
      }
    
      void setup() {
        _dr.assign(IdentRef _ident);
        pinMode(_pin, OUTPUT);
      }
    
      void update() {
        digitalWrite(_pin, _dr);
      }
    
    private:
      FlightSimInteger _dr;
      int _pin;
      const char * _ident;
    };
    
    // ^^^ this code is written once and then 'hidden' in a header file
    ////////
    // vvv this code is what we write for our exciting new TransponderLight project
    
    DataRefIdent xpdrLightIdent[] = "sim/cockpit/radios/transponder_light";
    SimpleLED xpdrLight (12, xpdrLightIdent);
    
    void setup() {
      xpdrLight.setup();
    }
    
    void loop() {
      FlightSim.update();
      xpdrLight.update();
    }

    Look at that – no more calls to pinMode or digitalWrite or XPlaneRef when we want to use an LED with a nice boolean-style dataref! Two lines, one for the ident and another to create a SimpleLED, and that’s it! We still need to call .setup() and .update() for each LED, but we can eliminate all bar one of those using a linked list.

    We still need to write the ident on its own line, because the ident is a PROGMEM constant, and the class cannot be PROGMEM. (If I’ve missed something fundamental about C++ syntax, please let me know!)

    Then you can do all kinds of wonderful labour-saving things, like putting your power-supply simulation into the class, along with the ‘Is X-Plane enabled?’ checking, a generic bulb-test feature, and – most important – that linked list for setting up or updating all the instances of the class with one instruction, instead of writing foo.setup(); bar.setup(); baz.setup(); over and over again.

    I’ve been doing exactly that with my SimObjects library. So far it covers dataref-driven LEDs, program logic-driven LEDs, and dataref-driven servos. I’ll write more about it when it’s more polished and tested, but an up-to-date development version is available on GitHub here.

    As a final example, here’s the GearPanel project again, using the 15 Nov 2012 version of SimObjects.

    #include "Bounce.h"
    #include "SimLEDDev.h"
    
    
    //// Gear control elements
    const int gearSwitchPin = 45;
    Bounce gearSwitch = Bounce (gearSwitchPin, 5);
    FlightSimCommand gearUp;
    FlightSimCommand gearDown;
    
    
    //// Simulated power supply elements
    FlightSimFloat supplyVolts;
    const float voltsNeeded = 10.0;
    
    
    //// Gear position lights elements
    // Gear position dataref idents
    DataRefIdent gearPosID[3][64] = {
      "sim/flightmodel2/gear/deploy_ratio[1]",
      "sim/flightmodel2/gear/deploy_ratio[0]",
      "sim/flightmodel2/gear/deploy_ratio[2]" };
    
    // Gear is down when deploy-ratios are equal to 1.
    // These are the green lights. They light when their dataref is
    //  between 1.0 and 999.
    SimLEDFloatDR gearDownLeft  (15, gearPosID[0], 1.0, 999.0);
    SimLEDFloatDR gearDownNose  (16, gearPosID[1], 1.0, 999.0);
    SimLEDFloatDR gearDownRight (17, gearPosID[2], 1.0, 999.0);
    // and that's the green lights done. We don't need to think about
    // them specifically anywhere below this point!
    
    // Gear is wrong when gear deploy position doesn't match the gear
    // handle position. These are the red lights.
    SimLEDLocal gearWrongLeft  (12);
    SimLEDLocal gearWrongNose  (13);
    SimLEDLocal gearWrongRight (14);
    // The 'local' means 'Teensy program'. The signal to drive the LED
    // comes from our code, not automatically from an dataref from the
    // other end of the USB cable.
    
    // We need these for the red light driving logic.
    FlightSimFloat gearPos[3];
    FlightSimInteger gearHandleDown;
    
    
    
    void setup() {
      // Gear control
      pinMode (gearSwitchPin, INPUT_PULLUP);
      
      gearUp = XPlaneRef("sim/flight_controls/landing_gear_up");
      gearDown = XPlaneRef("sim/flight_controls/landing_gear_down");
      
      // Simulated power supply
      supplyVolts =     XPlaneRef("sim/cockpit2/electrical/bus_volts[0]");
      
      // Gear position light logic
      for (int i = 0; i < 3; ++i) {
        gearPos[i].assign(IdentRef gearPosID[i]);
      }
      gearHandleDown =  XPlaneRef("sim/cockpit2/controls/gear_handle_down");
      
      // Set up every SimLED
      SimObject::setup();
    }
    
    
    
    void loop() {
      FlightSim.update();
      
      // Gear control
      gearSwitch.update();
      if(gearSwitch.read() == LOW) { // if the switch is closed
        if (gearHandleDown == 0) { // if gear handle is up
          gearDown.once(); // move it down
        }
      } else {
        if (gearHandleDown == 1) { // if gear handle is down
          gearUp.once(); // move it up
        }
      }
      
      // Power supply
      SimObject::hasPower = (supplyVolts > voltsNeeded);
      
      // Gear position light logic
      gearWrongLeft.setActive(gearHandleDown != gearPos[0]);
      gearWrongNose.setActive(gearHandleDown != gearPos[1]);
      gearWrongRight.setActive(gearHandleDown != gearPos[2]);
      
      // Update every SimLED
      SimObject::update();
    }

    The classes are still in development, so function names and interfaces will change. (I welcome suggestions and feedback on this.) Looking at this code, I see opportunities to write more classes to manage switches, commands, and to wrap around datarefs to reduce the boilerplate even further.

    I understand Teensy 3.0 has a conventional memory architecture and will ignore the PROGMEM statement entirely. Then we can just store identifiers as regular old char arrays. But until Teensy 3.0 gains X-Plane support, and while users still have Teensy 2.0 projects to build, this is the way to make our lives easier.

    I hope this might be useful!
    – Jack

    Project management, & IDEs revisited

    Since writing ‘Setting up your Teensy development environment’ I’ve developed some better ways of handling all the source code that a Teensy-based hardware project can produce.

    1 – Separate your source code into different files.

    This is my workbench testing box, as of this morning. It is a combination of the Gear Lights tutorial, yesterday’s guide to using encoders, and a general-purpose radio tuner called OmniTune, all driven by one Teensy++ 2.0 board and making use of less than half of its pins.

    And this is the source code in the main sketch file, TestBox.ino:

    #include <LiquidCrystalFast.h>
    #include <Bounce.h>
    #include <Encoder.h>
    
    #include "EncoderLCD.h"
    #include "OmniTune.h"
    #include "DialController.h"
    #include "GearLights.h"
    
    void setup() {
      setupEncoderLCD();
      setupGearLights();
    }
    
    void loop() {
      FlightSim.update();
    
      loopEncoderLCD();
      loopGearLights();
    }
    

    All the work has been split up into four header files, making the code much more manageable. These are the files:

    GearLights, more or less exactly the code from the last Gear Panel tutorial. It lights the six gear lights based on X-Plane’s state and moves the gear handle according to the switch position.

    EncoderLCD, which handles input and output using the LCD and encoders. It parses the input through the encoders, and passes it to either OmniTune or Dial Controller, depending on which is selected. Also it calls the ‘update display’ function of the selected module.

    OmniTune, the all-purpose radio tuner, where you select a radio channel, see its frequency on the screen, and alter it with the encoders.

    Dial Controller, based on ‘Using encoders with X-Plane‘ article, using two encoders (one coarse, one fine) for easy and precise control of various dials in the cockpit.

    To turn GearLights into a header, this was remarkably easy.

    • copy GearLights.ino to the TestBox project folder
    • rename setup() and loop() to setupGearLights() and loopGearLights(),
    • rename GearLights.ino to GearLights.h
    • #include it in the TestBox.ino source file
    • call setupGearLights() and loopGearLights() from TestBox.ino’s setup() and loop() functions.

    Voila! GearLights is in my TestBox project, but out of the way. I could hammer away at the encoder-wrangling bits without needing to scroll through the ‘frozen’ GearLights code, and then use its pretty lights as a sanity check when testing, to help me realise I’d forgotten about FlightSim.update() again.

    It was a bit more effort to separate the input-parsing code from my old OmniTune project, and move it into EncoderLCD.h. I then needed to alter the dial-controlling code to use the same interface as OmniTune. The end result is I have four source files, each with limited to only doing one or two things, and a clear interface between them to add new modes and capabilities to the display (an etch-a-sketch mode comes to mind!). Furthermore, in the OmniTune and Dial Controller files, the input-handling and data-displaying bits are completely separate functions. It’s good to separate stuff out like this – I could replace the LCD-drawing code with 8-segment LEDs and not need to change the input-handling code at all, for example.

    I have no idea if this is  a ‘good’ way of separating a project out. All the code is in the global namespace, and it’s necessary to #include the files in the right order, and there’s probably a much smarter way of doing this using classes or something. (I’d be delighted to hear suggestions and criticism!)  But it works, and it’s a sight better than the One Monolithic Source Code File approach.

    The source code is available from my GitHub page for the curious.

    One caveat: the Arduino IDE does some truly horrible things to your source code when you press ‘compile’. You can’t include Arduino library files (Encoder.h, LiquidCrystalFast.h etc) from any file except your root .ino file – that’s why they’re polluting TestBox.ino, which you might notice hasn’t got any encoders or LCDs. The conventional way of doing things is to include Encoder.h from the file that uses encoders, but there are philosophical reasons why the Arduino IDE won’t let you do that.

    2. Use a sane IDE to develop your code

    I’ve touched on this before, but I’ve learned a lot more since the dark old days of two weeks ago. The Arduino IDE is geared for beginners, and fills that role very well – it doesn’t do much that Windows Notepad doesn’t do. But it gets very cumbersome very rapidly as projects become complex. That’s why I use Qt Creator to write my code, and only use the Arduino IDE to compile it.

    Before, I spouted some nonsense about copy-pasting from a generic Qt project into the Arduino IDE. There must have been something funny in the water that week. A much better way is to have a Qt project for each Arduino project, containing all the source files, with all the Arduino libraries included in the Qt .pro file. This way, you edit your code in Qt, hit ‘save’, switch to Arduino IDE, hit ‘upload’, switch to X-Plane, and test.

    This is the project view for my TestBox project:

    Snapshot of my project folder in Qt Creator

    All the source code (one .ino file, and indefinite headers) are in the project as  ‘Headers’. (It doesn’t seem to like having source files with an .ino extension). There are still many INCLUDEPATHs for all the Arduino libraries, but I’ve added the usb_flightsim/usb_api.h file from the bowels of the Arduino directory to the .pro file as a header, too. This means we just need to #include "usb_api.h" in a file to get proper syntax highlighting for the FlightSimObjects in Qt Creator, and we don’t need to remove that line before compiling. Unlike the very long and awkward #include I recommended last week, this one doesn’t make the Arduino IDE choke.

    If you set the Arduino IDE to ‘use external editor’, it will automatically reload your source code whenever you save it in Qt. Write your code in the comfort and elegance of Qt’s editing environment, hit ‘save’, switch over to Arduino, press ‘upload’, and hope you don’t get an error code!

    My current favourite Qt Creator feature is how holding down Ctrl makes your code into links. Ctrl-click on a function call and it jumps you to its definition. Ctrl-click on a filename in an #include statement and it opens that file. It makes for very fluid navigation of your code, especially if you’ve split your project into multiple files.

    The .pro file is on GitHub with the rest of my TestBox project, but for completeness I’ll copy the contents here. You should see a ‘show source’ link here. Look for the <> icon on the mouseover menu; it lets you copy source code from WordPress pages very easily. It should be obvious how to alter this for your own use!

    HEADERS += \
        GearLights.h \
        OmniTune.h \
        EncoderLCD.h \
        DialController.h \
        TestBox.ino
    
    INCLUDEPATH += \
    ../../xteensy/SimObjectsDev/ \
    ../_arduino-1.0.1/libraries/AccelStepper \
    ../_arduino-1.0.1/libraries/AltSoftSerial \
    ../_arduino-1.0.1/libraries/ArdOSC \
    ../_arduino-1.0.1/libraries/Bounce \
    ../_arduino-1.0.1/libraries/CapSense \
    ../_arduino-1.0.1/libraries/DmxSimple \
    ../_arduino-1.0.1/libraries/DogLcd \
    ../_arduino-1.0.1/libraries/EEPROM \
    ../_arduino-1.0.1/libraries/Encoder \
    ../_arduino-1.0.1/libraries/Ethernet \
    ../_arduino-1.0.1/libraries/Firmata \
    ../_arduino-1.0.1/libraries/FlexiTimer2 \
    ../_arduino-1.0.1/libraries/FreqCount \
    ../_arduino-1.0.1/libraries/FreqMeasure \
    ../_arduino-1.0.1/libraries/FrequencyTimer2 \
    ../_arduino-1.0.1/libraries/IRremote \
    ../_arduino-1.0.1/libraries/Keypad \
    ../_arduino-1.0.1/libraries/ks0108 \
    ../_arduino-1.0.1/libraries/LedControl \
    ../_arduino-1.0.1/libraries/LedDisplay \
    ../_arduino-1.0.1/libraries/LiquidCrystal \
    ../_arduino-1.0.1/libraries/LiquidCrystalFast \
    ../_arduino-1.0.1/libraries/list.txt \
    ../_arduino-1.0.1/libraries/LowPower \
    ../_arduino-1.0.1/libraries/Metro \
    ../_arduino-1.0.1/libraries/MIDI \
    ../_arduino-1.0.1/libraries/MsTimer2 \
    ../_arduino-1.0.1/libraries/NewSoftSerial \
    ../_arduino-1.0.1/libraries/OneWire \
    ../_arduino-1.0.1/libraries/Ping \
    ../_arduino-1.0.1/libraries/PS2Keyboard \
    ../_arduino-1.0.1/libraries/PWMServo \
    ../_arduino-1.0.1/libraries/SD \
    ../_arduino-1.0.1/libraries/Servo \
    ../_arduino-1.0.1/libraries/ShiftPWM \
    ../_arduino-1.0.1/libraries/SoftPWM \
    ../_arduino-1.0.1/libraries/SoftwareSerial \
    ../_arduino-1.0.1/libraries/SPI \
    ../_arduino-1.0.1/libraries/ST7565 \
    ../_arduino-1.0.1/libraries/Stepper \
    ../_arduino-1.0.1/libraries/TimerOne \
    ../_arduino-1.0.1/libraries/TinyGPS \
    ../_arduino-1.0.1/libraries/TimerThree \
    ../_arduino-1.0.1/libraries/Tlc5940 \
    ../_arduino-1.0.1/libraries/VirtualWire \
    ../_arduino-1.0.1/libraries/Wire \
    ../_arduino-1.0.1/libraries/x10 \
    ../_arduino-1.0.1/libraries/XBee \
    
    HEADERS += \
    ../_arduino-1.0.1/hardware/teensy/cores/usb_flightsim/usb_api.h
    
    SOURCES +=
    

    3: Use automatically-generated documentation.

    By using a slightly specialised commenting syntax (generally, beginning comments with /// instead of //) you can get your comments to be picked up by automatic documentation-generating packages. I’ve been using Doxygen for this for a couple of days, so I’ve got a lot to learn about it. It looks like it will make complex project much easier to understand and maintain. Watch this space!

    Using encoders with X-Plane

    Rotary encoders are very useful little components. In this article I’ll explain what they are, and walk through the process of using them to control stuff in X-Plane.

    If you’re not sure exactly what an encoder is, look at your mouse scroll wheel: that’s a rotary encoder. It’s a device which you can rotate, usually with sprung detents so you can feel how far you’ve turned it, and encode the rotation such that an Arduino-esque microcontroller can read it. They are used to send a pair of commands to your system, like ‘scroll up/scroll down’, ‘zoom in/zoom out’, ‘volume up/volume down’. For our purposes we can add ‘move heading bug left/right’, ‘move elevator trim nose-up/nose-down’, ‘COM2 fine adjust up/down’, ‘turn ignition switch clockwise/counterclockwise’…

    All the above can be controlled with two keyboard buttons, or (the traditional flight sim method) clicking on two slightly different parts of a knob. I was quite fond of the method used by old DreamFleet for Microsoft Flight Sim projects, where left-click was ‘turn left’ and right-click was ‘turn right’, but there appear to be deeply-held philosophical reasons why X-Plane ignores the right mouse button. (I think it’s because Laminar are all Mac users and don’t have a right mouse button – just kidding, just kidding!) It doesn’t matter though, because we can use an encoder to apply the up/down commands instead. An encoder is basically a convenient way to apply up/down-style pairs of commands.

    They look similar to potentiometers (pots), but they work in a completely different way. Encoders signal change of position, while pots signal absolute position. You can share input between an encoder and other components – for example, you can scroll this page with your mouse scroll wheel or with the cursor keys – but if a system uses a pot for input it is usually not sensible to take input from anything else.

    The main advantage of pots is the input of the pot indicates the state of the input – you can look at the volume control knob, see the white mark is halfway between ‘off’ and ‘max’, and know the volume is set to 50% – and it is easy to make small and coarse inputs with one control. You can’t do either with a single encoder. The angle of an encoder’s shaft has no meaning, it is only changes to angle which have meaning. And you can either set the encoder to make small changes when you turn it, which makes it tedious to make large changes as you have to turn the shaft a lot, or to make larger changes, making it impossible to make small changes at all. (I’ll show how two encoders can circumvent this.)

    There is another type of encoder – an absolute encoder – which shows the absolute position of the shaft. (It knows as soon as the system is powered up that the shaft is exactly 85.5° from the datum position.) Typically they have a lot more pins and are a lot more expensive than the relative-position encoders we’re interested in.

    An encoder has three pins. The middle one is (usually) the common pin, and the other two connect to the middle pin as the shaft is turned, transmitting the rotation in Gray code to the microcontroller. The simplest way to explain this is to demonstrate. Here, I’ve connected two LEDs to the output pins and the common pin to ground, so the LEDs light when their pin is connected to the common pin:

    The encoders I bought also have integrated pushbuttons, which are very useful. The pushbutton is electrically separate from the encoder, and is activated by pressing down on the shaft. It uses the two pins on the other side of the encoder, visible in the photo at the top of the page.

    I got mine from eBay for about $0.50 each. Look for phrases like ‘continuous rotary encoder’, ‘for use with microcontrollers’, and ‘2-bit gray code output’. They have little tags on their bases, which if you bend out slightly, let you put the encoder into the middle of a breadboard. I find they keep popping out of the board, though – it’s much better to solder leads onto the pins and attach the encoders to a basic panel of some kind.

    Using an encoder with Arduino/Teensy/X-Plane.

    The Arduino Encoder library does most of the hard work for us. Let’s look at the ‘Basic’ example sketch which comes with the Arduino IDE:

    /* Encoder Library - Basic Example
     * http://www.pjrc.com/teensy/td_libs_Encoder.html
     *
     * This example code is in the public domain.
     */
    
    #include <Encoder.h>
    
    // Change these two numbers to the pins connected to your encoder.
    // Best Performance: both pins have interrupt capability
    // Good Performance: only the first pin has interrupt capability
    // Low Performance: neither pin has interrupt capability
    Encoder myEnc(5, 6);
    // avoid using pins with LEDs attached
    
    void setup() {
     Serial.begin(9600);
     Serial.println("Basic Encoder Test:");
    }
    
    long oldPosition = -999;
    
    void loop() {
     long newPosition = myEnc.read();
     if (newPosition != oldPosition) {
     oldPosition = newPosition;
     Serial.println(newPosition);
     }
    }

    Don’t quote me on this, but I’m not sure the stuff about interrupt pins is definitely relevant for human-interface encoders. I think that’s for situations where you’re using the encoder to record the motion of fast-moving motorised things, for instance a digital odometer for  car. But here, the input’s coming from our fingers, which are slower than motors. I haven’t used interrupt pins and the encoder works just fine – your mileage may vary!

    There’s two important things to notice about the code. We make an Encoder object:

    Encoder myEnc(5, 6);

    and we read it using myEnc.read(). We also need to store the previous position (oldPosition), because we determine an input by comparing myEnc.read() with oldPosition. If read() is bigger, we’ve turned the encoder one way; if it is smaller, we’ve turned the other way. (Which direction depends on which way round you’ve connected the pins. I don’t try to remember which way is clockwise-positive, I just swap the pins (Encoder myEnc(6, 5)) if I get it wrong the first time.)

    Let’s turn this into an X-Plane example, and adjust the heading bug dataref:

    #include <Encoder.h>
    
    Encoder myEnc(7, 8);
    long myEncPrev = 0; // more descriptive name than oldPosition, IMO
    
    FlightSimFloat headingBug;
    
    void setup() {
     headingBug = XPlaneRef("sim/cockpit2/autopilot/heading_dial_deg_mag_pilot");
    }
    
    void loop() {
     FlightSim.update();
    
     // compare current position to previous position
     int myEncDiff = myEnc.read() - myEncPrev;
    
     // update previous position
     myEncPrev = myEnc.read();
    
     // if there was movement, change the dataref
     if (myEncDiff) {
     headingBug = headingBug + myEncDiff;
     // (no += operator for FlightSimFloat yet!)
     }
    }

    This seems to work just fine! Twiddle the encoder and the bug moves in X-Plane. But, if I look carefully at what’s going on (go to the Plugin menu, then ‘Show Communications’ from the TeensyControls submenu), the dataref is changing by 4 degrees each click.

    The reason for this is my encoders have been set up this way. The Gray code output changes by 4 between each detent: it goes (off-off), (off-on), (on-on), (on-off), *click!*(off-off). I’m only interested in how many detents I’ve clicked through, so I need to divide myEncDiff by 4 to get that value. Happily, C++ integer division will round (3/4) down to 0, so if(myEncDiff) is only true when I click through a whole detent.

    Another problem is where we pass through 0/360. The encoder sets the dataref to values out of range, like -15 and 368, and after a second or so X-Plane notices and puts the dataref back to the 0-360 range. We shouldn’t rely on X-Plane being nice to us like this, so let’s revise the program with range checking.

    Finally, instead of updating myEncPrev with myEnc.read(), let’s set them both to 0 when we reach a detent. This way, we won’t eventually (after many many turns) overflow the integer. Also we can use short instead of long or int and save a couple of bytes of RAM ;-)

    #include <Encoder.h>
    
    Encoder myEnc(7, 8);
    short myEncPrev = 0;
    
    FlightSimFloat headingBug;
    
    void setup() {
      headingBug = XPlaneRef("sim/cockpit2/autopilot/heading_dial_deg_mag_pilot");
    }
    
    void loop() {
      FlightSim.update();
    
      // divide by 4 to find how many 'clicks' the encoder's gone through
      short myEncDiff = (myEnc.read() - myEncPrev) / 4;
    
      if (myEncDiff) {
        // only update prev when we've reached a detent!
        myEncPrev = 0;
        myEnc.write(0);
    
        // copy dataref to temporary value
        float hdg = headingBug;
    
        // apply changes to temp value
        hdg += myEncDiff;
    
        // do range checking
        while (hdg < 0.0) hdg += 360.0;
        while (hdg >= 360.0) hdg -= 360.0;
    
        // write validated new heading back to dataref
        headingBug = hdg;
      }
    }

    That’s it! We have a working heading-bug controller!

    Let’s extend the code a bit. How about also controlling the elevator trim with the same encoder? We’ll use a button to change the modes, and LEDs to indicate which mode we’re in.

    #include <Encoder.h>
    #include <Bounce.h>
    
    // here I'm using enum to avoid writing 'const int' a lot
    enum pins {
      MyEnc_A = 7,
      MyEnc_B = 8,
      ModeSwitchPin = 20,
      HdgModeLED = 12,
      ElevTrimLED = 13
    };
    
    Encoder myEnc(MyEnc_A, MyEnc_B);
    short myEncPrev = 0;
    
    Bounce modeSwitch = Bounce (ModeSwitchPin, 5);
    int mode = 0;
    // mode: 0 for heading-bug, 1 for elevator
    
    FlightSimFloat headingBug;
    FlightSimFloat elevTrim;
    
    void setup() {
      pinMode(ModeSwitchPin, INPUT_PULLUP);
      pinMode(HdgModeLED, OUTPUT);
      pinMode(ElevTrimLED, OUTPUT);
    
      headingBug = XPlaneRef("sim/cockpit2/autopilot/heading_dial_deg_mag_pilot");
      elevTrim = XPlaneRef("sim/cockpit2/controls/elevator_trim");
    }
    
    void loop() {
      FlightSim.update();
      modeSwitch.update();
    
      // change mode when switch pressed
      if(modeSwitch.fallingEdge()) {
        ++mode;
        if(mode > 1)
          mode = 0;
      }
    
      //light status LEDs
      digitalWrite(HdgModeLED, (mode == 0));
      digitalWrite(ElevTrimLED, (mode == 1));
    
      short myEncDiff = (myEnc.read() - myEncPrev) / 4;
    
      if (myEncDiff) {
        myEncPrev = 0;
        myEnc.write(0);
    
        if (mode == 0) {
          float tmp = headingBug;
          tmp += myEncDiff;
          while (tmp < 0.0) tmp += 360.0;
          while (tmp >= 360.0) tmp -= 360.0;
          headingBug = tmp;
        }
    
        if (mode == 1) {
          float tmp = elevTrim;
          tmp += myEncDiff;
          while (tmp < -1.0) tmp = -1.0;
          while (tmp > 1.0) tmp = 1.0;
          elevTrim = tmp;
        }
      }
    }
    

    Oops, this doesn’t work. We’re altering the elevator trim by 1; we ought to scale it. Let there be elevTrimScalar!

    Also, picking -1.0 and 1.0 for the trim position limits is naive; it varies by aircraft, and there are datarefs for the limits.

    Incidentally, comparing mode to 0 and 1 all the time is poor form. Let’s use another enum to automatically number the modes.

    #include <Encoder.h>
    #include <Bounce.h>
    
    enum pins {
      MyEnc_A = 7,
      MyEnc_B = 8,
      ModeSwitchPin = 20,
      HdgModeLED = 12,
      ElevTrimLED = 13
    };
    
    // Encoder things
    Encoder myEnc(MyEnc_A, MyEnc_B);
    short myEncPrev = 0;
    
    // Mode things
    Bounce modeSwitch = Bounce (ModeSwitchPin, 5);
    enum Modes {
      Mode_Heading, // automatically = 0
      Mode_ElevTrim, // automatically = 1
      Mode_Count // automagically = 2, and we have 2 modes, that's convenient!
    };
    int mode = 0;
    
    // Heading mode things
    FlightSimFloat headingBug;
    
    // Elev trim mode things
    FlightSimFloat elevTrim;
    FlightSimFloat elevMin;
    FlightSimFloat elevMax;
    float elevTrimScalar = 0.005;
    
    void setup() {
      pinMode(ModeSwitchPin, INPUT_PULLUP);
      pinMode(HdgModeLED, OUTPUT);
      pinMode(ElevTrimLED, OUTPUT);
    
      headingBug = XPlaneRef("sim/cockpit2/autopilot/heading_dial_deg_mag_pilot");
      elevTrim = XPlaneRef("sim/cockpit2/controls/elevator_trim");
      elevMin = XPlaneRef("sim/aircraft/controls/acf_min_trim_elev");
      elevMax = XPlaneRef("sim/aircraft/controls/acf_max_trim_elev");
    }
    
    void loop() {
      FlightSim.update();
      modeSwitch.update();
    
      // change mode when switch pressed
      if(modeSwitch.fallingEdge()) {
        ++mode;
        if(mode >= Mode_Count)
          mode = 0;
      }
    
      //light status LEDs
      digitalWrite(HdgModeLED, (mode == Mode_Heading));
      digitalWrite(ElevTrimLED, (mode == Mode_ElevTrim));
    
      short myEncDiff = (myEnc.read() - myEncPrev) / 4;
    
      if (myEncDiff) {
        myEncPrev = 0;
        myEnc.write(0);
    
        if (mode == Mode_Heading) {
          float tmp = headingBug;
          tmp += myEncDiff;
          while (tmp < 0.0) tmp += 360.0;
          while (tmp >= 360.0) tmp -= 360.0;
          headingBug = tmp;
        }
    
        if (mode == Mode_ElevTrim) {
          float tmp = elevTrim;
          tmp += (myEncDiff * elevTrimScalar);
          while (tmp < -elevMin) tmp = -elevMin;
          while (tmp > elevMax) tmp = elevMax;
          elevTrim = tmp;
        }
      } //if encDiff
    } // loop
    

    All is well and good! And using enum to keep track of the modes like this makes it easy to add new datarefs. Ideally, we would build a class, containing a dataref, upper and lower limits, the policy for when the value exceeds limits (do they loop round like the heading, 361 going to 1, or get cut back like the trim?), and some static function which takes the mode and the difference as arguments and does all the calculation for us behind the scenes. But that’s beyond the scope of this article.

    Instead, what if we add a second encoder? Remember, earlier, that a disadvantage of encoders is they change a value by a fixed amount, which is generally both too small and too large. We might want to set the heading bug in increments smaller than 1 degree, we might want to turn it faster than 1 degree per increment. Using two encoders, one for coarse change, the other for fine change, we can do this.

    I’ve added a second encoder, so we have a coarseEnc and a fineEnc now, and also a coarseEncDiff and a fineEncDiff. We combine these two values to get a final encDiff which we then give to the mode-specific bits exactly like before. The ratio between coarse and fine is defined by CoarsetoFineRatio. For good measure I’ve added a Nav1 OBS setting too.

    #include <Encoder.h>
    #include <Bounce.h>
    
    enum pins {
      CoarseEnc_A = 7,
      CoarseEnc_B = 8,
      FineEnc_A = 10, // wired this one in backwards. Instead of playing
      FineEnc_B = 9,  // with the wires, let's just swap the order here!
      ModeSwitchPin = 20,
      HdgModeLED = 12,
      Nav1OBSLED = 13,
      ElevTrimLED = 14
    };
    
    // Encoder things
    Encoder coarseEnc(CoarseEnc_A, CoarseEnc_B);
    short coarseEncPrev = 0;
    
    Encoder fineEnc(FineEnc_A, FineEnc_B);
    short fineEncPrev = 0;
    
    const int CoarseToFineRatio = 20;
    
    // Mode things
    Bounce modeSwitch = Bounce (ModeSwitchPin, 5);
    enum Modes {
      Mode_Heading, // automatically = 0
      Mode_Nav1OBS, // let's just add this in here
      Mode_ElevTrim, // automatically now equal to 2 since we put Nav1OBS in front
      Mode_Count // automagically = 3, and we have 3 modes!
    };
    int mode = 0;
    
    // Heading mode things
    FlightSimFloat headingBug;
    float headingBugScalar = 0.25;
    
    // Nav1 OBS things
    FlightSimFloat nav1OBS;
    float nav1OBSScalar = 0.25;
    
    // Elev trim mode things
    FlightSimFloat elevTrim;
    FlightSimFloat elevMin;
    FlightSimFloat elevMax;
    float elevTrimScalar = 0.005;
    
    void setup() {
      pinMode(ModeSwitchPin, INPUT_PULLUP);
      pinMode(HdgModeLED, OUTPUT);
      pinMode(Nav1OBSLED, OUTPUT);
      pinMode(ElevTrimLED, OUTPUT);
    
      headingBug = XPlaneRef("sim/cockpit2/autopilot/heading_dial_deg_mag_pilot");
    
      nav1OBS = XPlaneRef("sim/cockpit2/radios/actuators/nav1_obs_deg_mag_pilot");
    
      elevTrim = XPlaneRef("sim/cockpit2/controls/elevator_trim");
      elevMin = XPlaneRef("sim/aircraft/controls/acf_min_trim_elev");
      elevMax = XPlaneRef("sim/aircraft/controls/acf_max_trim_elev");
    }
    
    void loop() {
      FlightSim.update();
      modeSwitch.update();
    
      // change mode when switch pressed
      if(modeSwitch.fallingEdge()) {
        ++mode;
        if(mode >= Mode_Count)
          mode = 0;
      }
    
      // light status LEDs
      digitalWrite(HdgModeLED, (mode == Mode_Heading));
      digitalWrite(Nav1OBSLED, (mode == Mode_Nav1OBS));
      digitalWrite(ElevTrimLED, (mode == Mode_ElevTrim));
    
      // find encoder movement
      short coarseEncDiff = (coarseEnc.read() - coarseEncPrev) / 4;
      short fineEncDiff = (fineEnc.read() - fineEncPrev) / 4;
    
      // reset encoders after they move
      if (coarseEncDiff) {
        coarseEncPrev = 0;
        coarseEnc.write(0);
      }
      if (fineEncDiff) {
        fineEncPrev = 0;
        fineEnc.write(0);
      }
    
      // combine coarseEncDiff with fineEncDiff
      int encDiff = (CoarseToFineRatio * coarseEncDiff) + fineEncDiff;
    
      if (encDiff) {
        if (mode == Mode_Heading) {
          float tmp = headingBug;
          tmp += encDiff * headingBugScalar;
          while (tmp < 0.0) tmp += 360.0;
          while (tmp >= 360.0) tmp -= 360.0;
          headingBug = tmp;
        }
    
        if (mode == Mode_Nav1OBS) {
          float tmp = nav1OBS;
          tmp += encDiff * nav1OBSScalar;
          while (tmp < 0.0) tmp += 360.0;
          while (tmp >= 360.0) tmp -= 360.0;
          nav1OBS = tmp;
        }
    
        if (mode == Mode_ElevTrim) {
          float tmp = elevTrim;
          tmp += encDiff * elevTrimScalar;
          while (tmp < -elevMin) tmp = -elevMin;
          while (tmp > elevMax) tmp = elevMax;
          elevTrim = tmp;
        }
      } //if encDiff
    } // loop

    One final note. Don’t delay the main Arduino loop when using encoders. If you need to delay something, put it into a separate function and put a timer or counter in loop() to call it at a suitable interval. (See the OmniTune code for updating the display for an example.) If you delay the main loop(), your system will fail to read() changes in the encoder’s position that happen during the delay. You’ll notice this happening because the encoder doesn’t work. I learned this the hard way…

    I think I’ll leave it there. Play around with the scalars and the ratios and find a setting that works for you. I leave mode-specific coarse-to-fine ratios as an exercise for the reader ;-)

    If you’d like to see encoders used for tuning the radios, have a look at my OmniTune project – the source code is on GitHub here.

    Gear panel tutorial, part 3b – Building a temporary panel

    This is a small update to yesterday’s post. No programming at all this time!

    I’ve built a quick and simple, temporary panel for the Gear Panel project. Breadboards are great for developing circuits, but it’s awkward trying to flip a toggle switch that’s floating on the end of a pair of wires. There’s the risk of accidentally disconnecting stuff while it’s powered up, which I assume could damage the Teensy. By mounting the components on a piece of MDF or plywood, we can use the system more comfortably while we test and develop it.

    I should warn you. I love writing code, but I’m far less skilled when it comes to things made out of atoms. What I’ve made isn’t particularly wonderful. Quick and simple, remember? It is built with the stuff I found lying around the family garage. All I was looking for was a board I could drill holes in and attach the switch and LEDs to, and some way of keeping it off the desk and attached to the Teensy breadboard. This isn’t intended as a ‘production’-standard piece of hardware, but a workbench tool.

    I got a big sheet of 4mm MDF from the local hardware shop, and cut a panel to fit a convenient cardboard box. I used nails held in place with hot glue to locate the panel into the box. (Only because I had nails right there on the countertop in a little box, I didn’t want to dig through the junk to find dowels or bolts.) I drilled a few holes into an offcut to find out which diameter drill bits I needed for the LEDS and for the toggle switch. Then I used those holes as a jig, to hold the LEDs in place while I soldered on the leads.

    I connected the cathodes of the LEDs together so I could use a single return pin. I found my heatshrink too narrow to go over two wires so I couldn’t insulate those joins, unfortunately. The six LED anodes I connected to a strip of header pins, and the cathode got a single pin. I used header pins to easily and reliably connect back to the breadboard; for a permanent project I’d just solder the wires directly to the stripboard or PCB. Header pins reduce the number of loose wires floating around your project – always a bonus!

    Perhaps I’m a little OCD, but I didn’t want to drill holes randomly into the final board, even if it is just a workbench tool. I needed to create a template. As with most problems in life, this was solved by using a spreadsheet:

    Adjusting the width and height of the columns was the easiest and quickest way of getting a piece of paper with nice symmetrically- spaced marks. I used the offcut again to practice drilling through the template – I needed practice getting the drill aligned.

    I would use a hot glue gun to permanently mount and protect the LEDs, but the 5mm hole holds them firmly enough for now.

    I have a confession. I lied when I said there was no programming. But it was only to change the pin numbers! This is what my breadboard looks like now:

    I had to butcher the cardboard box slightly to get the breadboard to fit inside it. The final touch was an assault with a Dymo labeller.

    Now it’s much easier to move the gear switch, and to see or feel what position the gear is supposed to be in. And the big empty space on the remaining 90% of the board is crying out to be filled with more useful switches, buttons, potentiometers, servos… watch this space.

    Gear panel tutorial, part 3 – toggle switch input

    Let’s add a landing gear switch to our panel.

    To simplify things, let’s start a new Arduino sketch, work on the switch without being distracted by the LED code, then merge the two together again afterwards.

    I got a basic toggle switch from my local electronics shop. It’s a latching (i.e., not momentary) ON-ON switch, but ON-OFF would work just as well here. For a simple application we can assume that ‘not down’ is the same thing as ‘up’. I can’t just plug it into the breadboard because it has solder-tag connections instead of PCB pins, so I had to solder leads onto it to connect it to the Teensy.

    (It is a toggle switch, not a slide switch, but Fritzing doesn’t have any images for those.)

    Connecting switches and buttons to a Teensy is very simple. The switch/button just connects one of the Teensy’s pins to the ground. As with the LEDs, it doesn’t matter which pin we use, as long as it’s not:
    – one of the power supply pins (marked +5V and GND)
    – the analog reference pin (marked R)
    – the pin with the built-in LED wired into it. For Teensy++ 2.0 this is pin 6; for the smaller Teensy 2.0 it is pin 11.

    A toggle switch generally has three pins. A common pin (in the middle), and two output pins. The position of the switch determines which output pin is connected to the common pin. We should connect the common pin to ground, and one of the output pins to an ordinary Teensy pin. I’m using pin 25.

    While I’m talking about wires in tedious detail, it’s a really good idea to get a lot of different coloured wire. It’s not necessary to perfectly adhere to a comprehensive colour-coding system, but if you always use black wire to for the ground connections, red wire for the +5v connections, and other colours for other connections, it will be much harder to become confused while wiring things up. And a stack of different-coloured spools of wire looks pretty.

    Let’s test the switch by making the onboard LED light up. It’s always worth testing each new thing and only adding to something you know is working. Whenever I jump ahead I end up with code which doesn’t work for many possible reasons and it takes much longer to find out which part exactly contains the fault. I end up rewriting code which contains no faults to see if that fixes the problem, which then introduces new faults…

    This code will read the switch/button connected to pin 25, and light up the on-board LED with the result.

    #include <Bounce.h>
    
    const int mySwitchPin = 25;
    Bounce mySwitch = Bounce (mySwitchPin, 5);
    
    void setup() {
      pinMode (mySwitchPin, INPUT_PULLUP);
      pinMode (LED_BUILTIN, OUTPUT);
    }
    
    void loop() {
      mySwitch.update();
    
      if(mySwitch.read() == LOW) {
        // it's LOW if we've closed the switch
        // and connected the pin to the ground
        digitalWrite(LED_BUILTIN, HIGH);
      } else {
        digitalWrite(LED_BUILTIN, LOW);
      }
    }
    

    A ‘Bounce’ object smooths out the data coming from the input pin, to hide any ‘chatter’ or flickering in input in the milliseconds where the switch is half-closed (a phenomena known as ‘bouncing’). To use a Bounce object, we need to include the Bounce header file in our project. #include <Bounce.h> essentially is an automatic copy-paste from the Bounce.h file where Bounces are defined.

    In this line:

    Bounce mySwitch = Bounce (mySwitchPin, 5);

    we create a Bounce object named mySwitch which listens to pin 25 and waits for 5 milliseconds before deciding the input has definitely changed. Instead of investigating the pin directly with the digitalRead function, we look at mySwitch to find out if the switch is open or closed.

    We do this with this line.

    if (mySwitch.read() == LOW) {

    mySwitch.read() will be low if the switch is closed, connecting it to ground. If the switch is open, the pullup resistor we activated when we said pinMode (mySwitchPin, INPUT_PULLUP); will make the pin voltage high, which makes mySwitch.read() also be high.

    Let’s rename mySwitch to gearSwitch and link it to a dataref. We need to find a dataref which describes the gear handle position and is writeable. We could dig around with DataRefEditor again, but as it happens we found one in part 2: sim/cockpit2/controls/gear_handle_down. We can write to the dataref in the same way as we just updated the LED:

    #include <Bounce.h>
    
    const int gearSwitchPin = 25;
    Bounce gearSwitch = Bounce (gearSwitchPin, 5);
    
    // Landing gear control dataref
    FlightSimInteger gearHandleDown;
    
    void setup() {
      pinMode (gearSwitchPin, INPUT_PULLUP);
      pinMode (LED_BUILTIN, OUTPUT);
    
      gearHandleDown = XPlaneRef("sim/cockpit2/controls/gear_handle_down");
    }
    
    void loop() {
      FlightSim.update();
      gearSwitch.update();
    
      if(gearSwitch.read() == LOW) { // if switch is closed
        digitalWrite(LED_BUILTIN, HIGH); // turn on LED
        // move gear handle to 'down' position
        gearHandleDown = 1;
      } else { // if switch is up
        digitalWrite(LED_BUILTIN, LOW);  // turn off LED
        // move gear handle to 'up' position
        gearHandleDown = 0;
      }
    }
    

    You should find your switch now controls the gear handle as well as the on-board LED!

    But this is not a very good way of controlling a dataref. Open up the TeensyControls monitor screen (Plugins menu, TeensyControls, and ‘Show Communication’). Move the switch a few times and you’ll see how frantically often we’re updating the gear handle. Carelessly re-sending the same information over and over again is wasteful. But we can avoid this by only writing to gearHandleDown when it isn’t already the value we want it to be.

    if(gearSwitch.read() == LOW) { // if hardware switch is closed
        digitalWrite(LED_BUILTIN, HIGH);
        if (gearHandleDown == 0) { // and X-Plane's gear handle is up
          gearHandleDown = 1; // move it down
        }
      } else { // if the switch is open
        digitalWrite(LED_BUILTIN, LOW);
        if (gearHandleDown == 1) { // and gear handle is down
          gearHandleDown = 0; // move it up
        }
      }
    

    Look again at the TeensyControls communication screen. No more frantic repetition!

    You should also find that nothing else can move the gear handle. Press G, click on the handle – the Teensy moves it straight back again so it agrees with our toggle switch. We’ve just implemented what I call a blocking input. The Teensy is blocking out all the other inputs.

    The alternative is to set up as a sharing input. Here, we only update the dataref when we move the switch, but we ignore the dataref otherwise. This means other inputs (like the G key) can act on the dataref and not immediately get overridden by the Teensy.

      if (gearSwitch.fallingEdge() ) {
        digitalWrite(LED_BUILTIN, HIGH);
        gearHandleDown = 1;
      }
      if (gearSwitch.risingEdge() ) { 
        digitalWrite(LED_BUILTIN, LOW);
        gearHandleDown = 0;
      }
    

    The fallingEdge and risingEdge functions are pretty useful. When you call them, they return true, one time only, if the input has just changed. fallingEdge is true if the input’s just gone low, and risingEdge means it’s just gone high. In plain English, they mean ‘Has this input gone low (or high) since the last time I asked?’.

    We need one extra refinement though. Move the hardware switch down (so the orange light goes on), then unplug the Teensy. Move the gear lever up in the simulator, and plug the Teensy back in again. The gear lever is still up, but our hardware says it should be down. It will be tiresome to need to move every single switch up and down to check X-Plane matches its position!

    It turns out that Bounce has a function which solves this perfectly, called rebounce(int milliseconds). It triggers a false ‘I’ve just moved’ signal on the Bounce object at a number of milliseconds in the future. We can just slap it into the end of the setup function:

    void setup() {
      pinMode (gearSwitchPin, INPUT_PULLUP);
      pinMode (LED_BUILTIN, OUTPUT);
    
      gearHandleDown = XPlaneRef("sim/cockpit2/controls/gear_handle_down");
      gearSwitch.rebounce(2000);
    }
    

    and after 2 seconds (after everything is set up and running) either risingEdge or fallingEdge will be true. And the ordinary this-switch-just-moved code will send the correct number to the dataref. This is great news, we don’t need to repeat ourselves or do anything elaborate to make this special one-off update happen. If we change what happens when we move the switch normally, we’re also changing what happens when the Teensy board first connects!

    Let’s do exactly that. We’re writing to a dataref to move the gear. But it is, generally, better to use commands for input, if a suitable command is available. Teensyduino FlightSimCommand objects are very similar to FlightSimInteger and FlightSimFloat, except we can’t read them and we can only set them to 1 or 0, which represent Command Start and Command End. There’s also a .once() function, which combines Start and End so the command is only active momentarily. We can easily swap one FlightSimInteger for two FlightSimCommands, gearUp and gearDown, and call them .once() when the switch is moved. This is a non-blocking input, we’re only calling the commands when the switch moves.

    #include <Bounce.h>
    
    const int gearSwitchPin = 25;
    Bounce gearSwitch = Bounce (gearSwitchPin, 5);
    
    // Landing gear control commands
    FlightSimCommand gearUp;
    FlightSimCommand gearDown;
    
    void setup() {
      pinMode (gearSwitchPin, INPUT_PULLUP);
      pinMode (LED_BUILTIN, OUTPUT);
    
      gearUp = XPlaneRef("sim/flight_controls/landing_gear_up");
      gearDown = XPlaneRef("sim/flight_controls/landing_gear_down");
    
      gearSwitch.rebounce(2000);
    }
    
    void loop() {
      FlightSim.update();
      gearSwitch.update();
    
      if (gearSwitch.fallingEdge() ) {
        digitalWrite(LED_BUILTIN, HIGH);
        gearDown.once();
      }
      if (gearSwitch.risingEdge() ) { 
        digitalWrite(LED_BUILTIN, LOW);
        gearUp.once();
      }
    }
    

    The choice of blocking or sharing input is up to you. The advantage of a blocking input is that the position of your hardware switches is definitely the position of the simulated switches. The downside is you can’t use other inputs, and two blocking inputs on the same dataref will fight each other. I prefer blocking inputs, because I can reach a switch with my fingers and tell by touch what state the input is in. Your system may vary!

    One more thing to do today. Let’s combine the new input code with the LED-output code to have a complete gear panel, with a blocking-input gear position switch:

    #include <Bounce.h>
    
    ////////////////////////////////////////
    // Hardware input
    //
    const int gearSwitchPin = 25;
    Bounce gearSwitch = Bounce (gearSwitchPin, 5);
    
    ////////////////////////////////////////
    // Hardware output
    //
    const int greenLeftPin  = 10;
    const int greenNosePin  = 11;
    const int greenRightPin = 12;
    
    const int redLeftPin    = 20;
    const int redNosePin    = 19;
    const int redRightPin   = 18;
    
    ////////////////////////////////////////
    // X-Plane input and output
    //
    // Landing gear handle commands
    FlightSimCommand gearUp;
    FlightSimCommand gearDown;
    
    // Landing gear actual position 
    FlightSimFloat gearDeployLeft;
    FlightSimFloat gearDeployNose;
    FlightSimFloat gearDeployRight;
    
    // Landing gear handle position 
    FlightSimInteger gearHandleDown;
    
    // Aircraft essential bus voltage
    FlightSimFloat supplyVolts;
    
    void setup() {
    
      //////////////////
      // Input
      //
      pinMode (gearSwitchPin, INPUT_PULLUP);
    
      //////////////////
      // Output
      //
      pinMode(greenLeftPin,  OUTPUT);
      pinMode(greenNosePin,  OUTPUT);
      pinMode(greenRightPin, OUTPUT);
    
      pinMode(redLeftPin,    OUTPUT);
      pinMode(redNosePin,    OUTPUT);
      pinMode(redRightPin,   OUTPUT);
    
      //////////////////
      // X-Plane
      //
      gearUp          = XPlaneRef("sim/flight_controls/landing_gear_up");
      gearDown        = XPlaneRef("sim/flight_controls/landing_gear_down");
    
      gearDeployLeft  = XPlaneRef("sim/flightmodel2/gear/deploy_ratio[1]");
      gearDeployNose  = XPlaneRef("sim/flightmodel2/gear/deploy_ratio[0]");
      gearDeployRight = XPlaneRef("sim/flightmodel2/gear/deploy_ratio[2]");
    
      gearHandleDown  = XPlaneRef("sim/cockpit2/controls/gear_handle_down");  
    
      supplyVolts     = XPlaneRef("sim/cockpit2/electrical/bus_volts[0]");
    }
    
    void loop() {
    
      FlightSim.update();
      gearSwitch.update();
    
      //////////////////
      // Process input
      //
    
      // blocking input on gear handle position
      if(gearSwitch.read() == LOW) { // if the switch is closed
        if (gearHandleDown == 0) {   // if gear handle is up
          gearDown.once();           // move it down
        }
      } else {
        if (gearHandleDown == 1) {   // if gear handle is down
          gearUp.once();             // move it up
        }
      } // gearSwitch
    
      //////////////////
      // Process output
      //
    
      // we need 10V and the sim to be running to light the LEDs
      bool canLight = (supplyVolts > 10.0) && FlightSim.isEnabled();
    
      // light red LEDs if gear handle and position disagree and we have power
      digitalWrite(redLeftPin,  (gearHandleDown != gearDeployLeft)  && canLight);
      digitalWrite(redNosePin,  (gearHandleDown != gearDeployNose)  && canLight);
      digitalWrite(redRightPin, (gearHandleDown != gearDeployRight) && canLight);
    
      // light green LEDs if gear down and we have power
      digitalWrite(greenLeftPin,  (gearDeployLeft  == 1.0) && canLight);
      digitalWrite(greenNosePin,  (gearDeployNose  == 1.0) && canLight);
      digitalWrite(greenRightPin, (gearDeployRight == 1.0) && canLight);
    
    }
    

    I’ve changed the formatting a bit, and we don’t need the rebounce with this blocking implementation. Notice we’re checking the handle position using the dataref, then using a command to change the position if the it is wrong.

    That’s all for today! I’ll continue this series when the post arrives from ebay, and we’ll add a flaps gauge to the panel. Any suggestions or feedback, you can get in touch in the comments or through simulationelectronics@gmail.com.

    – Jack

    Gear panel tutorial, part 2 – Position Lights

    In part 1 we created the three green gear-down-and-locked indicator lights. In Part 2 we’ll add the red gear-out-of-position lights, which go on when the landing gear is not in the position selected by the gear lever, and do some refining. Not many images in this session, it’s mostly altering code.

    Let’s get started. Put three red LEDs onto the breadboard along with the green ones:

    Again we will test the hardware before adding any new logic. Add the red LEDs to the code and light them up alongside the green ones.

    const int greenLeftPin = 10;
    const int greenNosePin = 11;
    const int greenRightPin = 12;
    
    // our new LEDs
    const int redLeftPin = 20;
    const int redNosePin = 19;
    const int redRightPin = 18;
    
    // Landing gear position datarefs
    FlightSimFloat gearDeployLeft;
    FlightSimFloat gearDeployNose;
    FlightSimFloat gearDeployRight;
    
    void setup() {
      pinMode(greenLeftPin, OUTPUT);
      pinMode(greenNosePin, OUTPUT);
      pinMode(greenRightPin, OUTPUT);
    
      pinMode(redLeftPin, OUTPUT);
      pinMode(redNosePin, OUTPUT);
      pinMode(redRightPin, OUTPUT);
    
      gearDeployLeft = XPlaneRef("sim/flightmodel2/gear/deploy_ratio[1]");
      gearDeployLeft.onChange(updateGearDeployLeft);
      gearDeployNose = XPlaneRef("sim/flightmodel2/gear/deploy_ratio[0]");
      gearDeployNose.onChange(updateGearDeployNose);
      gearDeployRight = XPlaneRef("sim/flightmodel2/gear/deploy_ratio[2]");
      gearDeployRight.onChange(updateGearDeployRight);
    }
    
    void loop() {
      FlightSim.update();
    }
    
    void updateGearDeployLeft(float pos) {
      digitalWrite(greenLeftPin, (pos == 1.0));
      digitalWrite(redLeftPin, (pos == 1.0));
    }
    void updateGearDeployNose(float pos) {
      digitalWrite(greenNosePin, (pos == 1.0));
      digitalWrite(redNosePin, (pos == 1.0));
    }
    void updateGearDeployRight(float pos) {
      digitalWrite(greenRightPin, (pos == 1.0));
      digitalWrite(redRightPin, (pos == 1.0));
    }

    You should see each red LED lighting up alongside the green ones. It’s a simple way to check we’ve got the numbers right and the LEDs the right way round, connected to the right holes in the breadboard, etc.

    There’s no ‘gear out of position’ dataref, so we’ll need to write our own mini-simulator to decide when these LEDs should light. A brief search through DataRefEditor brings up sim/cockpit2/controls/gear_handle_down, which is 0 for Up and 1 for Down. Comparing gear_handle_down with gear_deploy_ratio should do it:

    ...
    FlightSimInteger gearHandleDown;
    
    void setup() {
      ...
      gearHandleDown = XPlaneRef("sim/cockpit2/controls/gear_handle_down");  
    }
    
    void loop() {
      FlightSim.update();
    
      // if gear handle doesn't match nose gear position
      if (gearHandleDown != gearDeployNose) {
        // light red nose LED (the Rudolph LED?)
        digitalWrite(redNosePin, HIGH);
      } else {
        // otherwise extinguish this LED
        digitalWrite(redNosePin, LOW);
      }
    }
    
    void updateGearDeployLeft(float pos) {
      digitalWrite(greenLeftPin, (pos == 1.0));
    }
    void updateGearDeployNose(float pos) {
      digitalWrite(greenNosePin, (pos == 1.0));
    }
    void updateGearDeployRight(float pos) {
      digitalWrite(greenRightPin, (pos == 1.0));
    }

    That works, but we can make it a bit less verbose:

      // light red nose LED if gear handle and nose gear disagree
      digitalWrite(redNosePin, (gearHandleDown != gearDeployNose));

    (gearHandleDown != gearDeployNose) is evaluated as a true/false condition by C++. If the two numbers are not exactly the same, we get a true result, which is equivalent to 1 or HIGH here, and so the LED will light. Otherwise we get false/zero/LOW and the light goes out. Of course the numbers are only the same when the gear’s fully up or down and and the handle is up or down in the same position.

    After testing it to see that it works, let’s extend this to all the red LEDs:

    void loop() {
      FlightSim.update();
    
      // light red LEDs if gear handle and gear position disagree
      digitalWrite(redLeftPin, (gearHandleDown != gearDeployLeft));
      digitalWrite(redNosePin, (gearHandleDown != gearDeployNose));
      digitalWrite(redRightPin, (gearHandleDown != gearDeployRight));
    }

    This is working, but not very sophisticated. What is supposed to be powering these indicators? In reality, they’re fed off your computer’s USB power supply, but in an aircraft, they’re fed from one of the electrical busses. We can use the sim/cockpit2/electrical/bus_volts[0] dataref – there’s no explicit ‘essential power bus’ dataref in default X-Plane.

    We’ll create a new FlightSimFloat for the voltage dataref – let’s call it supplyVolts. And let’s say that we need 10V to light our indicators. Now we could put && (supplyVolts > 10.0) into all of the lighting checks, but it is neater (and easier to amend) if we create a bool flag to store this information and then check against that. Let’s name it hasPower.

    // Aircraft essential bus voltage
    FlightSimFloat supplyVolts;
    
    // flag to show whether our indicators can light
    bool hasPower = false;
    
    void setup() {
      ...
      supplyVolts = XPlaneRef("sim/cockpit2/electrical/bus_volts[0]");
    }
    
    void loop() {
      FlightSim.update();
    
      // let's say we need 10V to run the lights
      hasPower = (supplyVolts > 10.0);
      //hasPower is true if we have at least 10V, otherwise it is false.
    
      // light red LEDs if gear handle and gear position disagree and we have power
      digitalWrite(redLeftPin, (gearHandleDown != gearDeployLeft) && hasPower);
      // test on only one light first
      digitalWrite(redNosePin, (gearHandleDown != gearDeployNose));
      digitalWrite(redRightPin, (gearHandleDown != gearDeployRight));
    }
    
    // light green LEDs if gear down and locked and we have power
    void updateGearDeployLeft(float pos) {
      digitalWrite(greenLeftPin, (pos == 1.0) && hasPower);
    }
    ...

    Turn off the engines and the battery supply, and… uh-oh! The red LED is behaving correctly, not lighting if the voltage is low, but the green LED isn’t lighting at all.

    I scratched my head for a little while before seeing the reason: updateGearDeployLeft only runs when the gear position changes. If the supply voltage drops while the LED is lit, the LED stays lit until the gear position changes.

    We could call the updateGearDeploy functions explicitly whenever supplyVolts passes 10V, or we could bring the green LED lighting code up into the loop() function. Both would work. I’ll choose the second option; it will make the green LED code consistent with the red LEDs. It’s a matter of taste, but I think it also makes the code simpler.

    // I haven't shown it, but you should remove
    // the onChange lines from setup() too.
    ...
    void loop() {
      FlightSim.update();
    
      // let's say we need 10V to run the lights
      hasPower = (supplyVolts > 10.0);
    
      // light red LEDs if gear handle and position disagree and we have power
      digitalWrite(redLeftPin,  (gearHandleDown != gearDeployLeft)  && hasPower);
      digitalWrite(redNosePin,  (gearHandleDown != gearDeployNose)  && hasPower);
      digitalWrite(redRightPin, (gearHandleDown != gearDeployRight) && hasPower);
    
      // light green LEDs if gear down and we have power
      digitalWrite(greenLeftPin,  (gearDeployLeft == 1.0)  && hasPower);
      digitalWrite(greenNosePin,  (gearDeployNose == 1.0)  && hasPower);
      digitalWrite(greenRightPin, (gearDeployRight == 1.0) && hasPower);
    }

    One last refinement. Let’s make the lights always extinguish when X-Plane isn’t running. We can do this with the FlightSim.isEnabled() function. It is true when X-Plane’s running and ready to talk to the Teensy boards, and false otherwise.

    We might as well use hasPower for this as well as the electrical power:

    hasPower = (supplyVolts > 10.0) && FlightSim.isEnabled();

    But now the name’s misleading. If you’re using Qt Creator, you can right-click on hasPower and use Refactor >> Rename Symbol Under Cursor to change the name to canLight intelligently. If you’re using the Arduino IDE to edit your code, change the name manually or with Find/Replace. Then read my guide to using a sensible IDE for writing Arduino/Teensy code.

    Here’s the complete code listing:

    const int greenLeftPin  = 10;
    const int greenNosePin  = 11;
    const int greenRightPin = 12;
    
    const int redLeftPin    = 20;
    const int redNosePin    = 19;
    const int redRightPin   = 18;
    
    // Landing gear position datarefs
    FlightSimFloat gearDeployLeft;
    FlightSimFloat gearDeployNose;
    FlightSimFloat gearDeployRight;
    
    // Landing gear control dataref
    FlightSimInteger gearHandleDown;
    
    // Aircraft essential bus voltage
    FlightSimFloat supplyVolts;
    
    // flag to show whether our indicators can light
    bool canLight = false;
    
    void setup() {
      pinMode(greenLeftPin,  OUTPUT);
      pinMode(greenNosePin,  OUTPUT);
      pinMode(greenRightPin, OUTPUT);
    
      pinMode(redLeftPin,    OUTPUT);
      pinMode(redNosePin,    OUTPUT);
      pinMode(redRightPin,   OUTPUT);
    
      gearDeployLeft =  XPlaneRef("sim/flightmodel2/gear/deploy_ratio[1]");
      gearDeployNose =  XPlaneRef("sim/flightmodel2/gear/deploy_ratio[0]");
      gearDeployRight = XPlaneRef("sim/flightmodel2/gear/deploy_ratio[2]");
    
      gearHandleDown =  XPlaneRef("sim/cockpit2/controls/gear_handle_down");  
    
      supplyVolts =     XPlaneRef("sim/cockpit2/electrical/bus_volts[0]");
    }
    
    void loop() {
      FlightSim.update();
    
      // we need 10V and the sim to be running to light the LEDs
      canLight = (supplyVolts > 10.0) && FlightSim.isEnabled();
    
      // light red LEDs if gear handle and position disagree and we have power
      digitalWrite(redLeftPin,  (gearHandleDown != gearDeployLeft)  && canLight);
      digitalWrite(redNosePin,  (gearHandleDown != gearDeployNose)  && canLight);
      digitalWrite(redRightPin, (gearHandleDown != gearDeployRight) && canLight);
    
      // light green LEDs if gear down and we have power
      digitalWrite(greenLeftPin,  (gearDeployLeft  == 1.0) && canLight);
      digitalWrite(greenNosePin,  (gearDeployNose  == 1.0) && canLight);
      digitalWrite(greenRightPin, (gearDeployRight == 1.0) && canLight);
    }
    

    With all these changes, we now have a complete set of gear indicator lights, which don’t light when the sim’s not running or when the aircraft has no power. Next time, we’ll add a gear lever!

    – Jack 25/Oct/2012