Inhoudsopgave:
- Benodigdheden
- Stap 1: Het robotplatform bouwen
- Stap 2: Programmeer de robot
- Stap 3: Hoe het werkt
- Stap 4: Ideeën en restjes
Video: DuvelBot - ESP32-CAM bierserveerrobot - Ajarnpa
2024 Auteur: John Day | [email protected]. Laatst gewijzigd: 2024-01-30 11:15
Na een dag hard werken komt er niets in de buurt van het nippen van je favoriete biertje op de bank. In mijn geval is dat het Belgische blonde bier "Duvel". Na alles behalve instorten worden we geconfronteerd met een zeer ernstig probleem: de koelkast met mijn Duvel is een onoverbrugbare 20 voet verwijderd van die bank.
Hoewel een lichte dwang van mijn kant misschien een tiener uit de koelkast zou kunnen halen om mijn wekelijkse portie Duvel te schenken, is de taak om het daadwerkelijk aan zijn bijna uitgeputte voorouder te bezorgen duidelijk een stap te ver.
Tijd om de soldeerbout en het toetsenbord uit de kast te halen…
DuvelBot is een no-nonsense AI-Thinker ESP32-CAM-gebaseerde rij-webcam, die u kunt bedienen vanaf uw smartphone, browser of tablet.
Het is gemakkelijk om dit platform aan te passen of uit te breiden naar minder alcoholisch gebruik (denk aan SpouseSpy, NeighbourWatch, KittyCam…).
Ik heb deze robot voornamelijk gebouwd om iets te leren over de hele webprogrammering en IoT-dingen, waar ik niets van af wist. Dus aan het einde van dit Instructable is een uitgebreide uitleg van hoe dat werkt.
Veel delen van deze Instructable zijn gebaseerd op de uitstekende uitleg die te vinden is in Random Nerd Tutorials, dus ga ze alsjeblieft bezoeken!
Benodigdheden
Wat je nodig hebt:
De onderdelenlijst is niet in steen gebeiteld en veel onderdelen kunnen worden verkregen in een heleboel verschillende versies en op veel verschillende plaatsen. Ik kocht de meeste van Ali-Express. Zoals Machete zei: improviseer.
Hardware:
- AI Thinker ESP32-CAM-module. Het zou waarschijnlijk kunnen werken met andere ESP32-CAM-modules, maar dat heb ik gebruikt
- L298N motorbesturingskaart,
- Een goedkoop 4-wielig robotica-platform,
- Een behuizing met een groot plat oppervlak zoals de Hammond Electronics 1599KGY,
- USB-naar-3.3V-TTL-converter voor programmeren.
- Voor de verlichting: 3 witte LED's, BC327 of andere universele transistor NPN (Ic=500mA), 4k7k weerstand, 3 82Ohm weerstanden, perfboard, kabels (zie schema en afbeeldingen).
- Een aan/uit-tuimelschakelaar en een normaal open drukknop voor programmering.
Optioneel:
- Een fisheye-camera met een langere flex dan de standaard OV2460-camera voorzien van de ESP32-CAM-module,
- WiFi-antenne met voldoende lange kabel en Ultra Miniature Coax Connector, zoals deze. De ESP32-CAM heeft een antenne aan boord en de behuizing is van plastic, dus een antenne is niet echt nodig, maar ik vond het er cool uitzien, dus…
- Inkjet printbaar stickerpapier voor het ontwerp van de bovenklep.
De gebruikelijke hardware tools: soldeerbout, boren, schroevendraaiers, tangen…
Stap 1: Het robotplatform bouwen
Het schema:
Het schema is niet bijzonder. De ESP32-cam stuurt de motoren aan via het L298N motor driver board, dat twee kanalen heeft. De motoren van de linker- en rechterkant zijn parallel geplaatst en elke kant neemt één kanaal in beslag. Vier kleine 10..100nF keramische condensatoren dicht bij de motorpinnen zijn zoals altijd raadzaam om RF-interferentie tegen te gaan. Ook kan een grote elektrolytische dop (2200…4700uF) op de voeding van de motorkaart, zoals weergegeven in het schema, hoewel niet strikt nodig, de rimpel van de voedingsspanning een beetje beperken (als je een horrorfilm wilt zien, probeer dan Vbat met een oscilloscoop terwijl de motoren actief zijn).
Merk op dat beide motorkanalen ENABLE-pinnen worden aangedreven door dezelfde pulsbreedtegemoduleerde (PWM) pin van de ESP32 (IO12). Dit komt omdat de ESP32-CAM-module niet veel GPIO's heeft (het schema van de module is ter referentie bijgevoegd). De LED's van de robot worden aangedreven door IO4, die ook de ingebouwde flits-LED aanstuurt, dus verwijder Q1 om te voorkomen dat de flits-LED oplicht in een gesloten behuizing.
Programmeerknop, aan/uit-schakelaar, laadconnector en programmeerconnector zijn toegankelijk onder de robot. Ik had veel beter werk kunnen doen voor de programmeerconnector (3,5 mm jack?), maar het bier kon niet meer wachten. Ook over-the-air-updates (OTA) zouden leuk zijn om in te stellen.
Om de robot in de programmeermodus te zetten, drukt u op de programmeerknop (dit trekt IO0 laag) en schakelt u hem vervolgens in.
Belangrijk: om de NiMH-batterijen van de robot op te laden, gebruikt u een laboratoriumvoedingsset (onbelast) tot ongeveer 14V en een stroomlimiet van 250mA. De spanning zal zich aanpassen aan de spanning van de batterijen. Koppel de robot los als de robot warm aanvoelt of als de batterijspanning ongeveer 12,5 V bereikt. Een voor de hand liggende verbetering hier zou zijn om een goede batterijlader te integreren, maar dat valt buiten het bestek van deze Instructable.
De hardware:
Zie ook de opmerkingen bij de foto's. De behuizing wordt met 4 M4-bouten en zelfborgende moeren op de robotbasis gemonteerd. Let op de rubberen slang die wordt gebruikt als afstandshouders. Hopelijk geeft dit ook wat vering aan de Duvel, mocht de rit hobbelig blijken te zijn. De ESP32-CAM-module en het L298N-motorbord zijn in de behuizing gemonteerd met behulp van plastic plakvoetjes (niet zeker van de juiste naam in het Engels), om te voorkomen dat er extra gaten moeten worden geboord. Ook de ESP32 is gemonteerd op zijn eigen perfboard en pluggable pinheaders. Dit maakt het gemakkelijk om de ESP32 uit te wisselen.
Vergeet niet: als je voor een externe wifi-antenne gaat in plaats van de ingebouwde, soldeer dan ook de antenneselectiejumper aan de onderkant van het ESP32-CAM-bord.
Print het bovenste logo in het bestand DuvelBot.svg op inkjet stickerpapier (of ontwerp je eigen), en je bent klaar om te gaan!
Stap 2: Programmeer de robot
Het is raadzaam om de robot te programmeren voordat je hem sluit, om er zeker van te zijn dat alles werkt en er geen magische rook verschijnt.
U hebt de volgende softwaretools nodig:
- De Arduino-IDE,
- De ESP32-bibliotheken, SPIFFS (seriële perifere flash-bestandssysteem), ESPAsync Webserver-bibliotheek.
De laatste kan worden geïnstalleerd door deze randomnerdtutorial te volgen tot en met de sectie "uw bestanden organiseren". Ik zou het echt niet beter kunnen uitleggen.
De code:
Mijn code is te vinden op:
- Een Arduino-schets DuvelBot.ino,
- Een submap met gegevens die de bestanden bevat die met SPIFFS naar de ESP-flash worden geüpload. Deze map bevat de webpagina die de ESP zal bedienen (index.html), een logo-afbeelding die deel uitmaakt van de webpagina (duvel.png) en een gecascadeerd stylesheet of CSS-bestand (style.css).
Om de robot te programmeren:
- Sluit de USB-TTL-converter aan zoals weergegeven in het schema,
- Bestand -> Openen -> ga naar de map waar DuvelBot.ino zich bevindt.
- Wijzig uw netwerkreferenties in de schets:
const char* ssid = "yourNetworkSSIDHere";const char* password = "yourPasswordHere";
- Tools -> Board -> "AI-Thinker ESP-32 CAM" en selecteer de juiste seriële poort voor je pc (Extra -> Port -> zoiets als /dev/ttyUSB0 of COM4),
- Open de seriële monitor in de Arduino IDE, terwijl u op de PROG-knop drukt (die IO0 laag trekt), schakel de robot in,
- Controleer op de seriële monitor of de ESP32 klaar is om te downloaden,
- Sluit de seriële monitor (anders mislukt de SPIFFS-upload),
- Tools -> "ESP32 Sketch Data Upload" en wacht tot het klaar is,
- Schakel uit en weer in door de PROG-knop ingedrukt te houden om terug te keren naar de programmeermodus,
- Druk op de "Upload"-pijl om de schets te programmeren en wacht tot deze klaar is,
- Open de seriële monitor en reset de ESP32 door uit/aan te zetten,
- Nadat het is opgestart, noteert u het ip-adres (zoiets als 192.168.0.121) en koppelt u de robot los van de USB-TTL-converter,
- Open een browser op dit ip-adres. U zou de interface moeten zien zoals op de afbeelding.
- Optioneel: stel het mac-adres van de ESP32 in op een vast ip-adres in je router (afhankelijk van de router hoe dit te doen).
Dat is het! Lees verder als je wilt weten hoe het werkt…
Stap 3: Hoe het werkt
Nu komen we bij het interessante deel: hoe werkt het allemaal samen?
Ik zal proberen het stap voor stap uit te leggen, maar houd er rekening mee dat Kajnjaps geen specialist in webprogrammering is. In feite was het hele uitgangspunt van het bouwen van DuvelBot het leren van een beetje webprogrammeren. Als ik duidelijke fouten maak, laat dan een reactie achter!
Ok, nadat ESP32 is ingeschakeld, initialiseert het, zoals gebruikelijk in de setup, de GPIO's, associeert het ze met PWM-timers voor motor- en LED-besturing. Zie hier voor meer informatie over de motorbesturing, het is vrij standaard.
Vervolgens wordt de camera geconfigureerd. Ik heb de resolutie bewust vrij laag gehouden (VGA of 640x480) om een trage reactie te voorkomen. Merk op dat het AI-Thinker ESP32-CAM-bord een seriële ram-chip (PSRAM) heeft die het gebruikt om cameraframes met een grotere resolutie op te slaan:
if(psramFound()) { Serial.println("PSRAM gevonden."); config.frame_size = FRAMESIZE_VGA; config.jpg_quality = 12; config.fb_count = 2; //aantal framebuffers zie: https://github.com/espressif/esp32-camera } else { Serial.println ("geen PSRAM gevonden."); config.frame_size = FRAMESIZE_QVGA; config.jpg_quality = 12; config.fb_count = 1; }
Vervolgens wordt het seriële perifere flash-bestandssysteem (SPIFFS) geïnitialiseerd:
// initialiseer SPIFFS if(!SPIFFS.begin(true)) { Serial.println("Er is een fout opgetreden tijdens het aankoppelen van SPIFFS!"); opbrengst; }
SPIFFS werkt als een klein bestandssysteem op de ESP32. Hier wordt het gebruikt om drie bestanden op te slaan: de webpagina zelf index.html, een gecascadeerd bestand stylesheet style.css en een png-afbeelding logo duvel.png. Deze bestanden worden door de ESP32 geleverd aan iedereen die er als client verbinding mee maakt. Hoewel het mogelijk en gemakkelijk is om de hele webpagina vanuit de schets te bedienen door een server.send(…) uit te voeren, vergelijkbaar met het doen van een serial.println() op een grote tekstreeks, is het gemakkelijker om in plaats daarvan gewoon een bestand te serveren, omdat dit ook werkt voor afbeeldingen en andere niet-tekstuele gegevens.
Vervolgens maakt de ESP32 verbinding met uw router (vergeet niet uw inloggegevens in te stellen voordat u uploadt):
//wijzig de inloggegevens van uw router hereconst char* ssid = "yourNetworkSSIDHere";const char* password = "yourPasswordHere"; … //verbind met WiFi Serial.print("Verbinding maken met WiFi"); WiFi.begin(ssid, wachtwoord); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); vertraging (500); } // nu verbonden met de router: ESP32 heeft nu ip-adres
Om daadwerkelijk iets nuttigs te doen, starten we een asynchrone webserver:
// maak een AsyncWebServer-object op poort 80AsyncWebServer-server (80); … server.begin(); // begin te luisteren naar verbindingen
Als u nu het ip-adres intypt dat door de router aan de ESP32 is toegewezen in de adresbalk van een browser, krijgt de ESP32 een verzoek. Dit betekent dat het op de client (u of uw browser) moet reageren door iets aan te bieden, bijvoorbeeld een webpagina.
De ESP32 weet hoe te reageren, omdat in de setup de reacties op alle mogelijke toegestane verzoeken zijn geregistreerd met server.on(). De hoofdwebpagina of index (/) wordt bijvoorbeeld als volgt afgehandeld:
server.on("/", HTTP_GET, (AsyncWebServerRequest *request){ Serial.println(" / request ontvangen!"); request->send(SPIFFS, "/index.html", String(), false, verwerker); });
Dus als de client verbinding maakt, reageert de ESP32 door het bestand index.html vanaf het SPIFFS-bestandssysteem te verzenden. De parameterprocessor is de naam van een functie die de html voorbewerkt en eventuele speciale tags vervangt:
// Vervangt tijdelijke aanduidingen in de html zoals %DATA%// door de variabelen die u wilt tonen//
Gegevens: %DATA%
String processor(const String& var){ if(var == "DATA"){ //Serial.println("in processor!"); return String (dutyCycleNow); } retourneer String();}
Laten we nu de webpagina index.html zelf ontleden. Over het algemeen zijn er altijd drie delen:
- html code: welke elementen moeten getoond worden (knoppen/tekst/sliders/afbeeldingen etc.),
- stijlcode, hetzij in een apart.css-bestand of in een … sectie: hoe de elementen eruit moeten zien,
- javascript a … sectie: hoe de webpagina moet werken.
Zodra index.html in de browser wordt geladen (die weet dat het html is vanwege de DOCTYPE-regel), komt het op deze regel terecht:
Dat is een verzoek om een css-stylesheet. De locatie van dit blad wordt gegeven in href="…". Dus wat doet uw browser? Juist, het lanceert een ander verzoek naar de server, dit keer voor style.css. De server legt dit verzoek vast, omdat het is geregistreerd:
server.on("/style.css", HTTP_GET, (AsyncWebServerRequest *request){ Serial.println(" css-verzoek ontvangen"); request->send(SPIFFS, "/style.css", "text/css "); });
Netjes toch? Overigens zou het href="/some/file/on/the/other/side/of/the/moon" kunnen zijn, voor al je browsers. Het zou dat bestand net zo graag gaan ophalen. Ik zal niet uitleggen over de stylesheet, omdat deze alleen het uiterlijk bepaalt, dus het is hier niet echt interessant, maar als je meer wilt weten, bekijk dan deze tutorial.
Hoe verschijnt het DuvelBot-logo? In index.html hebben we:
waarop de ESP32 reageert met:
server.on("/duvel", HTTP_GET, (AsyncWebServerRequest *request){ Serial.println("duvel logo request ontvangen!"); request->send(SPIFFS, "/duvel.png", "image-p.webp
.. nog een SPIFFS-bestand, dit keer een volledige afbeelding, zoals aangegeven door "image/png" in het antwoord.
Nu komen we bij het echt interessante deel: de code voor de knoppen. Laten we ons concentreren op de FORWARD-knop:
NAAR VOREN
De naam is slechts een naam om deze aan de stylesheet te koppelen om de grootte, kleur, etc. aan te passen. De belangrijke onderdelen zijn onmousedown="toggleCheckbox('forward')" en onmouseup="toggleCheckbox('stop') ". Dit zijn de acties van de knop (hetzelfde voor ontouchstart/ontouchend maar daarvoor zijn touchscreens/telefoons). Hier roept de knopactie een functie aan toggleCheckbox(x) in de javascript-sectie:
functie toggleCheckbox(x){ var xhr = new XMLHttpRequest(); xhr.open("GET", "/" + x, waar); xhr.send(); // zou ook iets met het antwoord kunnen doen als het klaar is, maar dat doen we niet}
Dus als u op de vooruit-knop drukt, wordt toggleCheckbox('forward') onmiddellijk gebeld. Deze functie lanceert dan een XMLHttpRequest "GET", van de locatie "/forward" die net werkt alsof u 192.168.0.121/forward zou hebben getypt in de adresbalk van uw browser. Zodra dit verzoek bij de ESP32 binnenkomt, wordt het afgehandeld door:
server.on("/forward", HTTP_GET, (AsyncWebServerRequest *request){ Serial.println("received /forward"); actionNow = FORWARD; request->send(200, "text/plain", "OK forward"."); });
Nu antwoordt de ESP32 eenvoudig met een tekst "OK vooruit". Opmerking toggleCheckBox() doet niets met (of wacht op) dit antwoord, maar het zou kunnen, zoals later in de cameracode wordt getoond.
Op zichzelf stelt het programma tijdens deze reactie alleen een variabele actionNow = FORWARD in als reactie op het indrukken van de knop. Nu in de hoofdlus van het programma wordt deze variabele bewaakt met als doel de PWM van de motoren te verhogen/verlagen. De logica is: zolang we een actie hebben die niet STOP is, laat de motoren in die richting draaien totdat een bepaald aantal (dutyCycleMax) is bereikt. Houd dan die snelheid aan, zolang de actionNow niet is veranderd:
void loop(){ currentMillis = millis(); if (currentMillis - previousMillis >= dutyCycleStepDelay) {// bewaar de laatste keer dat u de lus hebt uitgevoerd previousMillis = currentMillis; // mainloop is verantwoordelijk voor het op- en afbouwen van de motoren if (actionNow! = previousAction) { // ramp down, then stop, then change action and ramp up dutyCycleNow = dutyCycleNow-dutyCycleStep; if (dutyCycleNow <= 0) { // als na het aflopen dc 0 is, zet dan de nieuwe richting, begin bij min dutycycle setDir (actionNow); previousAction = actionNow; dutyCycleNow = dutyCycleMin; } } else //actionNow == previousAction ramp up, behalve wanneer de richting STOP is {if (actionNow!= STOP) { dutyCycleNow = dutyCycleNow+dutyCycleStep; if (dutyCycleNow > dutyCycleMax) dutyCycleNow = dutyCycleMax; } anders dutyCycleNow = 0; } ledcWrite(pwmChannel, dutyCycleNow); // pas de motorcyclus aan }}
Dit verhoogt langzaam de snelheid van de motoren, in plaats van gewoon op volle snelheid te lanceren en de kostbare kostbare Duvel te morsen. Een voor de hand liggende verbetering zou zijn om deze code naar een timer-interrupt-routine te verplaatsen, maar het werkt zoals het is.
Als we nu de doorstuurknop loslaten, roept uw browser toggleCheckbox('stop') aan, wat resulteert in een verzoek om GET /stop. De ESP32 zet actionNow op STOP (en reageert met "OK stop."), waardoor de hoofdlus de motoren laat draaien.
Hoe zit het met de LED's? Hetzelfde mechanisme, maar nu hebben we een schuifregelaar:
In het javascript wordt de instelling van de schuifregelaar gecontroleerd, zodat bij elke wijziging een oproep om "/LED/xxx" te krijgen plaatsvindt, waarbij xxx de helderheidswaarde is waarop de LED's moeten worden ingesteld:
var slide = document.getElementById('slide'), sliderDiv = document.getElementById("sliderAmount"); slide.onchange = function() { var xhr = nieuwe XMLHttpRequest(); xhr.open("GET", "/LED/" + deze.waarde, waar); xhr.send(); sliderDiv.innerHTML = deze.waarde; }
Merk op dat we document.getElementByID('slide') hebben gebruikt om het slider-object zelf op te halen, dat is gedeclareerd met en dat de waarde wordt uitgevoerd naar een tekstelement met bij elke wijziging.
De handler in de schets vangt alle helderheidsverzoeken op met behulp van "/LED/*" in de handlerregistratie. Vervolgens wordt het laatste deel (een getal) gesplitst en naar een int gecast:
server.on("/LED/*", HTTP_GET, (AsyncWebServerRequest *request){ Serial.println("led request ontvangen!"); setLedBrightness((request->url()).substring(5).toInt ()); request->send(200, "text/plain", "OK Leds."); });
Zoals hierboven beschreven, regelen de keuzerondjes variabelen die de PWM-standaardwaarden instellen, zodat DuvelBot langzaam naar je toe kan rijden met het bier, voorzichtig om dat vloeibare goud niet te morsen, en snel terug naar de keuken om wat meer te halen.
… Dus hoe wordt het camerabeeld bijgewerkt zonder dat u de pagina hoeft te vernieuwen? Daarvoor gebruiken we een techniek genaamd AJAX (Asynchronous JavaScript en XML). Het probleem is dat een client-server verbinding normaal gesproken een vaste procedure volgt: client (browser) doet verzoek, server (ESP32) reageert, case gesloten. Gedaan. Er gebeurt niets meer. Als we de browser maar op de een of andere manier konden misleiden om regelmatig updates van de ESP32 te vragen … en dat is precies wat we zullen doen met dit stukje javascript:
setInterval(function(){ var xhttp = new XMLHttpRequest(); xhttp.open("GET", "/CAMERA", true); xhttp.responseType = "blob"; xhttp.timeout = 500; xhttp.ontimeout = function(){}; xhttp.onload = function(e){ if (this.readyState == 4 && this.status == 200) { //zie: https://stackoverflow.com/questions/7650587/using… // https://www.html5rocks.com/en/tutorials/file/xhr2/ var urlCreator = window. URL || window.webkitURL; var imageUrl = urlCreator.createObjectURL(this.response); //maak een object van de blob document.querySelector("#camimage").src = imageUrl; urlCreator.revokeObjectURL(imageurl) } }; xhttp.send(); }, 250);
setInterval neemt als parameter een functie en voert deze om de zoveel tijd uit (hier eenmaal per 250 ms wat resulteert in 4 frames/seconde). De functie die wordt uitgevoerd, vraagt om een binaire "blob" op het adres /CAMERA. Dit wordt afgehandeld door de ESP32-CAM in de schets als (van Randomnerdtutorials):
server.on("/CAMERA", HTTP_GET, (AsyncWebServerRequest *request){ Serial.println("camera request ontvangen!"); camera_fb_t * fb = NULL; //esp_err_t res = ESP_OK; size_t _jpg_buf_len = 0; uint8_t * _jpg_buf = NULL; //capture a frame fb = esp_camera_fb_get(); if (!fb) {Serial.println("Frame buffer kon niet worden verkregen");return;} if(fb->format != PIXFORMAT_JPEG)/ /al in dit formaat van config{ bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len); esp_camera_fb_return(fb); fb = NULL; if(!jpeg_converted){Serial.println("JPEG-compressie mislukt");return; } } else{ _jpg_buf_len = fb->len; _jpg_buf = fb->buf; } //Serial.println(_jpg_buf_len); //stuur het opgemaakte afbeeldingsverzoek->send_P(200, "image/jpg", _jpg_buf, _jpg_buf_len); //opschonen if(fb){ esp_camera_fb_return(fb); fb = NULL; _jpg_buf = NULL; } else if(_jpg_buf){ free(_jpg_buf); _jpg_buf = NULL; } });
De belangrijke onderdelen zijn om het frame fb = esp_camera_fb_get() te converteren naar een-j.webp
De javascript-functie wacht vervolgens tot deze afbeelding arriveert. Dan kost het alleen wat werk om de ontvangen "blob" om te zetten in een url die als bron kan worden gebruikt om de afbeelding op de html-pagina bij te werken.
pff, we zijn klaar!
Stap 4: Ideeën en restjes
Het doel van dit project voor mij was om net genoeg webprogrammering te leren om hardware op het web te koppelen. Er zijn meerdere uitbreidingen op dit project mogelijk. Hier zijn een paar ideeën:
- Implementeer 'echte' camerastreaming zoals hier en hier uitgelegd en verplaats deze naar een 2e server zoals hier uitgelegd op dezelfde ESP32, maar op de andere CPU-kern, importeer vervolgens de camerastream in de html die wordt bediend door de 1e server met behulp van een …. Dit zou moeten resulteren in snellere camera-updates.
- Gebruik de toegangspuntmodus (AP), zodat de robot meer op zichzelf staat, zoals hier wordt uitgelegd.
- Uitbreiden met batterijspanningsmeting, deep-sleep mogelijkheden etc. Dit is momenteel wat lastig omdat de AI-Thinker ESP32-CAM niet veel GPIO's heeft; heeft uitbreiding nodig via uart en bijvoorbeeld een slave-arduino.
- Verander in een kattenzoekende robot die van tijd tot tijd kattensnoepjes uitwerpt met een pootdruk op een grote knop, stream tonnen leuke kattenfoto's gedurende de dag…
Reageer als je het leuk vond of vragen hebt en bedankt voor het lezen!