Wednesday, March 19, 2014

RC navigation lights

Someone on LinkedIn asked me for some help with some lights on his RC plane. Since I had an evening to spare anyway I was happy to help. There were just a few requirements:
  • Use an Arduino Nano
  • Control 5 LEDs
  • Each LED should be controllable using just one switch on a remote control
  • Flipping the switch X times will toggle LED X
That's just your basic programmable RC navigation/landing light system you'll find on quite a few scale model airplanes and helicopters. You can find these kind of systems in most stores that sell RC equipment, some for as little as $12, but most for $25 and up (and up up up). But where's the fun in that?

So after a bit of programming and testing this is what I came up with:
Total cost: less than $10. You can pick up a Nano on eBay for as little as $6 and resistors and LEDs should be in your parts bin anyway ;)

Here's the breadboard layout (doesn't match the one from the movie exactly, but you get the idea)
 The servo represents the receiver, I used it since it has the same connectors (signal, 5V, gnd)

And the schematics:

And of course, the code!

Let's cover the reading of the switch on the remote first. By connecting the signal wire (usually the white one) from the receiver to pin 9 on the Arduino we can read the state of the switch. There will be a pulse coming from the receiver about 50 times per second. This pulse will be between 1000 and 2000 microseconds in length depending on the position of the switch. We can assume that anything above 1500 is the up position, and below 1500 the down position. So it's simply a matter of detecting the pulse and measuring its length. Normally I'd use interrupts to handle this, but I wanted to keep this one simple so I used the pulseIn() function instead.

Let's define a few variables first:


1:  uint8_t cfg_inputPin;  
2:  bool     loop_lastSwitchPosition;  
3:  uint8_t    loop_switchCount;  
4:  unsigned long loop_lastSwitchTime;  
cfg_inputPin holds which pin the receiver is connected to. loop_lastSwitchPosition holds the last read state of the switch, loop_switchCount holds how often it has been flipped up and loop_lastSwitchTime holds when the switch was flipped up.

Now for the setup:
1:  void setup()  
2:  {  
3:      cfg_inputPin = 9;  
4:    
5:      loop_lastSwitchPosition = pulseIn(cfg_inputPin, HIGH) > 1500;  
6:  }  

First we set the input pin. Pins are already in INPUT mode initially so no need to set that here. Furthermore we initialize the loop variable here with some default value.

1:  void loop()  
2:  {  
3:      unsigned long duration = pulseIn(cfg_inputPin, HIGH);  
4:      while ( duration == 0 )  
5:      {  
6:          duration = pulseIn(cfg_inputPin, HIGH);  
7:      }  
8:        
9:      bool newSwitchPosition = duration > 1500;  
10:      if ( newSwitchPosition != loop_lastSwitchPosition )  
11:      {  
12:          loop_lastSwitchPosition = newSwitchPosition;  
13:          if ( newSwitchPosition )  
14:          {  
15:              ++loop_switchCount;  
16:              loop_lastSwitchTime = millis();  
17:          }  
18:      }  
19:      else  
20:      {  
21:          if ( loop_lastSwitchTime != 0 && millis() - loop_lastSwitchTime > 1000 )  
22:          {  
23:              uint8_t channel = loop_switchCount - 1;  
24:              if ( channel < CHANNELS )  
25:              {  
26:                  // do something  
27:              }  
28:              loop_switchCount = 0;  
29:              loop_lastSwitchTime = 0;  
30:          }  
31:      }  
32:  }  
That's more like it :)
In lines 3-7 we wait for a valid pulse.
Line 9 converts the pulse duration to a switch position.
Line 10 checks if there's a change in switch position since the last loop. If so, we update the stored switch position on line 12.
Lines 13-17 handle the transition of the switch from down to up. If that happens we increment the switch counter and update the "last switch" timestamp.
The rest of the lines handle the situation where the switch did not change.
Line 21 checks if there was a switch change in one of the previous loops (loop_lastSwitchTime != 0) and if more than a second has passed since. So this expression will evaluate to true when you've flipped the switch up a second ago. That basically means you've stopped flipping the switch.
Lines 23 to 29 convert the switch counter to a channel index, check if that's not out of bounds (flipped the switch more than there are channels to control) and line 26 is where you put your actual code. The remaining two lines reset the loop variables.

Let's add some LEDs!
1:  #define CHANNELS 5  
2:  uint8_t cfg_pins[CHANNELS];  
3:  bool   state_switch[CHANNELS];  
4:    
5:  void setChannelPin(uint8_t p_channel, uint8_t p_pin)  
6:  {  
7:      digitalWrite(p_pin, LOW);  
8:      pinMode(p_pin, OUTPUT);  
9:      cfg_pins[p_channel] = p_pin;  
10:  }  
11:    
12:  void setChannelSwitch(uint8_t p_channel, bool p_on)  
13:  {  
14:      state_switch[p_channel] = p_on;  
15:      if ( p_on == false )  
16:      {  
17:          digitalWrite(cfg_pins[p_channel], LOW);  
18:      }  
19:      else  
20:      {  
21:          digitalWrite(cfg_pins[p_channel], HIGH);  
22:      }  
23:  }  
24:    
25:    
26:  void toggleChannelSwitch(uint8_t p_channel)  
27:  {  
28:      setChannelSwitch(p_channel, !state_switch[p_channel]);  
29:  }  
30:    
The define is used to define how many channels we'll be able to control. cfg_pins holds the pin for each channel. state_switch holds the state of the "virtual" switch for each channel. setChannelPin can be used to configure the pin for a channel. It'll set the pin in output mode and make sure it's off. The setChannelSwitch function allows you to set the state of the "virtual" switch for the channel and turns the LED on or off. toggleChannelSwitch toggles the state of the switch.

Now we need to update our setup function and add a few lines:
1:      setChannelPin(0, 4);  
2:      setChannelPin(1, 5);  
3:      setChannelPin(2, 6);  
4:      setChannelPin(3, 7);  
5:      setChannelPin(4, 8);  
So our five channels are connected to pins 4 - 8.

In our loop function we need to replace the "// do something " comment with
1:  toggleChannelSwitch( channel );  

Tadaaah! We can now turn channels on and off with the switch on the remote.

Hmm, that's a bit boring though... Let's add some blinking and flashing! For this we need some sort of update function that updates all the "animations". We need to keep track of time as well. The easiest way to handle this is by giving the update function a parameter that holds the number of milliseconds passed since the last update.

1:  unsigned long loop_lastMillis;  
2:    
3:  void setup()  
4:  {  
5:      // previous code here  
6:      loop_lastMillis = millis();  
7:  }  
8:    
9:  void loop()  
10:  {  
11:      // previous code here  
12:    
13:      unsigned long now = millis();  
14:      uint16_t deltaMillis = now - loop_lastMillis);  
15:      loop_lastMillis = now;  
16:    
17:      if ( deltaMillis > 0 )  
18:      {  
19:          update( deltaMillis );  
20:      }  
21:  }  
22:    
23:  void update(uint16_t p_delta)  
24:  {  
25:      // animation code here  
26:  }  
Pretty self explanatory I guess? We calculate the difference between the current and last timestamp and pass that to the update function.

For the animations I want to be able to specify how often an LED should blink, how long it should blink, how much time should be between blinks and how much time should be between the last blink in a sequence and the first one of the next sequence. I want to be able to create a strobe that flashes two times rapidly and then waits for a second or so, but I also want to be able to have a LED that blinks at a constant rate. And of course, I want some LEDs to be on all of the time.

We'll need a few more variables...
1:  uint8_t cfg_blinks[CHANNELS];  
2:  uint16_t cfg_onDuration[CHANNELS];  
3:  uint16_t cfg_offDuration[CHANNELS];  
4:  uint16_t cfg_pauseDuration[CHANNELS];  
5:    
6:  bool   state_onOff[CHANNELS];  
7:  uint8_t state_currentBlink[CHANNELS];  
8:  uint16_t state_nextChange[CHANNELS];  
So we have our variable that specify how often a channel should blink (0 being constantly on), how long it should be on, how long it should be off between blinks and how long it should pause between blink sequences.
Then we have our variable to hold the state of the channels while the animation is running. We need to know whether it's currently on or off, which blink we're at and how many milliseconds remain until the next state change.

We'll want some convenience functions to configure the channels:
1:  void configureChannelBlink(uint8_t p_channel, uint8_t p_blinks, uint16_t p_onDuration, uint16_t p_offDuration, uint16_t p_pauseDuration)  
2:  {  
3:      cfg_blinks[p_channel]    = p_blinks;  
4:      cfg_onDuration[p_channel]  = p_onDuration;  
5:      cfg_offDuration[p_channel]  = p_offDuration;  
6:      cfg_pauseDuration[p_channel] = p_pauseDuration;  
7:      setChannelSwitch(p_channel, true);  
8:  }  
9:    
10:    
11:  void configureChannelConstant(uint8_t p_channel)  
12:  {  
13:      cfg_blinks[p_channel] = 0;  
14:      setChannelSwitch(p_channel, true);  
15:  }  
And we can call this from our setup function:
1:      configureChannelConstant(0);  
2:      configureChannelConstant(1);  
3:      configureChannelConstant(2);  
4:        
5:      configureChannelBlink(3, 1, 50, 0, 950);  
6:      configureChannelBlink(4, 2, 50, 50, 1850);  
So channels 0-2 are constantly on, channel 3 blinks constantly and channel 4 is a strobe (double flash).

We need to update our setChannelSwitch function so it will reset the animation when the channel is turned on:
1:  void setChannelSwitch(uint8_t p_channel, bool p_on)  
2:  {  
3:      state_switch[p_channel] = p_on;  
4:      if ( p_on == false )  
5:      {  
6:          digitalWrite(cfg_pins[p_channel], LOW);  
7:      }  
8:      else  
9:      {  
10:          state_onOff[p_channel]    = true;  
11:          state_currentBlink[p_channel] = 0;  
12:          state_nextChange[p_channel]  = cfg_onDuration[p_channel];  
13:          digitalWrite(cfg_pins[p_channel], HIGH);  
14:      }  
15:  }  
Lines 10-12 are new.

And lastly, our blinky code:
1:  void update(uint16_t p_delta)  
2:  {  
3:      for ( uint8_t channel = 0; channel < CHANNELS; ++channel )  
4:      {  
5:          if ( cfg_blinks[channel] == 0 )  
6:          {  
7:              continue;  
8:          }  
9:          if ( state_switch[channel] == false )  
10:          {  
11:              continue;  
12:          }  
13:            
14:          if ( p_delta >= state_nextChange[channel] )  
15:          {  
16:              state_onOff[channel] = ! state_onOff[channel];  
17:                
18:              if ( state_onOff[channel] )  
19:              {  
20:                  state_nextChange[channel] = cfg_onDuration[channel];  
21:                  digitalWrite(cfg_pins[channel], HIGH);  
22:              }  
23:              else  
24:              {  
25:                  state_currentBlink[channel]++;  
26:                  if ( state_currentBlink[channel] == cfg_blinks[channel] )  
27:                  {  
28:                      state_nextChange[channel] = cfg_pauseDuration[channel];  
29:                      state_currentBlink[channel] = 0;  
30:                  }  
31:                  else  
32:                  {  
33:                      state_nextChange[channel] = cfg_offDuration[channel];  
34:                  }  
35:                  digitalWrite(cfg_pins[channel], LOW);  
36:              }  
37:          }  
38:          else  
39:          {  
40:              state_nextChange[channel] = state_nextChange[channel] - p_delta;  
41:          }  
42:      }  
43:  }  
Lines 5-8 test if the channel should be constantly on, if so then we skip the rest of the loop and move to the next channel
Lines 9-12 test if the channel is switched off, if so we can again skip this channel.
Line 14 checks whether it's time to toggle the channel.
Line 15 toggles the state of the channel; from on to off and vice versa.
Lines 18-22 turn the LED on and update the "next change" counter.
Lines 23-37 turn the LED off, increment the blink counter and check if this was the last blink. If so then we use the pauseDuration, else we use the offDuration. Don't forget to reset the blink counter ( line 29).
Lines 38-41 handle the situation where a channel does not need to change state yet, all we need to do is decrement the amount of time remaining until the next state change.

That's all there's to it.
Here's the final complete code for your convenience:
1:  // NavLights.ino  
2:  // Arduino based remote controlled navigation lights  
3:  //  
4:  // By Daniel van den Ouden, 2014  
5:  //  
6:    
7:  // configuration for input  
8:  uint8_t cfg_inputPin;  
9:    
10:  // 5 output channels  
11:  #define CHANNELS 5  
12:    
13:  uint8_t cfg_blinks[CHANNELS];            // how often a channel will blink in a sequence, 8 bit, so value is 0 - 255, 0 means no blinking  
14:  uint16_t cfg_onDuration[CHANNELS];        // how long the channel will be on in milliseconds, 16 bit, so value is 0 - 65535  
15:  uint16_t cfg_offDuration[CHANNELS];        // how long the channel will be off between blinks in milliseconds, 16 bit  
16:  uint16_t cfg_pauseDuration[CHANNELS];    // how long the channel will be off between the last and first blink of a sequence, 16 bit  
17:  uint8_t cfg_pins[CHANNELS];            // which pin to use for which channel  
18:    
19:  bool   state_switch[CHANNELS];        // state of the channel's "switch" (true = on, false = off)  
20:  bool   state_onOff[CHANNELS];            // the output state of the channel (true = high, false = low)  
21:  uint8_t state_currentBlink[CHANNELS];    // how often this channel has blinked in the current sequence  
22:  uint16_t state_nextChange[CHANNELS];    // number of milliseconds until next change  
23:    
24:  unsigned long loop_lastMillis;            // time of last update  
25:  bool     loop_lastSwitchPosition;    // last measured switch position  
26:  uint8_t    loop_switchCount;            // number of switches in current sequence  
27:  unsigned long loop_lastSwitchTime;        // time at which the switch was flipped last  
28:    
29:  void setChannelPin(uint8_t p_channel, uint8_t p_pin)  
30:  {  
31:      digitalWrite(p_pin, LOW);  
32:      pinMode(p_pin, OUTPUT);  
33:      cfg_pins[p_channel] = p_pin;  
34:  }  
35:    
36:    
37:  void configureChannelBlink(uint8_t p_channel, uint8_t p_blinks, uint16_t p_onDuration, uint16_t p_offDuration, uint16_t p_pauseDuration)  
38:  {  
39:      cfg_blinks[p_channel]    = p_blinks;  
40:      cfg_onDuration[p_channel]  = p_onDuration;  
41:      cfg_offDuration[p_channel]  = p_offDuration;  
42:      cfg_pauseDuration[p_channel] = p_pauseDuration;  
43:      setChannelSwitch(p_channel, true);  
44:  }  
45:    
46:    
47:  void configureChannelConstant(uint8_t p_channel)  
48:  {  
49:      cfg_blinks[p_channel] = 0;  
50:      setChannelSwitch(p_channel, true);  
51:  }  
52:    
53:    
54:  void setup()  
55:  {  
56:      cfg_inputPin = 9;  
57:        
58:      setChannelPin(0, 4);  
59:      setChannelPin(1, 5);  
60:      setChannelPin(2, 6);  
61:      setChannelPin(3, 7);  
62:      setChannelPin(4, 8);  
63:        
64:      configureChannelConstant(0);  
65:      configureChannelConstant(1);  
66:      configureChannelConstant(2);  
67:        
68:      configureChannelBlink(3, 1, 50, 0, 950);  
69:      configureChannelBlink(4, 2, 50, 50, 1850);  
70:        
71:      loop_lastMillis = millis();  
72:      loop_lastSwitchPosition = pulseIn(cfg_inputPin, HIGH) > 1500;  
73:  }  
74:    
75:    
76:  void loop()  
77:  {  
78:      unsigned long duration = pulseIn(cfg_inputPin, HIGH);  
79:      while ( duration == 0 )  
80:      {  
81:          duration = pulseIn(cfg_inputPin, HIGH);  
82:      }  
83:        
84:      bool newSwitchPosition = duration > 1500;  
85:      if ( newSwitchPosition != loop_lastSwitchPosition )  
86:      {  
87:          loop_lastSwitchPosition = newSwitchPosition;  
88:          if ( newSwitchPosition )  
89:          {  
90:              ++loop_switchCount;  
91:              loop_lastSwitchTime = millis();  
92:          }  
93:      }  
94:      else  
95:      {  
96:          if ( loop_lastSwitchTime != 0 && millis() - loop_lastSwitchTime > 1000 )  
97:          {  
98:              uint8_t channel = loop_switchCount - 1;  
99:              if ( channel < CHANNELS )  
100:              {  
101:                  toggleChannelSwitch( channel );  
102:              }  
103:              loop_switchCount = 0;  
104:              loop_lastSwitchTime = 0;  
105:          }  
106:      }  
107:        
108:      unsigned long now = millis();  
109:      uint16_t deltaMillis = now - loop_lastMillis;  
110:      loop_lastMillis = now;  
111:        
112:      if ( deltaMillis > 0 )  
113:      {  
114:          update( deltaMillis );  
115:      }  
116:  }  
117:    
118:    
119:  void update(uint16_t p_delta)  
120:  {  
121:      for ( uint8_t channel = 0; channel < CHANNELS; ++channel )  
122:      {  
123:          if ( cfg_blinks[channel] == 0 )  
124:          {  
125:              continue;  
126:          }  
127:          if ( state_switch[channel] == false )  
128:          {  
129:              continue;  
130:          }  
131:            
132:          if ( p_delta >= state_nextChange[channel] )  
133:          {  
134:              state_onOff[channel] = ! state_onOff[channel];  
135:                
136:              if ( state_onOff[channel] )  
137:              {  
138:                  state_nextChange[channel] = cfg_onDuration[channel];  
139:                  digitalWrite(cfg_pins[channel], HIGH);  
140:              }  
141:              else  
142:              {  
143:                  state_currentBlink[channel]++;  
144:                  if ( state_currentBlink[channel] == cfg_blinks[channel] )  
145:                  {  
146:                      state_nextChange[channel] = cfg_pauseDuration[channel];  
147:                      state_currentBlink[channel] = 0;  
148:                  }  
149:                  else  
150:                  {  
151:                      state_nextChange[channel] = cfg_offDuration[channel];  
152:                  }  
153:                  digitalWrite(cfg_pins[channel], LOW);  
154:              }  
155:          }  
156:          else  
157:          {  
158:              state_nextChange[channel] = state_nextChange[channel] - p_delta;  
159:          }  
160:      }  
161:  }  
162:    
163:    
164:  void setChannelSwitch(uint8_t p_channel, bool p_on)  
165:  {  
166:      state_switch[p_channel] = p_on;  
167:      if ( p_on == false )  
168:      {  
169:          digitalWrite(cfg_pins[p_channel], LOW);  
170:      }  
171:      else  
172:      {  
173:          state_onOff[p_channel]    = true;  
174:          state_currentBlink[p_channel] = 0;  
175:          state_nextChange[p_channel]  = cfg_onDuration[p_channel];  
176:          digitalWrite(cfg_pins[p_channel], HIGH);  
177:      }  
178:  }  
179:    
180:    
181:  void toggleChannelSwitch(uint8_t p_channel)  
182:  {  
183:      setChannelSwitch(p_channel, !state_switch[p_channel]);  
184:  }  
185:    
186:    

First!

I needed some way to share some of my smaller (Arduino based) projects, so here it is :)