Laser Tower 3000 (Sad Cat Project)

by 654863 in Circuits > Arduino

213 Views, 0 Favorites, 0 Comments

Laser Tower 3000 (Sad Cat Project)

IMG-4530.jpg

The Laser Tower 3000 is a next-gen laser toy to make your sad cat happy. This machine is essentially a tower with a rotating laser affixed to the top. It comes with manual and automatic modes, four different LED presets, an LCD screen to display information, audio cues, as well as a remote control. Automatic mode runs random presets at random intervals, whereas manual mode gives the user full control over everything through the remote. Note: this project was done during the COVID pandemic on an accelerated timeline, so this Instructable represents my best attempt at this project in the given time.

Supplies

  • Arduino Uno
  • Wires
  • 220 ohm resistor
  • LED/Laser
  • LCD Screen
  • IR Remote and Sensor
  • Buzzer
  • Servo motor

Initial Planning and Design

concept.png
circuit.png

The first step to this project was planning out how I wanted to design the machine. To do this, I drew a concept sketch, which is attached in this step. As seen in the concept sketch, the initial design for the Laser Tower 3000 was a simple rectangular base with a tower. The tower was to have a rotating base, propelled by a motor, as well as four lasers. Additionally, there was a buzzer to provide the audio cues, an LCD to inform the user of the current mode, a remote control, as well as an Arduino Uno microcontroller to power and control all the components. I also wrote some pseudocode to get an idea of how I want the software behind the machine to run. I decided how I was going to write my code so that everything was modularized and easy to understand. You can find my pseudocode here. After this, I got some feedback from my teacher which led me to change a few things. Firstly, I decided to use one laser instead of four, which would more than likely confuse the cat rather than entertain it. I also decided it would be a better idea to only use the motor to rotate the top part of the tower (where the lasers were) because the motor would not be able to support the weight of the whole base. After these revisions, I made a basic circuit diagram (attached in this step) to plan out the setup for the electronics in my circuit.

Testing the Parts

remote_snippet.png

This step was possibly the most important step of all. Building this machine meant using a variety of components, some of which I had never used before. I had to test each of these components individually to understand how they worked before putting them together in the main circuit. For example, on the IR Remote, each and every key had a different code which I needed to write any sort of logic with the remote. I had to write some simple code to find and record each of the key codes, so that I was able to trigger certain logic when any of these keys were pressed. I have attached a screenshot of the code that I used to run this test. This is also when I took inventory. Because of the pandemic, I was not equipped with all of the resources I needed to build the entire circuit; I was missing a buzzer and a servo. So, after testing all my real-life components, I also tested these missing components using TinkerCAD. Later on, I also tested the code for these virtual components by pulling out snippets from my code (see Step 4) and putting them in the TinkerCAD to see if the components worked with it. You can find this proof-of-concept TinkerCAD here.

Building the Circuit

Buzzer and Servo Proof of Concept.png
IMG-4530.jpg
IMG-4531.jpg

After all the planning and testing of components was finished, I was ready to start building the circuit. Initially, the plan was to build the circuit as well as a cardboard structure as shown in the concept sketch. However, due to the pandemic and a of building resources, I was unable to build the actual cardboard structure; therefore, I just built the circuit itself. Even for this, my ability was limited due to the lack of components (see Step 2), but I tried my best given what was available. As noted in the last step, I used TinkerCAD for anything I could not do in real life. Attached are pictures of my completed circuit, as well as a screenshot of my proof-of-concept TinkerCAD for the buzzer and servo motor.

Writing the Code

This step was the most time-consuming part of the project. I already had a basic idea of how I wanted the code to be due to the pseudocode that I wrote in Step 1. However, there were a few details I still had to iron out, which presented me with a few challenges I needed to overcome.

For example, in my original pseudocode, I had accounted for four lasers. Therefore, after changing it to one laser, I needed to redo the presets and the code to account for it.

One other significant challenge I ran into while writing the code for this project was implementing the asynchronous behaviour needed for the automatic mode of the machine. Initially, I used simple loops and the delay function. However, I ran into problems by because these stopped my program's execution while they completed. To solve the problem, I employed the concept of clocks to keep track of "checkpoints" throughout the program. For example, one clock was used to track the interval between the change of presets in automatic mode. Instead of delaying, I simply stored the last known time the preset changed in the clock variables. Then, I would repeatedly check (with the help of the loop function) if a certain time past the checkpoint time had passed; if so, I would change the preset again and update the time. This functionality worked perfectly to bring the asynchronous behaviour I wanted for the automatic preset change, as well as other things.

Finally, one last challenge I had with the code in this project was writing the rotate function (to rotate the servo). The problem was that the servo could only rotate 180 degrees, instead of the 360 I expected. Therefore, I had to get creative with the rotate function. Initially, I thought I could use one rotate function for both the automatic and manual modes. I tried and failed with this method many many times, because it was hard to keep track of where the motor was and calculate what to do when the servo hit its limit in one direction. So, I split the rotate function into one for automatic and one for manual, both of which did something different. The manual one allows the user to rotate in any direction, but does not allow rotating over the limits of 0 and 180 degrees. However, in automatic mode, the servo only rotates in one direction and resets to 0 when it hits the limit of 180 degrees. I ended up doing something similar (splitting the functions) for other features as well (e.g. the preset functions), as this made it easier to handle a variety of use cases. Overall, I'm still not too happy with the way the rotate function works right now; if I had more time to do the project, I would have definitely spent more time on this to refine it more.

Overall, writing the code for this project was a fun and challenging experience, from which I learnt a lot. Here is an empty TinkerCAD with the full code. In case that link is expired, here is a pastebin for the same. If for whatever reason, both of these links are expired (unlikely), here is the code itself (not recommended to view it here due to the lack of syntax highlighting):

/******************************************************************************
Program: Sad Cat Fixer (Laser Tower 3000)

Description: Program to control the Laser Tower 3000. This includes manual and
automatic modes, an LCD screen, a servo, a buzzer, a laser (an LED for our
purposes), and an infrared remote control.

Author: Pranav Rao

Date: January 9, 2021

Arduino Resources used: digital pins 3, 4, 6, 8, and analog pins 4 and 5
******************************************************************************/

/******************************************************************************
Libraries: this section contains the importing of various libraries, which are
files that contain several classes, structs, and functions that are essential to
controlling various components such as the remote, LCD, and servo.
******************************************************************************/

#include         // library to interact with the IR remote
#include  // library to interact with the LCD
#include              // library to interact with the servo

/******************************************************************************
Constants: these are constant values that will be used to denote important
and consistent information. They are GLOBAL variables, and therefore can be used
by any function in this program.
******************************************************************************/

// declare constants to represent pins of each of the circuit components (IR,
// laser, servo, buzzer)
const int IR_RECEIVE_PIN = 3, LASER_PIN = 4, SERVO_PIN = 6, BUZZER_PIN = 8;

// declare constant strings for the words automatic and manual (which are
// printed on the LCD)
const char WORD_AUTOMATIC[] = "AUTOMATIC";
const char WORD_MANUAL[] = "MANUAL";

// declare constants for the sizes of the words so that they can be printed on
// the LCD
const int WORD_AUTOMATIC_SIZE =
    sizeof(WORD_AUTOMATIC) / sizeof(WORD_AUTOMATIC[0]);
const int WORD_MANUAL_SIZE = sizeof(WORD_MANUAL) / sizeof(WORD_MANUAL[0]);

// declare a set of variables used to keep track of various factors in the
// program. these variables will change throughout the program
int currentTime, currentPreset, randomInterval, currentServoRotation = 0,
                                                automaticRotationSpeed = 15;

// declare two clock variables, which will be used to keep track of certain
// times (amount of milliseconds from the beginning of the program). These
// clocks allow for the asynchronous behaviour of the automatic mode.
// clock1 is used to keep track of the intervals between preset changes in
// automatic mode. clock2 is used to keep track of the intervals between blinks
// for the auto blink functions
unsigned long clock1, clock2;

// declare two bool variables that will be used to keep track of the mode
// and the status of the laser
bool automatic = true, laserOn = false;

/******************************************************************************
Objects: these variables are instances of classes (imported in the header files
above). By making these instances, we are able to access all of the attributes
and functions defined in those classes
******************************************************************************/

// declare an object to represent the LCD
LiquidCrystal_I2C lcd(0x27, 16, 2);
// declare an object to represent the servo motor
Servo servo;

/******************************************************************************
writeText function: this function is called to write certain text to the
LCD. It takes a char pointer (a char array, essentially) and the length of the
char array, and returns void.
******************************************************************************/

void writeText(const char *text, int len) {
  Serial.println("LOG: Writing text to LCD.");

  lcd.clear(); // clear the LCD

  // start from the top left of the LCD. Then, sequencially print each character
  // in the given word, one character after the next
  for (int i = 0; i < len - 1; i++) {
    lcd.setCursor(i, 0); // set the cursor to the correct position
    lcd.print(text[i]);  // print the character
  }
}

/******************************************************************************
toggleLaser function: this function is called toggle the state of the laser. It
takes and returns nothing.
******************************************************************************/

void toggleLaser() {
  // if the laser is turned on
  if (laserOn) {
    digitalWrite(LASER_PIN, LOW);  // turn the laser off
  } else {                         // if the laser is off
    digitalWrite(LASER_PIN, HIGH); // turn the laser on
  }

  // if the laserOn boolean is false, set it to true else set it to false
  // (essentially toggle it)
  laserOn = laserOn ? false : true;
}

/******************************************************************************
Preset Functions: these functions are special functions that toggle different
presets of this machine. They all take nothing and return nothing.
******************************************************************************/

// this preset is used to keep the laser constantly on
void presetConstantOn() {
  // if the laser is not on
  if (!laserOn) {
    toggleLaser(); // turn on the laser
  }
}

// this preset is used to keep the laser constantly off
void presetConstantOff() {
  // if the laser is on
  if (laserOn) {
    toggleLaser(); // turn off the laser
  }
}

// this preset is used only in auto mode to keep the laser blink slowly
void presetAutoSlowBlink() {
  unsigned long currentTime = millis();

  if (currentTime > clock2 + 1500) {
    toggleLaser();
    clock2 = currentTime;
  }
}

// this preset is used only in auto mode to make the laser blink fast
void presetAutoFastBlink() {
  unsigned long currentTime = millis();

  if (currentTime > clock2 + 200) {
    toggleLaser();
    clock2 = currentTime;
  }
}

// this reset is used only in manual mode to make the laser blink slow
void presetManualSlowBlink() {
  for (int i = 0; i < 5; i++) {
    toggleLaser();
    delay(2000);
    toggleLaser();
    delay(2000);
  }
}

// this reset is used only in manual mode to make the laser blink fast
void presetManualFastBlink() {
  for (int i = 0; i < 5; i++) {
    toggleLaser();
    delay(500);
    toggleLaser();
    delay(500);
  }
}

// this is an array containing pointers to each of the automatic presets. A
// function will be called from this list depending on the current preset number
void (*autoPresets[4])() = {presetConstantOff, presetConstantOn,
                            presetAutoSlowBlink, presetAutoFastBlink};

// this is an array containing pointers to each of the manual presets. A
// function will be called from this list depending on the button pressed
void (*manualPresets[4])() = {presetConstantOff, presetConstantOn,
                              presetManualSlowBlink, presetManualFastBlink};

/******************************************************************************
Rotate Functions: these functions are functions that rotate the servo motor.
They both take a number of degrees to rotate and return nothing.
******************************************************************************/

// this function is used to rotate the servo a certain number of degrees in
// manual mode
void rotateManual(int degrees) {
  // calculate the new position of the servo using the current position of the
  // servo (stored in a variable)
  int newPosition = currentServoRotation + degrees;

  // if the new position is greater than 180 (the max the servo can turn in one
  // direction)
  if (newPosition > 180)
    newPosition = 180; // set the calculated new position back to 180

  // if the new position is less than 0 (the min the servo can turn in one
  // direction)
  if (newPosition < 0)
    newPosition = 0; // set the calculated new position back to 0

  // update the value of the current servo rotation to the new calculated value
  currentServoRotation = newPosition;

  servo.write(currentServoRotation); // turn the servo to the given position
  delay(500); // wait for the servo to turn to the given position
}

// this function is used to rotate the servo a certain number of degrees in
// manual mode
void rotateAutomatic(int degrees) {
  // the automatic mdoe wokrs by always moving in one direction until it reaches
  // the max position (180), where it resets to the min position (0)

  // given how the mode works, if the degrees value is negative, it must be
  // changed to positive
  int fixedDegrees =
      degrees < 0 ? -1 * degrees
                  : degrees; // change the degrees value to positive if it's
                             // negative, and store in a new variable

  // calculate the new position using the fixed degrees value
  int newPosition = currentServoRotation + fixedDegrees;

  // divide the new calculated position by 180 and take the remainder, then
  // store it in the variable to keep track of the current servo rotation. This
  // ensures that the position never exceeds 180 (the max).
  currentServoRotation = newPosition % 180;

  servo.write(currentServoRotation);
  delay(500); // wait for the servo to turn to the given position
}

/******************************************************************************
Mode Functions: these functions change the mode of the machine (manual vs
automatic). This affects how the user gives input and interacts with the
machine, and what the machine does given said input.
******************************************************************************/

// this function is the manual mode function. When called repeatedly in the loop
// function (see below), it gives the user full manual control of the machine,
// essentially allowing them to trigger any feature at random. The user can also
// use the play/pause button to switch to automatic mode
void manualMode() {
  // if input is received from the IR remote
  if (IrReceiver.decode()) {
    Serial.println("LOG: Received input from IR remote. Attempting to parse.");

    // attempt to parse the data and get a readable integer
    uint32_t decoded = IrReceiver.decodedIRData.decodedRawData;

    // this switch statement compares the value of the decoded variable with
    // each of the cases described. In this case, it is checking for the codes
    // of each remote button; if a certain remote button is hit, the machine
    // will perform the associated operation. Tests were performed beforehand to
    // find the code for each key on the remote.
    switch (decoded) {
    case 4077715200: // if the 1 key is pressed on the remote
      Serial.println("LOG: Remote input is button 1.");
      (*manualPresets[0])(); // call the first manual preset function declared
                             // in the array above
      break;
    case 3877175040: // if the 2 key is pressed on the remote
      Serial.println("LOG: Remote input is button 2.");
      (*manualPresets[1])(); // call the second manual preset function declared
                             // in the array above
      break;
    case 2707357440: // if the 3 key is pressed on the remote
      Serial.println("LOG: Remote input is button 3.");
      (*manualPresets[2])(); // call the third manual preset function declared
                             // in the array above
      break;
    case 4144561920: // if the 4 key is pressed on the remote
      Serial.println("LOG: Remote input is button 4.");
      (*manualPresets[3])(); // call the fourth manual preset function declared
                             // in the array above
      break;
    case 3141861120: // if the back key is pressed on the remote
      Serial.println("LOG: Remote input is button BACK.");
      rotateManual(
          -30); // rotate the motor 30 degrees counter clockwise (if possible)
      break;
    case 3158572800: // if the forward key is pressed on the remote
      Serial.println("LOG: Remote input is button FORWARD.");
      rotateManual(30); // rotate the motor 30 degrees clockwise (if possible)
      break;
    case 3208707840: // if the play/pause key is pressed on the remote
      Serial.println("LOG: Remote input is button PLAY/PAUSE.");
      Serial.println("LOG: Switching to AUTO mode.");
      automatic = true; // set the global automatic flag to true (change to
                        // automatic mode)
      writeText(WORD_AUTOMATIC,
                WORD_AUTOMATIC_SIZE); // print AUTOMATIC on the LCD
      // tone(BUZZER_PIN, 300, 1000); // play the buzzer sound to give the user
      // an audio cue for mode change
      break;
    }

    IrReceiver.resume(); // continue collecting input
  }

  return;
}

// this function is the automatic mode function. When called repeatedly in the
// loop function (see below), the function causes the machine to run
// automatically and randomly, in that it will continuously call random presets.
// this function will also give the user the ability to increase or decrease the
// speed of rotation using the up/down buttons on the remote. The user will
// also be able to switch to manual mode at any time using the play/pause button
// on the remote.
void automaticMode() {
  rotateAutomatic(
      automaticRotationSpeed); // rotate at the speed declared by the
                               // automaticRotationSpeed global variable

  unsigned long currentTime =
      millis(); // collect the current time to run comparisons against the two
                // async clocks

  // if the current time collected is greater than 5 seconds later than clock1
  // (the last recorded checkpoint)
  if (currentTime > clock1 + 5000) {
    Serial.print("LOG: Selecting new random preset. New preset: ");
    currentPreset =
        random(4); // select a new random number from 0 to 3 inclusive and set
                   // it as the global currentPreset variable
    Serial.println(currentPreset);
    clock1 = currentTime; // update clock1 to represent now as the last marked
                          // time (checkpoint)
  }

  // if input is received from the IR remote
  if (IrReceiver.decode()) {
    Serial.println("LOG: Received input from IR remote. Attempting to parse.");

    // attempt to parse the data and get a readable integer
    uint32_t decoded = IrReceiver.decodedIRData.decodedRawData;

    // this switch statement compares the value of the decoded variable with
    // each of the cases described. In this case, it is checking for the codes
    // of each remote button; if a certain remote button is hit, the machine
    // will perform the associated operation. Tests were performed beforehand to
    // find the code for each key on the remote.
    switch (decoded) {
    case 3208707840: // if the play/pause key is pressed on the remote
      Serial.println("LOG: Remote input is button PLAY/PAUSE.");
      Serial.println("LOG: Switching to MANUAL mode.");
      automatic = false; // set the global automatic flag to false (change to
                         // manual mode)
      writeText(WORD_MANUAL, WORD_MANUAL_SIZE); // print MANUAL on the LCD
      // tone(BUZZER_PIN, 300, 1000); // play the buzzer sound to give the user
      // an audio cue for mode change
      break;
    case 4127850240: // if the up key is pressed on the remote
      Serial.println("LOG: Increasing automatic speed by 5.");
      automaticRotationSpeed +=
          5; // increase the current speed for automatic mode rotation by 5
      if (automaticRotationSpeed >
          45) { // if the automatic rotation speed is above 45 (max)
        Serial.println("WARN: Automatic speed is above 45. Resetting to 45.");
        automaticRotationSpeed =
            45; // set the automatic rotation speed back to 45
      }
      break;
    case 4161273600: // if the down key is pressed on the remote
      Serial.println("LOG: Decreasing automatic speed by 5.");
      automaticRotationSpeed -=
          5; // decrease the current speed for automatic mode rotation by 5
      if (automaticRotationSpeed <
          0) { // if the automatic rotation speed is below 45 (min)
        Serial.println("WARN: Automatic speed is below 0. Resetting to 0.");
        automaticRotationSpeed =
            0; // set the automatic rotation speed back to 0
      }
      break;
    }

    IrReceiver.resume(); // continue collecting input
  }

  (*autoPresets[currentPreset])(); // call the current preset function (as
                                   // determined by the global var
                                   // currentPreset)
}

/******************************************************************************
Setup function: this function is automatically called once, and has a return
type of void.
******************************************************************************/

void setup() {
  Serial.begin(9600); // intialize the Serial monitor

  Serial.println("LOG: Starting init sequence.");

  // initialize the LCD
  Serial.println("LOG: Initializing LCD.");
  lcd.init();
  lcd.backlight(); // turn on the LCD backlight

  // initialize the IR Remote and bind it to the correct pin
  Serial.println("LOG: Initializing IR Remote.");
  IrReceiver.begin(IR_RECEIVE_PIN);

  // initialize the random seed (needed to use random functionality in automatic
  // function)
  Serial.println("LOG: Planting randomizer seed using empty analog input 0.");
  randomSeed(analogRead(0));

  // intialize clocks to current time to enable asynchronous functionality
  Serial.println("LOG: Initializing clocks.");
  clock1 = millis();
  clock2 = millis();

  // intialize currentPreset and randomInterval with random values from the
  // random seed
  Serial.println("LOG: Setting required random values.");
  randomInterval =
      random(5000, 15001);   // the random interval will always be a number from
                             // 5000 ms to 15000 ms (5-15 seconds)
  currentPreset = random(4); // the currentPreset will always be a number from
                             // 0-3 because there are four presets

  // set up laser
  Serial.println("LOG: Setting up laser.");
  pinMode(LASER_PIN, OUTPUT);   // set the laser to OUTPUT mode
  digitalWrite(LASER_PIN, LOW); // turn off the laser to begin

  // set up servo
  Serial.println("LOG: Setting up servo.");
  servo.attach(SERVO_PIN); // attach the servo object to the correct pin
  servo.write(currentServoRotation); // turn the servo to the correct position
                                     // (initially 0)

  Serial.println("LOG: Starting in AUTO mode.");
  writeText(WORD_AUTOMATIC,
            WORD_AUTOMATIC_SIZE); // print AUTOMATIC on the LCD (because it is
                                  // the starting mode)

  Serial.println("LOG: Playing initialization tone.");
  // tone(BUZZER_PIN, 300, 1000); // play a tone to notify the user the machine
  // has been initialized
}

/******************************************************************************
Loop function: this function is called repeatedly for the lifespan of the
program and has a return type of void.
******************************************************************************/

void loop() {
  // if the current mode is automatic (as determined by the automatic global
  // boolean), then call the automaticMode function. Else, call the manualMode
  // function. Since this check is in the loop function, this check will
  // continue to be run forever, meaning that the correct function (be it
  // automatic or manual) will be called many times in quick succession. The
  // program depends heavily on this mechanism to function.
  if (automatic) {
    automaticMode();
  } else {
    manualMode();
  }
}

Testing the Circuit

After finishing all of this, it was time to test the circuit. Here is a link to a video of the final real-life circuit working. Note that there is no buzzer or servo here because I did not have those components; a working version of those can be found in the TinkerCAD linked in step 2.