Motorized jukebox volume control

I modified this project and i am having some issues.

Video of project

Explanation of project:
I fully restored a 50s jukebox and i want to control the volume remotely using a rotary encoder and connecting a motor to the volume pot on the back of the jukebox.

Current Setup:
Currently i have a single arduino with a rotary encoder, oled display, dual channel pwm motor driver and a dc motor with a magnetic encoder.

Issues:

my main issue i would like to solve is when i remove power from the device and turn it back on i would like it to show the current position.
Ex. i turn volume to 7 the oled shows 7 and when power is off and turns back on id like it to show 7 again. currently it goes back to 0.

second issue is id like it to be wireless(this is my first project and I have no idea how to do this)

third is i have a momentary switch that id like to add and this will activate a relay momentarily(This is for the rejecting of a record)

Other issues i want to look into are:
If the motor encounters some resistance make it stop

currently i have a 12v motor, can i run it at 5v so i dont need 2 power supplies?

#include <Wire.h> //This is for i2C
#include <SSD1306Ascii.h> //i2C OLED
#include <SSD1306AsciiWire.h> //i2C OLED
#include <EEPROM.h>

#define I2C_ADDRESS 0x3C
#define RST_PIN -1
SSD1306AsciiWire oled;
float OLEDTimer = 0; //Timer for the display refresh interval

//I2C pins:
//STM32F103C8T6: SDA: PB7 SCL: PB6
//Arduino: SDA: A4 SCL: A5

//Input and output pins
//Motor encoder
const int encoderPin1 = 3; //this pin is also the interrupt pin!
const int encoderPin2 = 4; //this pin is a normal pin, read upon the interrupt
int encoderPin2Value = 0; //value of the encoder pin (0 or 1), this pin is read inside the interrupt - used for direction determination
//----------------------------------
//PWM motor driver
const int PWMPin = 11; //this is an analog pin (with the tilde (~) symbol), this pin can also do higher frequency + independent from millis()'s timer
int PWMValue = 0; //0-255 PWM value for speed, external PWM boards can go higher (e.g. PCA9685: 12-bit => 0-4095)
const int directionPin1 = 10; //digital pin, output, sets the direction
const int directionPin2 = 9; //digital pin, output, sets the direction
const int standByPin = 6; //STDBY pin, must be active high. Stops the H-bridge.
int motorDirection = 1; //direction value 0: CCW, 1: CW. - Stored value
//----------------------------------
//Rotary encoder
const int RotaryCLK = 2; //CLK pin on the rotary encoder, interrupt pin!
const int RotaryDT = 7; //DT pin on the rotary encoder, read inside the interrupt
const int RotarySW = 8; //SW pin on the rotary encoder (Button function)
int RotaryButtonValue = 0; //0 or 1 (pressed or not)
float RotaryTime; //timer for debouncing
volatile int rotaryValue = 0; //value manipulated by the encoder
int previousRotaryValue = -1; //a variable that stores the previous value - easy to follow changes
//----------------------

//Target values - Also called as setpoint!
float targetPosition = 0; //the PID will try to reach this value

//-----------------------
//Measured values
volatile float motorPosition = 0; //position based on the encoder
float previousMotorPosition = -1; //helps to keep track of changes (useful for the display update)
//-----------------------------------
//-----------------------------------
//PID parameters - tuned by the user
float proportional = 1.35; //k_p = 0.5
float integral = 0.00005; //k_i = 3
float derivative = 0.01; //k_d = 1
float controlSignal = 0; //u - Also called as process variable (PV)
//-----------------------------------
//-----------------------------------
//PID-related
float previousTime = 0; //for calculating delta t
float previousError = 0; //for calculating the derivative (edot)
float errorIntegral = 0; //integral error
float currentTime = 0; //time in the moment of calculation
float deltaTime = 0; //time difference
float errorValue = 0; //error
float edot = 0; //derivative (de/dt)

//Statuses of the DT and CLK pins on the encoder
int CLKNow;
int CLKPrevious;
int DTNow;
int DTPrevious;

// Define a memory address to store the volume position
#define VOLUME_POSITION_ADDRESS 0

void setup() {
  Serial.begin(115200);
  Wire.begin(); //start i2C
  Wire.setClock(800000L); //faster clock

  //Motor encoder-related
  pinMode(encoderPin1, INPUT); //A
  pinMode(encoderPin2, INPUT); //B
  attachInterrupt(digitalPinToInterrupt(encoderPin1), checkEncoder, RISING);
  pinMode(standByPin, OUTPUT); //

  //Definition of the pins, remember where you need
  pinMode(RotaryCLK, INPUT_PULLUP); //CLK
  pinMode(RotaryDT, INPUT_PULLUP); //DT
  pinMode(RotarySW, INPUT_PULLUP); //SW
  attachInterrupt(digitalPinToInterrupt(RotaryCLK), RotaryEncoder, CHANGE);
  //Store states
  CLKPrevious = digitalRead(RotaryCLK);
  DTPrevious = digitalRead(RotaryDT);

  //------------------------------------------------------------------------------
  //OLED part
#if RST_PIN >= 0
  oled.begin(&Adafruit128x32, I2C_ADDRESS, RST_PIN);
#else // RST_PIN >= 0
  oled.begin(&Adafruit128x32, I2C_ADDRESS);
#endif // RST_PIN >= 0

  oled.setFont(Adafruit5x7);
  oled.clear(); //clear display
  oled.set2X(); //
  oled.println("Seeburg"); //print some welcome message
  oled.println("Controller");
  oled.set1X();
  delay(1000);
  OLEDTimer = millis(); //start the timer
  oled.clear();
  displayPermanentItems();
  refreshDisplay();

  // Load volume position from EEPROM
  targetPosition = loadVolumePosition();
}

void loop() {
  calculatePID();
  driveMotor();
  CheckRotaryButton();
  refreshDisplay();
}

void checkEncoder() {
  //We need to read the other pin of the encoder which will be either 1 or 0 depending on the direction
  encoderPin2Value = digitalRead(encoderPin2);

  if (encoderPin2Value == 1) //CW direction
  {
    motorPosition++;
  }
  else //else, it is zero... -> CCW direction
  {
    motorPosition--;
  }
}

void driveMotor() {
  //Determine speed and direction based on the value of the control signal
  //direction
  if (controlSignal < 0) //negative value: CCW
  {
    motorDirection = 1;
  }
  else if (controlSignal > 0) //positive: CW
  {
    motorDirection = -1;
  }
  else //0: STOP - this might be a bad practice when you overshoot the setpoint
  {
    motorDirection = 0;
  }
  //---------------------------------------------------------------------------
  //Speed
  PWMValue = (int)fabs(controlSignal); //PWM values cannot be negative and have to be integers
  if (PWMValue > 255) //fabs() = floating point absolute value
  {
    PWMValue = 255; //capping the PWM signal - 8 bit
  }

  if (PWMValue < 30 && errorValue != 0)
  {
    PWMValue = 30;
  }
  //A little explanation for the
  // "bottom capping":
  //Under a certain PWM value, there won't be enough current flowing through the coils of the motor
  //Therefore, despite the fact that the PWM value is set to the "correct" value, the motor will not move
  //The above value is an empirical value, it depends on the motors perhaps, but 30 seems to work well in my case

  //we set the direction - this is a user-defined value, adjusted for TB6612FNG driver
  if (motorDirection == 1) //-1 == CCW
  {
    digitalWrite(directionPin1, LOW);
    digitalWrite(directionPin2, HIGH);
  }
  else if (motorDirection == -1) // == 1, CW
  {
    digitalWrite(directionPin1, HIGH);
    digitalWrite(directionPin2, LOW);
  }
  else // == 0, stop/break
  {
    digitalWrite(directionPin1, LOW);
    digitalWrite(directionPin2, LOW);
    digitalWrite(standByPin, LOW);
    PWMValue = 0;
    //In this block, we also shut down the motor and set the PWM to zero
  }
  //----------------------------------------------------
  //Then we set the motor speed
  analogWrite(PWMPin, PWMValue);

  //Optional printing on the terminal to check what's up
  /*
    Serial.print(errorValue);
    Serial.print(" ");
    Serial.print(PWMValue);
    
    Serial.print(" ");
    Serial.print(targetPosition);
    Serial.print(" ");
    Serial.print(motorPosition);
    Serial.println();
  */
}

void calculatePID() {
  //Determining the elapsed time
  currentTime = micros(); //current time
  deltaTime = (currentTime - previousTime) / 1000000.0; //time difference in seconds
  previousTime = currentTime; //save the current time for the next iteration to get the time difference
  //---
  errorValue = motorPosition - targetPosition; //Current position - target position (or setpoint)

  edot = (errorValue - previousError) / deltaTime; //edot = de/dt - derivative term

  errorIntegral = errorIntegral + (errorValue * deltaTime); //integral term - Newton-Leibniz, notice, this is a running sum!

  controlSignal = (proportional * errorValue) + (derivative * edot) + (integral * errorIntegral); //final sum, proportional term also calculated here

  previousError = errorValue; //save the error for the next iteration to get the difference (for edot)
}

void printValues() {
  //Serial.print("Position: ");
  Serial.println(motorPosition);
}

void displayPermanentItems() {
  //print the permanent items on the display
  oled.setCursor(0, 0); //(x [pixels], y[lines])
  oled.print("Target Volume");

  oled.setCursor(0, 2);
  oled.print("Volume Position");
}

void refreshDisplay() {
  if (millis() - OLEDTimer > 100) //check if we will update every 100 ms
  {
    if (previousRotaryValue != rotaryValue) {
      oled.setCursor(0, 1);
      oled.print("      ");
      oled.setCursor(0, 1);
      oled.print(rotaryValue / 515); //print the target value set by the rotary encoder

      previousRotaryValue = rotaryValue;
      OLEDTimer = millis(); //reset timer
    }

    if (motorPosition != previousMotorPosition) {
      oled.setCursor(0, 3);
      oled.print("      ");
      oled.setCursor(0, 3);
      oled.print(motorPosition / 515, 0); //print the new absolute position

      previousMotorPosition = motorPosition;
      OLEDTimer = millis(); //reset timer
    }
  } else {
    //skip
  }
}

void RotaryEncoder() {
  CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin
  // If last and current state of CLK are different, then a pulse occurred
  if (CLKNow != CLKPrevious && CLKNow == 1) {
    if (digitalRead(RotaryDT) != CLKNow) //the increment/decrement can depend on the actual polarity of CLK and DT
    {
      rotaryValue = max(0, rotaryValue - 515); //1
    } else {
      rotaryValue = min(515 * 10, rotaryValue + 515); //1
    }
  }
  CLKPrevious = CLKNow; // Store the last CLK state
}

void CheckRotaryButton() {
  RotaryButtonValue = digitalRead(RotarySW); //read the button state

  if (RotaryButtonValue == 0) //0 (activates when pressed)and 1 can differ based on the wiring
  {
    if (millis() - RotaryTime > 1000) {
      targetPosition = rotaryValue; //pass the setpoint (target value) to the corresponding variable
      digitalWrite(standByPin, HIGH); //enable motor driver
      RotaryTime = millis(); //save time

      // Save volume position to EEPROM
      saveVolumePosition(targetPosition);
    }
  }
}

void saveVolumePosition(float position) {
  EEPROM.put(VOLUME_POSITION_ADDRESS, position);
}

float loadVolumePosition() {
  float savedPosition;
  EEPROM.get(VOLUME_POSITION_ADDRESS, savedPosition);
  return savedPosition;
}

A rotary encoder has no "home" location, so if you store the value before power-loss, you can set any location as the stored value. Search "arduino store data" to see various methods. Top of the hobby list will be SD card (unlimited writes, slow), Flash memory (not EEPROM flash) and EEPROM (limited writes).

Would a 12vdc power supply work as the "one" power supply, and use a buck converter to get your 5vdc?

I don't need the rotary encoder position to be saved I need the motor position. So if I set volume to 5 the motor shaft will be in 5 position.

Right now if I turn it off and back on the motor will stay in the 5 position but the volume will say 0 because it resets.

Typically, motorized pots use gearmotors because a direct DC motor goes too fast.

I wouldn't store the position digitally because there's a chance of it getting "off" and errors could accumulate. You're digitizing/quantizing an analog position so it will never be "perfect"... If you add a "home sensor" at zero-volume it could re-calibrate itself "on command".

Maybe a 2nd pot to read the position (basically like how a servo works). Since you have to mechanically link the pot & motor, linking a 2nd pot might not be a big deal?

Cool!!!

...I wouldn't actually want to listen to records but I'll bet it looks cool!

It is a geared motor. I do have a few micro switches that I could add. 1 for the min and 1 for the max. Now I just need to figure out the coding for that. I would also need to figure out how to make it go to the min position then to the previous volume when turned back on

The only motorized volume control I have seen was in an old 1940's radio my Grandmother gave me. It was from a restaurant or bar and the motor driven volume control was ALSO the off-on control. The only way to turn the set off was to drive the motor to volume zero and also click the AC switch. The motor was reversed to turn the set on and begin moving the volume control. Obviously there was some way to power the motor one way with the set off, but at 13 years old, I did not bother with that.

I would suggest you take a look at digital pots to replace the existing one and FRAM (Ferroelectric Random Access Memory), basically unlimited reads and writes with no delays. Available in I2C and SPI for the Arduino. No need for feedback, you just keep it in FRAM and update each time you change it.

Seeburg came out with the PRVC in the late 60s. My jukebox used MRVC (master remote volume control) which used like a 6-7 wire cable to connect the controller to the jukebox.

I have since added a min limit switch which stops the motor when it gets to the lowest volume and sets the volume to 0. I need to now make the motor go to that position when turned off and on then back to the previous volume.

Once all that is done I need to get another Arduino nano and figure out how to make it wireless with 1 nano connected to the rotary encoder and the other connected to the motor. Then I'll be able to mount everything inside the controller and place it anywhere I want

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.