Hacked a stereo to use face plate

This post first describes the stereo I hacked and provides some classes written to help use the hacked hardware. The followup post is the complete code and some related notes. Feedback welcome.

Overview
I needed an IR Receiver and Radio Shack was fresh out and I did not want to wait for delivery. I had an old stereo that I was about to junk, so I pulled the IR Receiver out and it worked great.

Wow, look at all the stuff...
While in there, I noticed the volume knob (really nice rotary encoder) was on a break out board with some buttons.

I had not used a rotary encoder before so now was my chance. Using this link (Arduino Playground - RotaryEncoders) I had the rotary encoder working in no time. With some forum help I was able to get the three buttons working as well. These are buttons connected by different resistor levels so a single analog pin can read the "bank" of buttons - a very common approach for button reading.

Next I noticed the huge button panel for the front face and another cool rotary encoder were setup as pretty much the same animal. After following the super highway around the board I was able to determine which buttons where controlled by which output and playing told me which wires controlled the second rotary encoder.

So with control of all the buttons, both rotary encoders and even the illuminated power button led it was time to get the code working.

I am an application architect but C++ is my weakest language, so there is my disclaimer. :slight_smile:

First I had to create some classes to get the resistor button arrays and rotary encoder processing black boxed.

Analog Button Controller
The concept here is that multiple sets of buttons can be read by a separate analog inputs and can work either in tandem or stand alone. I do not want to use the memory to store values in an array, so I want to use code to determine the button value instead of a commonly used array look up table. For this reason I use a callback function to return the button based on the analog value. A callback is also used when a valid button is pressed. This allows the code to put into a library and only the base mechanism used.

class AnalogButtonController {
private:
  byte btnPressed;
  byte lastButtonPressed;
  byte btnPressedCache;

public:
  byte btnPin;
  void (*cmdButtonPressed)(int button);
  int (*cmdGetButtonValue)(int virtualValue);

  AnalogButtonController(void (*buttonPressedCallback)(int button), int (*getButtonValue)(int virtualValue), byte thePin) {
    this->cmdButtonPressed = buttonPressedCallback;
    this->cmdGetButtonValue = getButtonValue;
    btnPressed = 0;
    btnPressedCache = 0;
    lastButtonPressed = 0;
    btnPin = thePin;
  };

  ~AnalogButtonController() {
  };

  void checkButton(){
    int tmpVal = cmdGetButtonValue(analogRead(btnPin));
    //--- if an invalid range is returned then ignore it to not cause button to multi-click
    if( tmpVal < 0 ) return;
      
    if (tmpVal == btnPressedCache){
       btnPressed = tmpVal;
    } else {
       btnPressedCache = tmpVal;
    } 
  }

  void runButtonPress(){
    if( lastButtonPressed == btnPressed) return;
    lastButtonPressed = btnPressed;
    if( btnPressed == 0) return;
    cmdButtonPressed(btnPressed);
  }

};

Discovery Phase
The values from the analog inputs jump around some when pressed. So the first thing I needed to know what what range to look for to determine what button is pressed. I created this simple program which allowed me to hold down a button for a bit, then let up. It would tell me the high and low .. the buttons voltage range. Using the results allows me to code the callback routines.

Discovery Code:

/* Get Analog Value Range */

void setup()
{
  Serial.begin(9600);
}

boolean inDown = false;
int minVal = 20000;
int maxVal = 0;

void loop()
{
  int tmpVal = analogRead(5);
  if (tmpVal < 1000){
    if( tmpVal < minVal)
      minVal = tmpVal;
    if( tmpVal > maxVal)
      maxVal = tmpVal;
    inDown = true;
  } else {
    if (inDown){
      Serial.println(minVal, DEC);
      Serial.println(maxVal, DEC);
      Serial.println("===");
      minVal = 20000;
      maxVal = 0;
    }
    inDown = false;
  }
  delay(50);  //maybe remove this

Rotary Encoder Controller
The concept here is a Rotary Encoder can be set up with a range and optionally a tick amount and direction orientation. Then when the dial changes, resulting an actual change due to not being stopped at max or min, a callback is called.

class RotaryEncoderRangeController {
private:
  boolean encoderForward;
  int encoderPos;
  int encoderPinALast;
  int encoderMin;
  int encoderMax;
  byte encoderPinA;
  byte encoderPinB;

public:
  byte encoderIncr;
  
  void (*cmdValueChanged)(int newValue);

  RotaryEncoderRangeController(void (*valueChangedCallback)(int newValue), byte thePinA, byte thePinB) {
    this->cmdValueChanged = valueChangedCallback;
    encoderPinA = thePinA;
    encoderPinB = thePinB;
  };

  ~RotaryEncoderRangeController() {
  };

  void begin(){
    encoderForward = true;
    encoderIncr = 1;
    encoderPos = 0;
    encoderPinALast = LOW;
    pinMode (encoderPinA,INPUT);
    pinMode (encoderPinB,INPUT);
    encoderMin = -30000;
    encoderMax = 30000;
  }

  void begin(int theMinValue, int theMaxValue, int theCurrentVal, byte theIncrement){
     begin();
     setRange(theMinValue,theMaxValue,theCurrentVal,theIncrement);
  }
  void begin(int theMinValue, int theMaxValue, int theCurrentVal, byte theIncrement, boolean theIsForward){
     begin();
     setRange(theMinValue,theMaxValue,theCurrentVal,theIncrement,theIsForward);
  }
  
  void setRange(int theMinValue, int theMaxValue, int theCurrentVal, byte theIncrement){
    setRange(theMinValue,theMaxValue,theCurrentVal);
    setIncrement(theIncrement);
  }
  void setRange(int theMinValue, int theMaxValue, int theCurrentVal, byte theIncrement, boolean theIsForward){
    setRange(theMinValue,theMaxValue,theCurrentVal, theIncrement);
    encoderForward = theIsForward;
  }

  void setRange(int theMinValue, int theMaxValue, int theCurrentVal){
    encoderMin = theMinValue;
    encoderMax = theMaxValue;
    setPos(theCurrentVal);
  }

  void setRange(int theMinValue, int theMaxValue){
    encoderMin = theMinValue;
    encoderMax = theMaxValue;
    //--- to make sure in range
    encoderPos = setPos(encoderPos);
  }

  int setIncrement(byte theIncrement){
   encoderIncr = theIncrement;
  }
  
  int setPos(int theNewPos){
    int tmpVal = theNewPos;
    if (tmpVal < encoderMin)
      tmpVal = encoderMin;
    if (tmpVal > encoderMax)
      tmpVal = encoderMax;

          
    if (tmpVal != encoderPos){
      encoderPos = tmpVal;
      cmdValueChanged(encoderPos);
    }


    return encoderPos;
  }

  int getPos(){
    int tmpVal = encoderPos;
    if (tmpVal < encoderMin)
      tmpVal = encoderMin;
    if (tmpVal > encoderMax)
      tmpVal = encoderMax;
    return tmpVal;
  }

  void check(){
    int tmpV = digitalRead(encoderPinA);
    int tmpNew = tmpV;
    int tmpCurr = encoderPos;
    if ((encoderPinALast == LOW) && (tmpV == HIGH)) {
      
      if (digitalRead(encoderPinB) == LOW) {
        if( encoderForward ){
          tmpCurr += encoderIncr;
        } else {
          tmpCurr -= encoderIncr;
        }
      } 
      else {
        if( encoderForward ){
          tmpCurr -= encoderIncr;
        } else {
          tmpCurr += encoderIncr;
        }
      }
      tmpNew = setPos(tmpCurr);
    } 
    encoderPinALast = tmpNew;
  }

};

Full Demo
This code reads all 24 buttons on the main panel (1-24), the power and mute buttons (35,34), the three buttons around the volume knob (31,32,33) and the button in the center of the rotary encoder labeled set (30). Also the small rotary encoder is re1 and the larger volume knob is re2. This demo simply displays the button pressed or new value of a knob turn via the serial port.

This uses timer2 to allow for background reading of the values to not effect the loop. You can move this to any timer / fast read iteration.

*** Include - code from class AnalogButtonController here
*** Include - code from class RotaryEncoderRangeController here

//====================================

#include <MsTimer2.h>

//----------------------------
// Rotary Encoder Setup
//----------------------------

//--- create callback functions for Rotary encoder
void valueChangedForRE1(int theNewValue){
  Serial.print("RE1 ");
  Serial.println(theNewValue, DEC);
}
void valueChangedForRE2(int theNewValue){
  Serial.print("RE2 ");
  Serial.println(theNewValue, DEC);
}

//--- create a new Rotary encoder controller for RE1 using digital pins
RotaryEncoderRangeController re1 = RotaryEncoderRangeController(&valueChangedForRE2, 8, 9);
RotaryEncoderRangeController re2 = RotaryEncoderRangeController(&valueChangedForRE1, 6, 7);

//----------------------------
// Button Set Setup
//----------------------------
//--- create callback functions for button group
boolean tmpStatus = true;
void buttonPressed(int theButton){
  Serial.print("Button ");
  Serial.println(theButton, DEC);
}

int getButtonFromValueSet1(int theVal){
if( theVal > 1010 ) return 0;

if( theVal <= 930 && theVal >= 920 ) return 5;
if( theVal <= 950 && theVal >= 942 ) return 6;
if( theVal <= 970 && theVal >= 964 ) return 7;
if( theVal <= 976 && theVal >= 972 ) return 8;
if( theVal <= 200 && theVal >= 2 ) return 13;
if( theVal <= 877 && theVal >= 870 ) return 14;
if( theVal <= 965 && theVal >= 953 ) return 15;
if( theVal <= 1000 && theVal >= 992 ) return 31;
if( theVal <= 992 && theVal >= 984 ) return 32;
if( theVal <= 982 && theVal >= 978 ) return 33;

  return -1;
}

int getButtonFromValueSet2(int theVal){
if( theVal > 1010 ) return 0;


if( theVal <= 980 && theVal >= 974 ) return 1;
if( theVal <= 972 && theVal >= 969 ) return 2;
if( theVal <= 967 && theVal >= 962 ) return 3;
if( theVal <= 960 && theVal >= 951 ) return 4;
if( theVal <= 105 && theVal >= 0 ) return 9;
if( theVal <= 873 && theVal >= 868 ) return 10;
if( theVal <= 926 && theVal >= 919 ) return 11;
if( theVal <= 947 && theVal >= 939 ) return 12;
if( theVal <= 988 && theVal >= 982 ) return 34;
if( theVal <= 996 && theVal >= 990 ) return 35;

  return -1;
}

int getButtonFromValueSet3(int theVal){
if( theVal > 1010 ) return 0;


if( theVal <= 880 && theVal >= 870 ) return 17;
if( theVal <= 928 && theVal >= 920 ) return 18;
if( theVal <= 964 && theVal >= 960 ) return 19;
if( theVal <= 974 && theVal >= 968 ) return 20;
if( theVal <= 200 && theVal >= 0 ) return 21;
if( theVal <= 949 && theVal >= 941 ) return 22;
if( theVal <= 961 && theVal >= 954 ) return 23;
if( theVal <= 982 && theVal >= 977 ) return 24;
if( theVal <= 997 && theVal >= 989 ) return 16;
if( theVal <= 991 && theVal >= 984 ) return 30;

  return -1;
}


//--- Create three sets of button controlles that all read different values but call the same callback function
//-- Example: The first one ..
//      calls buttonPressed when button pressed, 
//      gets button from analog value from getButtonFromValueSet1 function 
//      reads analog pin 3 
AnalogButtonController set1 = AnalogButtonController(&buttonPressed, &getButtonFromValueSet1, 3);
AnalogButtonController set2 = AnalogButtonController(&buttonPressed, &getButtonFromValueSet2, 4);
AnalogButtonController set3 = AnalogButtonController(&buttonPressed, &getButtonFromValueSet3, 5);

//----------------------------


void processTimer2(){
  re1.check(); 
  re2.check(); 
  set1.checkButton(); 
  set2.checkButton(); 
  set3.checkButton(); 
}

void setup(){
  Serial.begin(19200);

  //--- Range from 1 to 256 with tick of 1 and step of 5
  re1.begin(1,256,1,5);
  //--- Same as above but goes backwards
  re2.begin(1,256,1,5,false);
  //Note: or setup range on the fly (for different usages of same encoder)
  //Example:  re1.setRange(10,10000,currentValueVar,50);

  MsTimer2::set(5, processTimer2);
  MsTimer2::start();
}


void loop(){
 //--- take action in loop to not blow out interrupt time
 set1.runButtonPress();
 set2.runButtonPress();
 set3.runButtonPress();
 delay(100);
}

Notes about the code:

I wanted the code to be "load and go" for someone making use of it. Also this is Alpha version one, not quite ready to move into library form. For these reasons the classes are just placed in line at the top of the code.

The buttons on the stereo are not connected to outputs in the same logical orientation I want to use them in. For example, the top eight buttons on the stereo go to 2 different outputs. I opted to create a logical breakdown of button numbers from left to right. So the top 8 are 1-8 the next row is 9-16 and so on. That is why the return numbers in the callbacks that map analog readings jump around. Your usage of this library may be simply numbered in order.

The action resulting from the callback should not happen in a timer or your code will lock up trying to do too much in the cycle. For this reason the runButtonPress routine is called inside the standard loop so the callback will run there as well.

// this is in loop
  set1.runButtonPress();
 set2.runButtonPress();
 set3.runButtonPress();

Here is a video of how I then used this stereo panel to control my lighting system.

That was so clever. I'll be looking much more closely at stuff when I go to garage sales from now on. One question though, how the heck did you get the lights to be white? It takes me hours to balance leds into white; actually near white, I never quite make it. So when I try to go to white, it's always light blue or pink or something else.

I'll be looking much more closely at stuff when I go to garage sales from now on.

Also "trash day" where people pile up stuff - since you are taking it apart for the buttons and dials - broken stuff is great. I have not tried to take apart the newer or low quality stuff - I would imagine the older and/or higher quality stuff you find - the better (why trash day is so great).

One question though, how the heck did you get the lights to be white?

I use LEDs with the correct resistors and controller chips built in (chips are LPD6803 and WS2801). So just setting R/G and B to full color does the trick.

I did often get blueish or redish faded white when working with raw LEDs and resistors. I found some LEDs I would purchase would be a little cyan colored and others would be pinkish - using the same resistors. So I think the color spectrums of the R/G/B (mostly R I think) may vary some from LED batch and/or brand .. also if the type of resistors being used is close but not exact then you may see some stronger color.

that is very smart, good work!

cool project.

an idea to consider which ive seen and its pretty cool.... put a real time clock in the code and you change the color of the LED to match/complement the sun light color out side based on time

Nice idea. I have been thinking about adding a clock to turn the whole room into a clock or build some LED clocks. Using the same clock for timer / timing functions for LED control is a natural fit.

Hey, saw this on hackedgadgets...where did you get you LED's ? Did make each pixel, or buy them premade, do you have any pictures or a link? Thx!

These are pre-wired LEDs with a shift register like chip on each LED.

This product best described each individual LED.

This product is a string of them.

This is the closest looking item to the product I use, but the chip is the same as the above ones.
http://www.bliptronics.com/item.aspx?ItemID=98

You can use code from bliptronics if using those lights or the nice fastspi library to control any of them.

Do not forget you need power - lots of DC power. 60ma per LED, so 50 LEDs is 3 amps and 15 watts.

I get mine from the factory in bulk via special order - but any of the above items should do the trick.

Hopes that gets you going in the right direction.

Awesome! Thanks for the info. I see that there are led strips available with HL1606 chips. Do you know if there is a huge difference between the HL1606 chips and the WS2801 that you use?

The HL1606 is a terrible chip, do not get it. That was the first chip I purchased and you can't even tell it "make this color".

The lpd6803 chip is nice and gives you complete control but it only has 32 steps per color (32x32x32=32768 colors). 32000 color seems like plenty but when you fade up red and only get 32 steps, it sure is choppy. The WS2801 chip has complete control and 256 steps. This is perfect because one byte is 256 combinations - a perfect fit. The chip is great because you tell the chip - go to this color (3 bytes) and it just stays like that until it gets another command. This means no timing issues, etc. These have been out for years but the controllers out there are pretty lame or are high end and costly. You can run them with an arduino but you have to start using a pretty complicated multi-chip setup if you want to do much logic and run over a couple hundred LEDs. That said, if you are looking at the arduino an a project with 100 to 200 LEDs and have the funds - there is no easier and more solid route.

Best of luck.