Cats and Lasers
- by TobiasWeis
- in Allgemein Cats Electronics Robotics
- posted October 12, 2019
I finally am a full member of the internet – I am the proud servant of two cats! To keep them entertained when me and my wife are at work, I built a web-controlled laser-turret:
At its heart is an ESP2866 on an nodemcu amica v2, a cheap servo pan/tilt kit with small 9g servos and some no-brand laser-module rated for 5v and 40mA.
In this build-log I will write about the process of creating it.
The code and all files I used can be found in my github-repo: https://github.com/TobiasWeis/cats_and_lasers.
Here is the machine in action with one of the test subjects:
As always, in the beginning was a rough idea, a breadboard and a mess of jumper-wires, with the usual quirks and problems: loose or sketchy contacts, wires in the way, a confusing colorfull mess.
I created a python-script to simulate the movements of the turret (can be seen on the screen in the background) using some sine/cosine-math.
However, when everything was working, I wanted a more permanent solution, and quickly created the schematic in EAGLE – there even is a part-library for the NodeMCU-Controller I was using. (I know, the interfaces for the contacts of the servos are messed up, in my case I just switched the pins inside their casing. As I own one of those very small CNC machines, I took the board-file from eagle to http://chilipeppr.com/jpadie, an online CAM processor that is able to read eagle board-files and output gcode, which can be sent to the CNCs controller. I mirrored the board along the X-axis in jpadie, as it will be milled on the bottom-layer of the board.
First I fitted a very sharp, 0.1mm and 10° engraving bit into the CNC, then I fastened the bare PCB using 3d-printed holders. Using the autolevel-feature of jpadie and connected probe-wires, I measured the real heights of the PCB at over 100 evenly spaced points. Milling took less than thirty minutes, and then I quickly soldered some pins and headers onto the board, and inserted the components.
All that was left to do was to cut out some holes (using a Dremel with a cutting-disc and a regular drill) from a casing for the servo and the cables, wrap the cables nicely, and polish the software.
The code is straight forward and based on standard libraries. I was using the arduino IDE to create and compile the code for the ESP2866, as there are easy-to-use libraries to quickly connect to a WiFi-network, and also handle webserver-tasks.
I gave the device a static IP in my router and assigned a hostname to it. That way, the website can simply be called by inserting http://katzenlaser in the browsers URL field. With a little bit of bootstrap and css, the website looks like this:
The Wifi is set up to connect to our access-point, and the webserver “hosts” a page to display and is able to handle post-requests sent by buttons. The arduino-code below handles said tasks, and calls functions to control the servos and the laser.
[showhide type=”pressrelease” more_text=”Show Arduino C-code for ESP2866 NodeMCU (%s More Words)” less_text=”Hide Code” hidden=”yes”]
#include <ESP8266WiFi.h> #include <ESP8266WebServer.h> #include <Servo.h> /* needs to be placed in separate header, * otherwise the arduino ide will "escape"/create prototypes * for javascript "function" statements and destroy the javascript-part */ #include "index.h" #define LASER_PIN D4 #define SERVO_PAN_PIN D2 #define SERVO_TILT_PIN D3 const char* ssid = "ROUTER_SSID_HERE"; //Hier SSID eures WLAN Netzes eintragen const char* password = "ROUTER_PWD_HERE"; //Hier euer Passwort des WLAN Netzes eintragen ESP8266WebServer server ( 80 ); const char* www_username = "laser"; const char* www_password = "laser"; Servo servo1; // base rotation Servo servo2; // up/down bool isLaserOn = false; //------------------------- Arduino-related code void setup() { pinMode(LASER_PIN, OUTPUT); digitalWrite(LASER_PIN,LOW); Serial.begin(115200); delay(10); WiFi.hostname("catlaser"); WiFi.begin(ssid,password); while(WiFi.status() != WL_CONNECTED){ delay(500); Serial.print("."); } server.on ( "/", handleRoot ); server.begin(); Serial.println ( "HTTP server started" ); Serial.println(); Serial.println(WiFi.localIP()); attach_servos(); test_functionality(); detach_servos(); // turn the pwm-pulse off to avoid noises and power consumption } void loop(){ server.handleClient(); delay(1000); } //------------------------- web-server-related code String getPage(){ return MAIN_page; } void handleSubmitAction(){ String action = server.arg("execute_action"); Serial.print(action); if(action == "sweep_once"){ attach_servos(); sweep_laser(1); detach_servos(); server.send(200, "text/html", "OK"); }else if(action == "sweep_five"){ attach_servos(); sweep_laser(5); detach_servos(); server.send(200, "text/html", "OK"); }else if(action == "test"){ attach_servos(); test_functionality(); detach_servos(); server.send(200, "text/html", "OK"); }else if(action == "toggle_laser"){ toggle_laser(); server.send(200, "text/html", "OK"); }else if(action == "apply_servo_value"){ Serial.println("Servo values are "); Serial.println(server.arg("servo_value1")); Serial.println(server.arg("servo_value2")); int value1 = server.arg("servo_value_1").toInt(); int value2 = server.arg("servo_value_2").toInt(); attach_servos(); servo1.write(value1); servo2.write(value2); delay(2000); detach_servos(); server.send(200, "text/html", "OK"); //server.send ( 200, "text/html", getPage() ); }else{ Serial.println("Error Servo Value"); } } void handleRoot() { if(!server.authenticate(www_username, www_password)) return server.requestAuthentication(); if (server.hasArg("execute_action")){ Serial.println("Executing action"); handleSubmitAction(); } else{ server.send (200, "text/html", getPage()); } } //------------------------- hardware related code void detach_servos(){ servo1.detach(); servo2.detach(); } void attach_servos(){ servo1.attach(SERVO_PAN_PIN); servo2.attach(SERVO_TILT_PIN); } void laser_on(){ digitalWrite(LASER_PIN, HIGH); isLaserOn=true; } void laser_off(){ isLaserOn=false; digitalWrite(LASER_PIN, LOW); } void toggle_laser(){ if(isLaserOn){ digitalWrite(LASER_PIN,LOW); isLaserOn=false; }else{ digitalWrite(LASER_PIN,HIGH); isLaserOn=true; } } // sweep each servo from 0 to 180 // and blink the laser to test functionality void test_functionality(){ // servo 1 for(int angle=0; angle<180;angle++){ servo1.write(angle); delay(10); } for(int angle=180; angle>0;angle--){ servo1.write(angle); delay(10); } // servo 2 for(int angle=0; angle<180;angle++){ servo2.write(angle); delay(10); } for(int angle=180; angle>0;angle--){ servo2.write(angle); delay(10); } // laser for(int num=0;num<5;num++){ laser_on(); delay(500); laser_off(); delay(500); } } void sweep_laser(int num){ laser_on(); int base = 50; int scale = 10; for(int k=0; k<num; k++){ for(int i=0; i<180;i++){ servo1.write(i); // 0 at 90 degrees, // 90 at 0 and 180 servo2.write(base + sin(deg2rad(i)*2)*scale); // @180: 90 delay(30); } for(int i=180; i>0;i--){ servo1.write(i); servo2.write(base-sin(deg2rad(i)*2)*scale); // @180: 0 delay(30); } } laser_off(); } //------------------------- utility functions int rad2deg(float rad){ return (rad*4068)/71; } float deg2rad(int deg){ return (deg*71)/4068.0; }[/showhide]
The frontend - a simple website with some buttons, input-fields and jquery to handle the ajax-requests - is saved as a progmem-variable in the included header-file (index.h) - there are two reasons for this, the first is readability, and the second is that the arduino-IDE will create function-prototypes when it sees the "function"-keyword - which you need to define javascript-functions. It does not care if it is inside a string or not, it will just mess up the resulting string if you try.
I wanted a nice image on the website but did not want to bother with the filesystem-plugins, so I just base64-encoded jpeg and directly inserted this string in the src-field of the img-tag.[showhide type="pressrelease" more_text="Show encoded Website/Javascript code for ESP2866 NodeMCU (%s More Words)" less_text="Hide Code" hidden="yes"]
const char MAIN_page[] PROGMEM = R"rawliteral( <!DOCTYPE html> <html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22https%3A%2F%2Fajax.googleapis.com%2Fajax%2Flibs%2Fjquery%2F3.3.1%2Fjquery.min.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" /> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2Fjavascript%22%3E%0A%20%24(document).ready(function()%7B%0A%20%24(%22p%22).click(function()%7B%0A%20%24(this).hide()%3B%0A%20%7D)%3B%0A%20%7D)%3B%0A%20%0A%20function%20js_submit_action(actionstr)%7B%0A%20console.log(%22js_submit_action%20called!%22)%3B%0A%20%24.ajax(%7B%0A%20type%3A%20%22POST%22%2C%0A%20url%3A%20%22%2F%22%2C%0A%20data%3A%20%7B%22execute_action%22%3Aactionstr%7D%2C%0A%20success%3A%20console.log(%22success!%22)%0A%20%7D)%3B%0A%20%7D%0A%20%0A%20function%20js_set_servo_value()%7B%0A%20console.log(%22js_set_servo_value%20called!%22)%3B%0A%20%24.ajax(%7B%0A%20type%3A%20%22POST%22%2C%0A%20url%3A%20%22%2F%22%2C%0A%20data%3A%20%7B%22execute_action%22%3A%20%22apply_servo_value%22%2C%20%0A%20%22servo_value_1%22%3A%24('%23servo_value1').val()%2C%0A%20%22servo_value_2%22%3A%24('%23servo_value2').val()%7D%2C%0A%20success%3A%20console.log(%22success!%22)%0A%20%7D)%3B%0A%20%7D%0A%20%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" /> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cstyle%3E%0A%20.btn%2C%20.btn%3Avisited%20%7B%0A%20color%3A%23333%20!important%3B%0A%20border-color%3A%23333%20!important%3B%0A%20%7D%0A%20.btn%3Ahover%2C%20.btn%3Aactive%7B%0A%20color%3A%23fff%20!important%3B%0A%20background-color%3A%20%23333%20!important%3B%0A%20%7D%0A%0A%20.form-control%3Afocus%2C%20.btn%3Afocus%7B%0A%20box-shadow%3A%200px%201px%201px%20rgba(0%2C0%2C0%2C0.075)%20inset%2C%200px%200px%208px%20rgba(255%2C0%2C0%2C0.5)%3B%0A%20%7D%0A%20%3C%2Fstyle%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;style&gt;" title="&lt;style&gt;" /> </head> <body> <main role="main" class="container" style="width:400px;margin-left:auto;margin-right:auto;"> <div style="width:400px;margin-left:auto;margin-right:auto;margin-bottom:20px;"> <!-- base64-encoded image as a string here --> <img src="data:image/jpeg;base64,ALOTOFDATA" /> </div> <button class="btn btn-outline-success my-2 my-sm-0" onclick='js_submit_action("sweep_once");' value='sweep'>Sweep 1x</button> <button class="btn btn-outline-success my-2 my-sm-0" onclick='js_submit_action("sweep_five");' value='sweep'>Sweep 5x</button> <button class="btn btn-outline-success my-2 my-sm-0" onclick='js_submit_action("test");' value='test'>Test</button> <hr> <input class="form-control mr-sm-2" type='text' id='servo_value1' placeholder="Servo-Value 1" aria-label="Servo-Value 1"/> <input class="form-control mr-sm-2" type='text' id='servo_value2' placeholder="Servo-Value 2" aria-label="Servo-Value 2"/> <button class="btn btn-outline-success my-2 my-sm-0" onclick='js_set_servo_value();' value='set value'>Set values</button> <button class="btn btn-outline-success my-2 my-sm-0" onclick='js_submit_action("toggle_laser");' value='Toggle laser'>Toggle laser</button> </main> </body> </html> )rawliteral";[/showhide]
I calculated/simulated the servo-movements using a jupyter-notebook, as I could quickly plug in some formulas and see the resulting curves without having to flash and test on the servos each time:
And finally, the parts I used:
- AZDelivery NodeMCU Lua Amica Modul V2 (6 EUR)
- DAGU pan/tilt servo kit https://robosavvy.com/store/dagu-pan-tilt-kit-with-servos.html (13 EUR)
- Some male and female pinheaders (... EUR)
- A blank 7cmx10cm PCB that I milled in a CNC (1 EUR)
Which means a total cost of something like 20 EUR, of course not counting in the reusable parts like breadboard, jumper-wires or the CNC...
Comments
Laser Toy Keeps Cats Entertained – ITmix.cz
October 26, 2019 at 4:51 pm[…] Cats are among the most popular domesticated creatures, and their penchant for chasing laser pointers is well known. With a pair of felines of his own to look after, [Tobi] set about making a device to help keep them entertained. […]
Pedro
October 26, 2019 at 8:59 pmIs there any danger of the cats looking into the lasers or the beam reflecting into their eyes?
TobiasWeis
October 27, 2019 at 1:49 amThose lasers are not really lasers as far as I understand, and are not as tightly bundled or nearly as strong.
Laser Toy Keeps Cats Entertained
October 26, 2019 at 10:51 pm[…] Cats are among the most popular domesticated creatures, and their penchant for chasing laser pointers is well known. With a pair of felines of his own to look after, [Tobi] set about making a device to help keep them entertained. […]
Немец собрал автоматическую лазерную указку для своих котов | Техно новости России и мира.
October 28, 2019 at 4:31 pm[…] «Чтобы развлечь их, когда мы с женой на работе, я построил управляемую […]