Laser Tower 3000 (Sad Cat Project)
by 654863 in Circuits > Arduino
220 Views, 0 Favorites, 0 Comments
Laser Tower 3000 (Sad Cat Project)
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
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
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
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.