├── .gitattributes ├── LICENSE ├── README.md ├── hardware ├── 3dPrinted_Parts │ ├── PlayBot_Base.stl │ ├── PlayBot_LowerFrame.stl │ ├── PlayBot_MiddleFrame.stl │ ├── PlayBot_Shell.stl │ └── PlayBot_UpperStage.stl ├── PCB │ ├── BOM_LiPo-Charging-Fuel-Gauge-with-5V-Boost-Converter_2024-11-11.csv │ ├── BOM_Playbot-Main-Board_2024-12-04.csv │ ├── Gerber_LiPo-Charging-Fuel-Gauge-with-5V-Boost_PCB_LiPo-Charging-Fuel-Gauge-with-5V-B_2024-11-11.zip │ ├── Gerber_Playbot-Main-Board_PCB_Playbot-Main-Board_2024-12-04.zip │ └── README.md └── README.md ├── images ├── App.jpg ├── Assembly_01.jpg ├── Assembly_02.jpg ├── Assembly_03.jpg ├── Assembly_04.jpg ├── Assembly_05.jpg ├── Assembly_BallCaster.jpg ├── Assembly_Base_Magnet.jpg ├── Assembly_Base_Plug.jpg ├── Assembly_Base_USB.jpg ├── Assembly_ITR.jpg ├── Assembly_MainBoard_Headers.jpg ├── Assembly_MiddleFrame.jpg ├── Assembly_MiddleFrame_02.jpg ├── Assembly_MiddleFrame_Inserts.jpg ├── Assembly_Motors.jpg ├── Assembly_PowerBoard.jpg ├── Assembly_PowerBoard_02.jpg ├── Assembly_PowerHarness.jpg ├── Assembly_Switch.jpg ├── Assembly_TEMT.jpg ├── Assembly_TEMT_02.jpg ├── Assembly_TOF_01.jpg ├── Assembly_Teensy_Headers.jpg ├── Assembly_Tof_02.jpg ├── Base_Print.jpg ├── Blender_Rig.gif ├── ConsoleBase_Turn.gif ├── DRV8835.jpg ├── ITR20001.jpg ├── LipoSocket.jpg ├── LowerFrame_Print.jpg ├── LowerStage_Turn.gif ├── MiddleFrame_Print.jpg ├── MiddleStage_Turn.gif ├── Motors.jpg ├── PCB_Options_01.jpg ├── PCB_Options_02.jpg ├── Plastic_ring.jpg ├── PlayBot.gif ├── PlayBot_MainBoard.jpg ├── PlayBot_PowerBoard.jpg ├── PlayBot_WireFrame.jpg ├── PlayBot_WireFrame_02.jpg ├── PlayBot_exploded.jpg ├── Robot_01.gif ├── Robot_02.gif ├── Robot_03.gif ├── Robot_04.gif ├── Robot_Back.png ├── Robot_Front.jpeg ├── Robot_Rig.gif ├── ScreenAttachment.jpg ├── Servo.jpg ├── Servo_Horn.jpg ├── Shell_Print.jpg ├── Shell_Turn.gif ├── Teensy_CutTrace.jpg ├── TestFirmware.jpg ├── TopBoard_01.jpg ├── TopFrame_BrassInserts_01.jpg ├── TopFrame_BrassInserts_02.jpg ├── UpperFrame_Print.jpg ├── UpperStage_Turn.gif └── Video.jpg └── src ├── PlayBot ├── AnimationManager.h ├── BatteryManager.h ├── CommunicationManager.h ├── Config.h ├── Debug.h ├── DistanceTracker.h ├── HardwareConfig.h ├── LEDController.h ├── MotorController.h ├── PIDConfig.h ├── PlayBot.ino ├── README.md ├── SensorManager.h └── StorageManager.h └── Playbot_SensorsTest ├── Playbot_SensorsTest.ino └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Guillaume Loquin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖 PlayBot 2 | 3 | PlayBot is an unofficial, experimental accessory for Panic Inc.'s Playdate handheld console, which transforms your console into a lovely little desktop robot. 4 | With its integrated charging capabilities, it can serve as a charging dock for your Playdate. 5 | 6 | Here is a little video of Playbot (click to watch) 7 | 8 | [![Playbot_Trailer](images/Video.jpg)](https://youtu.be/pZ3aDawW1vo?si=pN9pur77LKVZNdHx) 9 | 10 | ⚡ Battery life: ~45 min of autonomous operation per charge. 11 | 12 | ![PlaybotAction](images/Robot_01.gif) ![PlaybotAction](images/Robot_02.gif) 13 | ![PlaybotAction](images/Robot_03.gif) ![PlaybotAction](images/Robot_04.gif) 14 | 15 | ## ⚠️ Disclaimer 16 | 17 | This project is not affiliated with nor endorsed by [Panic](https://panic.com/). 18 | 19 | I cannot be held responsible for any damage, data loss, short circuit, burned down house, etc. 😘 20 | 21 | ## 🔧 Hardware 22 | 23 | The project is based on a Teensy 4.1 microcontroller that acts as a USB hub for the Playdate and communicates over serial. 24 | 25 | The Playdate acts as the robot's brain and sends commands to the Teensy to control motors and gather various sensor values: 26 | 27 | - 2 TOF distance sensors 28 | - TEMT6000 Ambient light sensor 29 | - 2 IR sensors 30 | - Dual motors with encoders 31 | - MAX1704X fuel gauge 32 | 33 | I'm also using the Playdate's integrated sensors: 34 | - Accelerometer 35 | - Microphone 36 | 37 | Additionnal Vision AI camera powered by [Huskylens](https://www.dfrobot.com/product-1922.html) will be compatible. 38 | 39 | PlayBot's frame is fully 3D printed. A complete bill of materials and assembly instructions can be found [here](https://github.com/GuybrushTreep/PlayBot/tree/main/hardware) 40 | 41 | ## 💾 Firmware 42 | 43 | You can flash the [firmware](https://github.com/GuybrushTreep/PlayBot/tree/main/src/PlayBot) using the Arduino IDE and Teensyduino add-on available [here](https://www.pjrc.com/teensy/teensyduino.html). 44 | 45 | ## 📚 Dependencies 46 | - [USBHost_t36](https://github.com/PaulStoffregen/USBHost_t36) by [PaulStoffregen](https://github.com/PaulStoffregen) 47 | - [Encoder](https://github.com/PaulStoffregen/Encoder) by [PaulStoffregen](https://github.com/PaulStoffregen) 48 | - [drv8835-motor-shield](https://github.com/pololu/drv8835-motor-shield) by [pololu](https://github.com/pololu) 49 | - [SD library](https://github.com/arduino-libraries/SD) by [Arduino](https://github.com/arduino-libraries) 50 | - [Servo library](https://github.com/arduino-libraries/Servo) by [Arduino](https://github.com/) 51 | - [MAX1704X](https://github.com/adafruit/Adafruit_MAX1704X) by [adafruit](https://github.com/adafruit) 52 | - [PID_V1](https://github.com/br3ttb/Arduino-PID-Library) by [br3ttb](https://github.com/br3ttb) 53 | - [WS2812FX](https://github.com/kitesurfer1404/WS2812FX) by [kitesurfer1404](https://github.com/kitesurfer1404/WS2812FX) 54 | - [CircularBuffer](https://github.com/rlogiacco/CircularBuffer) by [rlogiacco](https://github.com/kitesurfer1404/WS2812FX) 55 | 56 | ## 📱 Companion App 57 | 58 | 59 | 60 | The app, written in Lua, needs to be sideloaded onto the Playdate and is located in a [separate repository](https://github.com/GuybrushTreep/pd-playbot-app). 61 | 62 | ## 🎨 Custom rig 63 | ![PlaybotRig](images/Robot_Rig.gif) 64 | 65 | A Custom rig and sequence exporter written in python is also available for Blender in a [seperate repository](https://github.com/GuybrushTreep/Playbot_Rig). 66 | Animate, render and export custom animation sequences! 67 | 68 | ## ⚠️ Known Issues 69 | * ToF sensors show unreliable readings when pointing into empty space (possibly defective units) - planning to replace with IR sensors 70 | * Battery gauge readings become erratic under high current load - investigation ongoing 71 | 72 | ## 📜 License 73 | 74 | [MIT](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /hardware/3dPrinted_Parts/PlayBot_Base.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/3dPrinted_Parts/PlayBot_Base.stl -------------------------------------------------------------------------------- /hardware/3dPrinted_Parts/PlayBot_LowerFrame.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/3dPrinted_Parts/PlayBot_LowerFrame.stl -------------------------------------------------------------------------------- /hardware/3dPrinted_Parts/PlayBot_MiddleFrame.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/3dPrinted_Parts/PlayBot_MiddleFrame.stl -------------------------------------------------------------------------------- /hardware/3dPrinted_Parts/PlayBot_Shell.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/3dPrinted_Parts/PlayBot_Shell.stl -------------------------------------------------------------------------------- /hardware/3dPrinted_Parts/PlayBot_UpperStage.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/3dPrinted_Parts/PlayBot_UpperStage.stl -------------------------------------------------------------------------------- /hardware/PCB/BOM_LiPo-Charging-Fuel-Gauge-with-5V-Boost-Converter_2024-11-11.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/PCB/BOM_LiPo-Charging-Fuel-Gauge-with-5V-Boost-Converter_2024-11-11.csv -------------------------------------------------------------------------------- /hardware/PCB/BOM_Playbot-Main-Board_2024-12-04.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/PCB/BOM_Playbot-Main-Board_2024-12-04.csv -------------------------------------------------------------------------------- /hardware/PCB/Gerber_LiPo-Charging-Fuel-Gauge-with-5V-Boost_PCB_LiPo-Charging-Fuel-Gauge-with-5V-B_2024-11-11.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/PCB/Gerber_LiPo-Charging-Fuel-Gauge-with-5V-Boost_PCB_LiPo-Charging-Fuel-Gauge-with-5V-B_2024-11-11.zip -------------------------------------------------------------------------------- /hardware/PCB/Gerber_Playbot-Main-Board_PCB_Playbot-Main-Board_2024-12-04.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/hardware/PCB/Gerber_Playbot-Main-Board_PCB_Playbot-Main-Board_2024-12-04.zip -------------------------------------------------------------------------------- /hardware/PCB/README.md: -------------------------------------------------------------------------------- 1 | # JLCPCB Options 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /hardware/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Summary 3 | - [Bill of materials](#bill-of-materials) 4 | - [Recommended tools](#Recommended-tools) 5 | - [3d printed parts](#3d-printed-parts) 6 | - [Custom PCBs](#custom-pcbs) 7 | - [Power board](#power-board) 8 | - [Main board](#main-board) 9 | - [Assembly](#assembly) 10 | - [Top Frame](#Top-Frame) 11 | - [Servo mod](#servo-modification) 12 | - [Brass insert](#brass-inserts) 13 | - [Middle Frame](#middle-frame) 14 | - [DRV8835](#drv8835) 15 | - [Power harness](#power-harness) 16 | - [Lower Frame](#lower-frame) 17 | - [Ball caster wheels](#ball-caster-wheels) 18 | - [ITR20001-sensors](#itr20001-sensors) 19 | - [console base](#console-base) 20 | - [Usb](#usb) 21 | - [Shell](#Shell) 22 | - [light sensor](#TEMT6000-light-sensor) 23 | - [switch](#SPDT-switch) 24 | - [TOF10120](#TOF-sensors) 25 | - [Putting Everything Together](#Putting-Everything-Together) 26 | # Bill of materials 27 | 28 | This folder contains all the files and instructions to build Playbot. 29 | 30 | 31 | 32 | ## Hardware 33 | 34 | |Item| Quantity|Notes 35 | |---|---|---| 36 | |Custom PCBs|2|Order from [JLCPCB](https://jlcpcb.com) see Gerber files and BOMs 37 | |Teensy 4.1 microcontroller| 1|[PJCRC store](https://www.pjrc.com/store/teensy41.html) 38 | | TOF10120| 2| [AlliExpress](https://fr.aliexpress.com/item/1005006355757514.html?spm=a2g0o.detail.pcDetailTopMoreOtherSeller.3.5152nL9knL9kkQ&gps-id=pcDetailTopMoreOtherSeller&scm=1007.40050.354490.0&scm_id=1007.40050.354490.0&scm-url=1007.40050.354490.0&pvid=43a39a83-8bf7-4a9d-b2aa-4d4200da0014&_t=gps-id:pcDetailTopMoreOtherSeller,scm-url:1007.40050.354490.0,pvid:43a39a83-8bf7-4a9d-b2aa-4d4200da0014,tpp_buckets:668%232846%238107%231934&isseo=y&pdp_npi=4%40dis%21EUR%214.29%214.29%21%21%214.68%214.68%21%402103895417271255595797778e7ce3%2112000037973716121%21rec%21FR%21%21ABXZ&utparam-url=scene%3ApcDetailTopMoreOtherSeller%7Cquery_from%3A) 39 | | Low profile headers 12 pins| 1| [Example AlliExpress](https://fr.aliexpress.com/item/1005006252502654.html?spm=a2g0o.productlist.main.23.f22a276csirRub&algo_pvid=8485fcfc-69b6-4fa3-bf40-6245eae15df8&algo_exp_id=8485fcfc-69b6-4fa3-bf40-6245eae15df8-11&pdp_npi=4%40dis%21EUR%215.08%210.99%21%21%2139.04%217.62%21%40211b618e17271256823062451e979f%2112000036477546171%21sea%21FR%210%21ABX&curPageLogUid=J2GVUPW2oOxk&utparam-url=scene%3Asearch%7Cquery_from%3A) 40 | | SG90 servo | 1 | [Example AlliExpress](https://fr.aliexpress.com/item/1005006522732429.html?spm=a2g0o.productlist.main.1.2e7078dbdj347Y&algo_pvid=9f489bd2-ea5e-4c4a-88c8-4c1252d9fda9&algo_exp_id=9f489bd2-ea5e-4c4a-88c8-4c1252d9fda9-0&pdp_npi=4%40dis%21EUR%210.72%210.72%21%21%215.53%215.52%21%4021038dfc17271932356175976eb3da%2112000037520326755%21sea%21FR%210%21ABX&curPageLogUid=ZAvrS4F64GvW&utparam-url=scene%3Asearch%7Cquery_from%3A) 41 | | ITR20001 | 2| [Example AlliExpress](https://fr.aliexpress.com/item/1005007697148589.html?spm=a2g0o.productlist.main.25.715e137foby4aC&algo_pvid=6a9c50a2-0cd5-4385-87a5-524ec2daa876&algo_exp_id=6a9c50a2-0cd5-4385-87a5-524ec2daa876-12&pdp_npi=4%40dis%21EUR%211.69%211.69%21%21%2112.98%2112.98%21%40211b618e17271258031226100e979f%2112000041890684018%21sea%21FR%210%21ABX&curPageLogUid=HXnZdCWkpzS6&utparam-url=scene%3Asearch%7Cquery_from%3A) 42 | TEMT6000 ambient light sensor | 1 | [Example AlliExpress](https://fr.aliexpress.com/item/33034437035.html?src=google&pdp_npi=4%40dis%21EUR%211.18%211.18%21%21%21%21%210.9238%40%2167317181845%21ppc%21%21%21&src=google&albch=shopping&acnt=248-630-5778&isdl=y&slnk=&plac=&mtctp=&albbt=Google_7_shopping&aff_platform=google&aff_short_key=UneMJZVf&gclsrc=aw.ds&&albagn=888888&&ds_e_adid=&ds_e_matchtype=&ds_e_device=c&ds_e_network=x&ds_e_product_group_id=&ds_e_product_id=fr33034437035&ds_e_product_merchant_id=109029711&ds_e_product_country=FR&ds_e_product_language=fr&ds_e_product_channel=online&ds_e_product_store_id=&ds_url_v=2&albcp=19000710609&albag=&isSmbAutoCall=false&needSmbHouyi=false&gad_source=1&gclid=Cj0KCQjwxsm3BhDrARIsAMtVz6MFOhQxDsbiriZIzvAo15Y8bk2Jd7PBYBp9_BYVgLwGeoBBeu02b44aAlGTEALw_wcB) 43 | | mini ball caster wheels| 2| [Example AlliExpress](https://fr.aliexpress.com/item/1005007392098564.html?spm=a2g0o.detail.pcDetailTopMoreOtherSeller.5.6e78Rqs4Rqs4fD&gps-id=pcDetailTopMoreOtherSeller&scm=1007.40050.354490.0&scm_id=1007.40050.354490.0&scm-url=1007.40050.354490.0&pvid=af027fb5-cab6-4c62-8038-6874711e65c5&_t=gps-id:pcDetailTopMoreOtherSeller,scm-url:1007.40050.354490.0,pvid:af027fb5-cab6-4c62-8038-6874711e65c5,tpp_buckets:668%232846%238112%231997&isseo=y&pdp_npi=4%40dis%21EUR%210.21%210.21%21%21%210.23%210.23%21%402103895417271259822313224e7ce3%2112000040559594027%21rec%21FR%21%21ABXZ&utparam-url=scene%3ApcDetailTopMoreOtherSeller%7Cquery_from%3A) 44 | | N20 6V 500RPM motors| 2| [Example AlliExpress](https://fr.aliexpress.com/item/1005004999529855.html?spm=a2g0o.productlist.main.9.476c1b8bwLt7Fn&algo_pvid=a0f07c90-ab69-40fd-ac62-188fc3763f3e&algo_exp_id=a0f07c90-ab69-40fd-ac62-188fc3763f3e-4&pdp_npi=4%40dis%21EUR%215.62%215.59%21%21%216.13%216.09%21%40211b618e17271261375738397e979f%2112000031302274043%21sea%21FR%210%21ABX&curPageLogUid=eIQ0fq7ROtye&utparam-url=scene%3Asearch%7Cquery_from%3A) 45 | | N20 wheels| 2| [Example AlliExpress](https://fr.aliexpress.com/item/1005006640418754.html?spm=a2g0o.productlist.main.5.4390186ciMbiiV&algo_pvid=18114de1-0e8b-411d-8a66-9a63acacee0f&algo_exp_id=18114de1-0e8b-411d-8a66-9a63acacee0f-2&pdp_npi=4%40dis%21EUR%213.48%213.48%21%21%2126.71%2126.71%21%40211b618e17271262014572380e979f%2112000037899571195%21sea%21FR%210%21ABX&curPageLogUid=5F51BSrBxRqD&utparam-url=scene%3Asearch%7Cquery_from%3A) 46 | | DRV8835 Dual Motor Driver Carrier| 1| [Pololu](https://www.pololu.com/product/2135) 47 | | M2 Screws| 1 box| [Example AlliExpress](https://fr.aliexpress.com/item/1005002426046826.html?spm=a2g0o.productlist.main.17.5d2c02qB02qBON&algo_pvid=4e237d3e-e61e-4861-bcfc-cd9006211bb3&algo_exp_id=4e237d3e-e61e-4861-bcfc-cd9006211bb3-8&pdp_npi=4%40dis%21EUR%212.73%210.99%21%21%212.96%211.08%21%4021039f3217271904604808941e395c%2112000020776755059%21sea%21FR%210%21ABX&curPageLogUid=j6ltcxIlb15M&utparam-url=scene%3Asearch%7Cquery_from%3A) 48 | | M2 and M3 nylon nut set| | [Example Alliexpress](https://fr.aliexpress.com/item/33017191364.html?spm=a2g0o.productlist.main.61.58a8UaSaUaSax1&algo_pvid=998d6187-33be-4231-9b5a-b7b2b76323f7&algo_exp_id=998d6187-33be-4231-9b5a-b7b2b76323f7-30&pdp_npi=4%40dis%21EUR%210.83%210.83%21%21%210.91%210.91%21%40211b81a317277321413732983efdee%2167143762064%21sea%21FR%210%21ABX&curPageLogUid=iqw6aZ0Hb3BB&utparam-url=scene%3Asearch%7Cquery_from%3A) 49 | | M2 X 5 insert| 1 box| [Example AlliExpress](https://fr.aliexpress.com/item/1005007481465353.html?spm=a2g0o.productlist.main.7.93fd220ekV5QSL&algo_pvid=bfa72422-5d3a-44a2-8795-4eb09a8bf643&algo_exp_id=bfa72422-5d3a-44a2-8795-4eb09a8bf643-33&pdp_npi=4%40dis%21EUR%217.39%217.39%21%21%2156.58%2156.58%21%402103879317271908727925510eee95%2112000040927838417%21sea%21FR%210%21ABX&curPageLogUid=NgA9mCPcnJIl&utparam-url=scene%3Asearch%7Cquery_from%3A) 50 | | USBC connectors Male/Female| 1 | [Example AlliExpress](https://fr.aliexpress.com/item/1005004807492800.html?spm=a2g0o.productlist.main.3.6a1c713dWuoKkp&algo_pvid=f8315998-da24-4298-9379-5cac4823c946&algo_exp_id=f8315998-da24-4298-9379-5cac4823c946-1&pdp_npi=4%40dis%21EUR%211.49%210.99%21%21%211.62%211.08%21%40210384cc17271912443301441e5c75%2112000030569163725%21sea%21FR%210%21ABX&curPageLogUid=2nOJae7BAOXq&utparam-url=scene%3Asearch%7Cquery_from%3A) 51 | | SPDT switch| 1 | [Example AlliExpress](https://fr.aliexpress.com/item/32812689209.html?spm=a2g0o.productlist.main.11.540d62c3AtlIoR&algo_pvid=5dab6dae-69ef-4f38-b764-05826455a47c&algo_exp_id=5dab6dae-69ef-4f38-b764-05826455a47c-5&pdp_npi=4%40dis%21EUR%211.12%210.99%21%21%211.22%211.08%21%40211b6d3517271916289168180ed8d9%2164701560453%21sea%21FR%210%21ABX&curPageLogUid=cmJiNlaNp427&utparam-url=scene%3Asearch%7Cquery_from%3A) 52 | | Battery Lipo 1S 15000mAh 504050 | 1 | [Example AlliExpress](https://fr.aliexpress.com/item/32988366494.html?spm=a2g0o.productlist.main.5.77943e84pQCqFZ&algo_pvid=cedb2f8b-3d26-42ff-8e4b-c06506aa87fa&algo_exp_id=cedb2f8b-3d26-42ff-8e4b-c06506aa87fa-2&pdp_npi=4%40dis%21EUR%216.03%215.99%21%21%216.54%216.50%21%402103856417271918529103475ee9e2%2166794003013%21sea%21FR%210%21ABX&curPageLogUid=IWsQVFLzjuwk&utparam-url=scene%3Asearch%7Cquery_from%3A) 53 | | Pre crimped JST SH connector set 3 to 7 pins (pre crimped is much easier)| 1 | [Example AlliExpress](https://fr.aliexpress.com/item/1005004705798387.html?spm=a2g0o.productlist.main.17.6af84dbbMv6Q9z&algo_pvid=2816c474-a09c-4c15-ade2-a59e3dea51de&algo_exp_id=2816c474-a09c-4c15-ade2-a59e3dea51de-8&pdp_npi=4%40dis%21EUR%211.57%210.99%21%21%211.70%211.07%21%40211b65de17271921112724246ede95%2112000030170307469%21sea%21FR%210%21ABX&curPageLogUid=Dim0x4dEra8M&utparam-url=scene%3Asearch%7Cquery_from%3A) 54 | | JST PH connector Male/Female 2/4 pins | 1 box | [Example AlliExpress](https://fr.aliexpress.com/item/1005006498660940.html?spm=a2g0o.productlist.main.1.5be7e53b3rHXwg&algo_pvid=47ccf818-19de-41fa-b940-ce6a1f06d550&algo_exp_id=47ccf818-19de-41fa-b940-ce6a1f06d550-0&pdp_npi=4%40dis%21EUR%213.01%210.99%21%21%213.26%211.07%21%402103868d17271927901213422e1641%2112000037425305641%21sea%21FR%210%21ABX&curPageLogUid=USXJf5pJkqR8&utparam-url=scene%3Asearch%7Cquery_from%3A) 55 | | Flanged ball bearing 6X3X2.5 | 2 | [Example AlliExpress](https://fr.aliexpress.com/item/32969015516.html?spm=a2g0o.productlist.main.3.586d6bf817bbwF&algo_pvid=04ae072f-a77a-4c8c-a72f-fee04431f674&algo_exp_id=04ae072f-a77a-4c8c-a72f-fee04431f674-1&pdp_npi=4%40dis%21EUR%213.59%213.59%21%21%213.89%213.89%21%40211b80e117272049218613102e1fe2%2166588829720%21sea%21FR%210%21ABX&curPageLogUid=1cYg3OXHQMLk&utparam-url=scene%3Asearch%7Cquery_from%3A) 56 | | Nylon M3 X 40 screw | 1 | [Example AlliExpress](https://fr.aliexpress.com/item/1005003130662735.html?spm=a2g0o.productlist.main.9.38cd78b0LKOFkI&algo_pvid=4a069db4-0918-4b13-b2b5-e0f25a9854a5&algo_exp_id=4a069db4-0918-4b13-b2b5-e0f25a9854a5-4&pdp_npi=4%40dis%21EUR%211.44%211.37%21%21%211.57%211.49%21%4021039e0c17277341100491598e5524%2112000024260289627%21sea%21FR%210%21ABX&curPageLogUid=qN1JAKJijF9j&utparam-url=scene%3Asearch%7Cquery_from%3A) 57 | | Magnets 6X3| 4 | [Example AlliExpress](https://fr.aliexpress.com/item/1005006291065081.html?spm=a2g0o.productlist.main.5.1542Qf4qQf4q3P&algo_pvid=55609d9a-a952-4d67-82f9-ec55e0bcc53d&algo_exp_id=55609d9a-a952-4d67-82f9-ec55e0bcc53d-2&pdp_npi=4%40dis%21EUR%212.45%210.99%21%21%2118.76%217.62%21%40211b813c17272078815273190e3b6f%2112000036632421438%21sea%21FR%210%21ABX&curPageLogUid=kq2mNsJMVCzA&utparam-url=scene%3Asearch%7Cquery_from%3A) 58 | | Servo pull rod 120 mm| 1 | [Example AlliExpress](https://www.aliexpress.us/item/3256807116664206.html?spm=a2g0o.productlist.main.11.2340Jvx0Jvx0qE&algo_pvid=db665c48-aec3-4a6c-834a-dba8be968ce4&algo_exp_id=db665c48-aec3-4a6c-834a-dba8be968ce4-5&pdp_npi=4%40dis%21EUR%212.49%210.99%21%21%2118.71%217.45%21%402103868a17313546987914769e2bd1%2112000040199743168%21sea%21FR%210%21ABX&curPageLogUid=eaJh4OKVUcVg&utparam-url=scene%3Asearch%7Cquery_from%3A&gatewayAdapt=glo2usa4itemAdapt) 59 | | 1.3mm push rod connector| 1 |[Example AlliExpress](https://www.aliexpress.us/item/2255800230609318.html?spm=a2g0o.productlist.main.29.4b55QeqQQeqQDV&algo_pvid=700d217b-4f44-4084-85a9-6c46fda9fe91&algo_exp_id=700d217b-4f44-4084-85a9-6c46fda9fe91-14&pdp_npi=4%40dis%21USD%214.99%213.99%21%21%214.99%213.99%21%40211b819117313549045874610e3d14%2110000001783126286%21sea%21US%210%21ABX&curPageLogUid=cRb5HZZq58mU&utparam-url=scene%3Asearch%7Cquery_from%3A) 60 | | Micro SD card| 1 | 61 | | 24 AWG wire in various colors| 1 | 62 | | HuskyLens (optional, for advanced AI vision) | 1 | [DFRobot](https://www.dfrobot.com/product-1922.html?gad_source=1&gclid=Cj0KCQjwxsm3BhDrARIsAMtVz6O7-u6SEQ5DpgK14Sa_zBdhd3W4jWqcs46tiPwW6idb2EYJas_s3SIaAipaEALw_wcB) 63 | |A playdate !| 1 | [Play.date](https://play.date/) 64 | 65 | **Price range**: Around 100/120 $ (without shipping) **per bot**. Custom PCBs are cheap (10$ a piece) but you have to order in bulk of 5. 66 | 67 | 68 | 69 | ⚠️⚠️ Playbot is not a plug-and-play project as it requires a lot of soldering and custom connectors/wires. ⚠️⚠️ 70 | 71 | **This is a first prototype, and I'm already working on a V2 with only one PCB and less soldering involved.** 72 | 73 | ## Recommended tools 74 | 75 | | Tool | Notes | 76 | |------|-------| 77 | | Soldering Iron | | 78 | | Flux | For clean solder joints | 79 | | Heat Shrink Tubing | 1/2 mm| 80 | | Precision Screwdrivers | | 81 | | JST PH Crimp Tool | For JST PH connectors | 82 | | JST SH Crimp Tool | Optional - expensive, can use pre-crimped wires instead | 83 | | Kapton Tape | | 84 | | Wire strippers | (20-30 AWG) | 85 | | Tweezers || 86 | | Helping hands with magnifying glass| | 87 | 88 | ## 3D Printed Parts 89 | 90 | STL files are available [here](../hardware/3dPrinted_Parts) 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | I print these on a Bambu Lab A1 mini with generic PLA. 99 | 100 | ## Custom PCBs 101 | ### Power Board 102 | 103 | 1-cell LiPo charger (MCP73831) with 5V boost converter (TPS61023) and fuel gauge (MAX17048). The board is capable of load-sharing and can be plugged in while running the robot. 104 | This board will also charge the Playdate when the robot is active. 105 | Red LED when charging/Yellow LED when fully charged. 106 | 107 | ### Main Board 108 | 109 | 110 | 111 | A board that connects various sensors/motors/servos and distributes power to the Teensy. 112 | You will need special low-profile headers to fit the Teensy inside the case. 113 | 114 | ⚠️⚠️ **Important: To protect your battery-powered Teensy, it is recommended to cut the USB power trace.** ⚠️⚠️ 115 | 116 | 117 | 118 | - Solder male headers on Teensy main side pins and USB host pins. 119 | 120 | 121 | 122 | - Solder female low-profile headers on the Playbot main board. 123 | 124 | 125 | 126 | ## Assembly 127 | 128 | ### Testing 129 | 130 | ⚠️⚠️ For every step, I strongly advise you to double-check your connections, flash the test [firmware](https://github.com/GuybrushTreep/PlayBot/tree/main/src/Playbot_SensorsTest) to the Teensy, plug in your components, and test the boot sequence to ensure everything is working correctly. ⚠️⚠️ 131 | 132 | 133 | 134 | ### Top Frame 135 | 136 | 137 | 138 | #### Servo Modification: 139 | 140 | 141 | 142 | - Cut the left plastic ear flush. 143 | - Open the servo case by removing the 4 screws. 144 | - Cut/desolder the wires. Take note of the color code: **brown** -> **GND** | **red** -> **V+** | **orange** -> **signal**. 145 | - Solder new wires with a JST SH connector. Pay attention to the **servo** connector wiring. 146 | 147 | Also, due to tight space constraints, the servo horn needs to be modified. 148 | 149 | 150 | 151 | The hole is enlarged with a soldering iron and press-fitted onto the servo axle while soft, center the servo position with the test firmware before screwing it. 152 | 153 | #### Brass Inserts 154 | 155 | Check this excellent [tutorial](https://www.youtube.com/watch?v=KC1LLU54DKU&ab_channel=GeekDetour) for heated brass inserts on PLA. 156 | 157 | - Add 5 brass inserts on the top of the frame: 158 | 159 | 160 | 161 | - And 4 others on the frame's bottom: 162 | 163 | 164 | 165 | - Screw the mainboard directly into the frame using M2 * 4 screws. 166 | - Press-fit the 2 ball bearings into the frame 167 | 168 | 169 | - Solder the power switch and USB-C input to the power management board. Press-fit the USB-C plug into the bottom frame. You can add a bit of hot glue to secure it to the frame. 170 | 171 | 172 | - You can now screw the power board to the frame's bottom. 173 | 174 | 175 | - The lipo baterry slides into a dedicated socket on the frame. 176 | 177 | 178 | ### Middle Frame 179 | 180 | 181 | - Add 4 brass inserts. 182 | 183 | 184 | 185 | 186 | #### DRV8835 187 | The DRV8835 board is attached directly in the middle of the part with double-sided adhesive. 188 | 189 | 190 | 191 | - Solder pins MODE, AIN1, AIN2, BIN1, BIN2, VCC, and GND to a 7-pin JST SH connector for the motors Connector. 192 | 193 | Please check the [Pololu](https://www.pololu.com/product/2135) page for more information about this board. 194 | 195 | 196 | 197 | - Create a custom plug for motors. Power comes from the DRV8835 board (M1 and M2), and the 4 other wires are for encoder feedback (ENC1, ENC2). 198 | 199 | 200 | 201 | 202 | - Before adding motors, you must attach the middle and top frame together with 2 M2 screws. 203 | 204 | 205 | 206 | #### Power Harness 207 | 208 | Create a power harness with JST **PH** connectors to power the main board and DRV8835 from the power board's power output. 209 | 210 | 211 | 212 | ** note: I replaced the Main board's PH connector with simple solder pads on the last revision** 213 | 214 | ## Lower Frame 215 | 216 | 217 | ### Ball Caster Wheels 218 | 219 | - Remove screws, insert the ball caster wheels, and screw them back to the frame. 220 | 221 | 222 | 223 | ## Console Base 224 | 225 | 226 | 227 | ### USB 228 | 229 | - Create a USB plug connected to a 4-pin JST **PH** connector. 230 | You can add a bit of hot glue to secure the PH connector. 231 | 232 | 233 | 234 | - Create a 4-pin **PH** to **SH** connector that will connect to **USERIAL** connector on the main board. 235 | 236 | 237 | 238 | 239 | 240 | The USB plugs comes with little plastic rings that you can glue to the part. 241 | 242 | - Press-fit magnets into the dock. 243 | 244 | 245 | 246 | ## Shell 247 | 248 | 249 | ### TEMT6000 Light Sensor 250 | 251 | - Solder a 3-pin JST **SH** connector that connects to **TEMT** connector on the main board. 252 | 253 | 254 | 255 | - Screw and bolt it to the shell. 256 | 257 | 258 | 259 | ### SPDT Switch 260 | 261 | - Screw and bolt the switch to the shell. 262 | 263 | 264 | 265 | ### TOF Sensors 266 | 267 | - Solder a 4-pin JST **SH** connector that connects to **TOF1** and **TOF2** on the main board. You can cut RX/TX wires as we use I²C through a 2-channel multiplexer. 268 | 269 | 270 | 271 | - Attach both sensors at the front and back of the shell with M2.5x6 screws and nylon nuts. 272 | 273 | 274 | 275 | 276 | ## Putting Everything Together 277 | 278 | At this point, you're almost done. Congratulations! I assume you've already tested every component by flashing the test firmware. 279 | 280 | ⚠️⚠️ Don't forget to flash the micro SD card with the [animation files](https://github.com/GuybrushTreep/Playbot_Rig/tree/main/Animations/Export). You can still access the SD card from your computer with the help of this library: https://github.com/KurtE/MTP_Teensy ⚠️⚠️ 281 | 282 | If no errors show up, you're good to assemble the remaining parts in this order: 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | This is the tricky part, as everything will be tightly packed inside the shell. 291 | 292 | Attach the screen base with M3 screw, do not overtighten it. 293 | Cut a Servo pull rod at 20 mm and attach it to the servo's arm with a connector. 294 | 295 | 296 | 297 | Congratulations, your Playbot is now complete! 298 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /images/App.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/App.jpg -------------------------------------------------------------------------------- /images/Assembly_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_01.jpg -------------------------------------------------------------------------------- /images/Assembly_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_02.jpg -------------------------------------------------------------------------------- /images/Assembly_03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_03.jpg -------------------------------------------------------------------------------- /images/Assembly_04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_04.jpg -------------------------------------------------------------------------------- /images/Assembly_05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_05.jpg -------------------------------------------------------------------------------- /images/Assembly_BallCaster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_BallCaster.jpg -------------------------------------------------------------------------------- /images/Assembly_Base_Magnet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_Base_Magnet.jpg -------------------------------------------------------------------------------- /images/Assembly_Base_Plug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_Base_Plug.jpg -------------------------------------------------------------------------------- /images/Assembly_Base_USB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_Base_USB.jpg -------------------------------------------------------------------------------- /images/Assembly_ITR.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_ITR.jpg -------------------------------------------------------------------------------- /images/Assembly_MainBoard_Headers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_MainBoard_Headers.jpg -------------------------------------------------------------------------------- /images/Assembly_MiddleFrame.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_MiddleFrame.jpg -------------------------------------------------------------------------------- /images/Assembly_MiddleFrame_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_MiddleFrame_02.jpg -------------------------------------------------------------------------------- /images/Assembly_MiddleFrame_Inserts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_MiddleFrame_Inserts.jpg -------------------------------------------------------------------------------- /images/Assembly_Motors.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_Motors.jpg -------------------------------------------------------------------------------- /images/Assembly_PowerBoard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_PowerBoard.jpg -------------------------------------------------------------------------------- /images/Assembly_PowerBoard_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_PowerBoard_02.jpg -------------------------------------------------------------------------------- /images/Assembly_PowerHarness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_PowerHarness.jpg -------------------------------------------------------------------------------- /images/Assembly_Switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_Switch.jpg -------------------------------------------------------------------------------- /images/Assembly_TEMT.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_TEMT.jpg -------------------------------------------------------------------------------- /images/Assembly_TEMT_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_TEMT_02.jpg -------------------------------------------------------------------------------- /images/Assembly_TOF_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_TOF_01.jpg -------------------------------------------------------------------------------- /images/Assembly_Teensy_Headers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_Teensy_Headers.jpg -------------------------------------------------------------------------------- /images/Assembly_Tof_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Assembly_Tof_02.jpg -------------------------------------------------------------------------------- /images/Base_Print.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Base_Print.jpg -------------------------------------------------------------------------------- /images/Blender_Rig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Blender_Rig.gif -------------------------------------------------------------------------------- /images/ConsoleBase_Turn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/ConsoleBase_Turn.gif -------------------------------------------------------------------------------- /images/DRV8835.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/DRV8835.jpg -------------------------------------------------------------------------------- /images/ITR20001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/ITR20001.jpg -------------------------------------------------------------------------------- /images/LipoSocket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/LipoSocket.jpg -------------------------------------------------------------------------------- /images/LowerFrame_Print.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/LowerFrame_Print.jpg -------------------------------------------------------------------------------- /images/LowerStage_Turn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/LowerStage_Turn.gif -------------------------------------------------------------------------------- /images/MiddleFrame_Print.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/MiddleFrame_Print.jpg -------------------------------------------------------------------------------- /images/MiddleStage_Turn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/MiddleStage_Turn.gif -------------------------------------------------------------------------------- /images/Motors.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Motors.jpg -------------------------------------------------------------------------------- /images/PCB_Options_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/PCB_Options_01.jpg -------------------------------------------------------------------------------- /images/PCB_Options_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/PCB_Options_02.jpg -------------------------------------------------------------------------------- /images/Plastic_ring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Plastic_ring.jpg -------------------------------------------------------------------------------- /images/PlayBot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/PlayBot.gif -------------------------------------------------------------------------------- /images/PlayBot_MainBoard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/PlayBot_MainBoard.jpg -------------------------------------------------------------------------------- /images/PlayBot_PowerBoard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/PlayBot_PowerBoard.jpg -------------------------------------------------------------------------------- /images/PlayBot_WireFrame.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/PlayBot_WireFrame.jpg -------------------------------------------------------------------------------- /images/PlayBot_WireFrame_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/PlayBot_WireFrame_02.jpg -------------------------------------------------------------------------------- /images/PlayBot_exploded.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/PlayBot_exploded.jpg -------------------------------------------------------------------------------- /images/Robot_01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Robot_01.gif -------------------------------------------------------------------------------- /images/Robot_02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Robot_02.gif -------------------------------------------------------------------------------- /images/Robot_03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Robot_03.gif -------------------------------------------------------------------------------- /images/Robot_04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Robot_04.gif -------------------------------------------------------------------------------- /images/Robot_Back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Robot_Back.png -------------------------------------------------------------------------------- /images/Robot_Front.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Robot_Front.jpeg -------------------------------------------------------------------------------- /images/Robot_Rig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Robot_Rig.gif -------------------------------------------------------------------------------- /images/ScreenAttachment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/ScreenAttachment.jpg -------------------------------------------------------------------------------- /images/Servo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Servo.jpg -------------------------------------------------------------------------------- /images/Servo_Horn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Servo_Horn.jpg -------------------------------------------------------------------------------- /images/Shell_Print.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Shell_Print.jpg -------------------------------------------------------------------------------- /images/Shell_Turn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Shell_Turn.gif -------------------------------------------------------------------------------- /images/Teensy_CutTrace.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Teensy_CutTrace.jpg -------------------------------------------------------------------------------- /images/TestFirmware.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/TestFirmware.jpg -------------------------------------------------------------------------------- /images/TopBoard_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/TopBoard_01.jpg -------------------------------------------------------------------------------- /images/TopFrame_BrassInserts_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/TopFrame_BrassInserts_01.jpg -------------------------------------------------------------------------------- /images/TopFrame_BrassInserts_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/TopFrame_BrassInserts_02.jpg -------------------------------------------------------------------------------- /images/UpperFrame_Print.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/UpperFrame_Print.jpg -------------------------------------------------------------------------------- /images/UpperStage_Turn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/UpperStage_Turn.gif -------------------------------------------------------------------------------- /images/Video.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuybrushTreep/PlayBot/35bd5bc119dfa886ccf8f72648d6cc75e3371580/images/Video.jpg -------------------------------------------------------------------------------- /src/PlayBot/AnimationManager.h: -------------------------------------------------------------------------------- 1 | // AnimationManager.h 2 | #ifndef ANIMATION_MANAGER_H 3 | #define ANIMATION_MANAGER_H 4 | 5 | #include "Config.h" 6 | #include "Debug.h" 7 | #include 8 | 9 | // Manages robot animations loaded from SD card 10 | // Animations are triggered by Playdate messages: 11 | // - "a/filepath" : Start animation from SD file 12 | // - "x" : Stop current animation 13 | // Each animation frame contains: index/head_position/right_wheel/left_wheel 14 | class AnimationManager { 15 | private: 16 | // Static buffer sizes for memory efficiency 17 | static const size_t BUFFER_SIZE = 512; // SD card read buffer 18 | static const uint8_t MAX_LINE_LENGTH = 64; // Max animation line length 19 | 20 | // Static buffers for string operations 21 | char buffer[BUFFER_SIZE]; // Main file reading buffer 22 | char lineBuf[MAX_LINE_LENGTH]; // Current line buffer 23 | char wheelRightBuf[8]; // Right wheel command buffer 24 | char wheelLeftBuf[8]; // Left wheel command buffer 25 | 26 | // File and animation state 27 | File currentFile; // Current animation file handle 28 | const char* currentAnimation; // Path to current animation 29 | size_t bufferPos; // Position in read buffer 30 | size_t bytesInBuffer; // Valid bytes in buffer 31 | bool isPlaying; // Animation playback state 32 | uint32_t lastFrameTime; // Last frame timestamp 33 | 34 | // Hardware references 35 | Servo& headServo; // Head servo control 36 | DRV8835MotorShield& motors; // Motor control 37 | Encoder& encoderLeft; // Left wheel encoder 38 | Encoder& encoderRight; // Right wheel encoder 39 | 40 | // Fast integer parsing without String conversion 41 | inline int32_t parseIntFast(const char* str, size_t len) { 42 | int32_t result = 0; 43 | bool negative = false; 44 | size_t i = 0; 45 | 46 | if (str[0] == '-') { 47 | negative = true; 48 | i++; 49 | } 50 | 51 | for (; i < len && str[i] >= '0' && str[i] <= '9'; i++) { 52 | result = result * 10 + (str[i] - '0'); 53 | } 54 | 55 | return negative ? -result : result; 56 | } 57 | 58 | // Optimized line reading from SD card 59 | bool readNextLine() { 60 | size_t lineLen = 0; 61 | 62 | while (lineLen < MAX_LINE_LENGTH - 1) { 63 | // Refill buffer if needed 64 | if (bufferPos >= bytesInBuffer) { 65 | bytesInBuffer = currentFile.read(buffer, BUFFER_SIZE); 66 | bufferPos = 0; 67 | if (bytesInBuffer == 0) return false; 68 | } 69 | 70 | char c = buffer[bufferPos++]; 71 | if (c == '\n') { 72 | lineBuf[lineLen] = '\0'; 73 | return true; 74 | } 75 | if (c != '\r') { 76 | lineBuf[lineLen++] = c; 77 | } 78 | } 79 | 80 | lineBuf[lineLen] = '\0'; 81 | return true; 82 | } 83 | 84 | // Parse a single animation frame line 85 | // Format: index/head_position/right_wheel/left_wheel 86 | void parseAnimationLine() { 87 | char* ptr = lineBuf; 88 | char* nextSlash; 89 | 90 | // Skip frame index 91 | nextSlash = strchr(ptr, '/'); 92 | if (!nextSlash) return; 93 | ptr = nextSlash + 1; 94 | 95 | // Parse head servo position (500-2500 µs) 96 | nextSlash = strchr(ptr, '/'); 97 | if (!nextSlash) return; 98 | *nextSlash = '\0'; 99 | int headPos = parseIntFast(ptr, nextSlash - ptr); 100 | ptr = nextSlash + 1; 101 | 102 | // Parse right wheel speed 103 | nextSlash = strchr(ptr, '/'); 104 | if (!nextSlash) return; 105 | size_t len = nextSlash - ptr; 106 | if (len < sizeof(wheelRightBuf)) { 107 | memcpy(wheelRightBuf, ptr, len); 108 | wheelRightBuf[len] = '\0'; 109 | } 110 | ptr = nextSlash + 1; 111 | 112 | // Parse left wheel speed 113 | len = strlen(ptr); 114 | if (len < sizeof(wheelLeftBuf)) { 115 | memcpy(wheelLeftBuf, ptr, len + 1); 116 | } 117 | 118 | // Apply head position if motion is enabled 119 | if (MOTION_ENABLED && headPos >= 500 && headPos <= 2500) { 120 | headServo.writeMicroseconds(headPos); 121 | } 122 | } 123 | 124 | public: 125 | // Initialize manager with required hardware references 126 | AnimationManager(Servo& servo, DRV8835MotorShield& motorController, 127 | Encoder& encLeft, Encoder& encRight) 128 | : headServo(servo) 129 | , motors(motorController) 130 | , encoderLeft(encLeft) 131 | , encoderRight(encRight) 132 | , isPlaying(false) 133 | , lastFrameTime(0) 134 | , currentAnimation(nullptr) 135 | , bufferPos(0) 136 | , bytesInBuffer(0) { 137 | // Initialize wheel command buffers to "0" 138 | wheelRightBuf[0] = '0'; 139 | wheelRightBuf[1] = '\0'; 140 | wheelLeftBuf[0] = '0'; 141 | wheelLeftBuf[1] = '\0'; 142 | } 143 | 144 | // Check if animation is currently playing 145 | bool isAnimationPlaying() const { 146 | return isPlaying; 147 | } 148 | 149 | // Start new animation from SD card 150 | // Called when Playdate sends "a/filepath" message 151 | void startAnimation(const char* animationPath) { 152 | if (isPlaying) stopAnimation(); 153 | 154 | currentAnimation = animationPath; 155 | currentFile = SD.open(animationPath); 156 | 157 | if (!currentFile) { 158 | DEBUG_PRINT(DEBUG_WARNING, "Failed to open animation"); 159 | return; 160 | } 161 | 162 | isPlaying = true; 163 | bufferPos = bytesInBuffer = 0; 164 | encoderLeft.write(0); 165 | encoderRight.write(0); 166 | 167 | if (MOTION_ENABLED) { 168 | headServo.attach(SERVO_PIN); 169 | } 170 | } 171 | 172 | // Stop current animation playback 173 | // Called when Playdate sends "x" message 174 | void stopAnimation() { 175 | if (!isPlaying) return; 176 | 177 | headServo.detach(); 178 | currentFile.close(); 179 | encoderLeft.write(0); 180 | encoderRight.write(0); 181 | motors.setM1Speed(0); 182 | motors.setM2Speed(0); 183 | isPlaying = false; 184 | // Reset wheel commands to "0" 185 | wheelRightBuf[0] = '0'; 186 | wheelRightBuf[1] = '\0'; 187 | wheelLeftBuf[0] = '0'; 188 | wheelLeftBuf[1] = '\0'; 189 | bufferPos = bytesInBuffer = 0; 190 | } 191 | 192 | // Process animation frame - called in main loop 193 | // Maintains ~30fps timing (33ms per frame) 194 | void update() { 195 | if (!isPlaying) return; 196 | 197 | uint32_t currentTime = millis(); 198 | if (currentTime - lastFrameTime > 33) { 199 | if (currentFile && readNextLine()) { 200 | parseAnimationLine(); 201 | lastFrameTime = currentTime; 202 | } else { 203 | stopAnimation(); 204 | } 205 | } 206 | } 207 | 208 | // Get current wheel commands for motor controller 209 | void getWheelCommands(String& right, String& left) { 210 | right = wheelRightBuf; 211 | left = wheelLeftBuf; 212 | } 213 | }; 214 | 215 | #endif // ANIMATION_MANAGER_H -------------------------------------------------------------------------------- /src/PlayBot/BatteryManager.h: -------------------------------------------------------------------------------- 1 | // BatteryManager.h 2 | #ifndef BATTERY_MANAGER_H 3 | #define BATTERY_MANAGER_H 4 | 5 | #include "Config.h" 6 | #include "Debug.h" 7 | #include "HardwareConfig.h" 8 | #include "LEDController.h" 9 | #include "AnimationManager.h" 10 | #include 11 | 12 | // Manages battery monitoring and charging state 13 | // Communicates with Playdate using following messages: 14 | // - "msg b/percent/voltage/charging" : Battery status update 15 | // - "msg p/1" : Charging started 16 | // - "msg p/0" : Charging stopped 17 | class BatteryManager { 18 | private: 19 | // Hardware state tracking 20 | bool max1704x_initialized; // MAX17048 battery gauge initialization status 21 | bool chargingState; // Current charging status (true = charging) 22 | unsigned long lastBatteryCheckTime; 23 | bool firstReadingTaken; // Tracks if we have initial reading 24 | int consecutiveReadings; // Count of consistent readings 25 | 26 | // Hardware references 27 | USBSerial_BigBuffer& userial; // Serial communication for Playdate messages 28 | LEDController& ledController; // LED status indicator 29 | AnimationManager& animationManager; // Used to avoid battery updates during animations 30 | 31 | // Smoothing filters for stable readings 32 | Smoothed smoothedVoltage; 33 | Smoothed smoothedChargeRate; 34 | 35 | // Logs battery errors to debug system 36 | void recordBatteryError(const String& errorMessage) { 37 | setupErrors.push_back(errorMessage.c_str()); 38 | DEBUG_PRINT(DEBUG_WARNING, "Battery Manager Error: " + errorMessage); 39 | } 40 | 41 | // Sets up data smoothing for voltage readings 42 | void initializeBatteryDetection() { 43 | smoothedVoltage.begin(SMOOTHED_AVERAGE, 2); 44 | smoothedChargeRate.begin(SMOOTHED_AVERAGE, 2); 45 | DEBUG_PRINT(DEBUG_INFO, "Battery detection initialized"); 46 | } 47 | 48 | public: 49 | // Constructor initializes all dependencies 50 | BatteryManager(USBSerial_BigBuffer& serial, LEDController& led, AnimationManager& anim) 51 | : max1704x_initialized(false) 52 | , chargingState(false) 53 | , lastBatteryCheckTime(0) 54 | , firstReadingTaken(false) 55 | , consecutiveReadings(0) 56 | , userial(serial) 57 | , ledController(led) 58 | , animationManager(anim) { 59 | } 60 | 61 | // Initialize battery monitoring system 62 | // Returns true if MAX17048 gauge initialized successfully 63 | bool initialize() { 64 | DEBUG_PRINT(DEBUG_INFO, "Initializing battery systems"); 65 | delay(500); // Allow gauge to stabilize 66 | 67 | initializeBatteryDetection(); 68 | 69 | if (maxlipo.begin()) { 70 | DEBUG_PRINT(DEBUG_INFO, "MAX1704X initialized successfully"); 71 | max1704x_initialized = true; 72 | return true; 73 | } else { 74 | recordBatteryError("Battery gauge (MAX1704X) initialization failed"); 75 | max1704x_initialized = false; 76 | return false; 77 | } 78 | } 79 | 80 | // Check for USB power connection changes 81 | // Sends "msg p/1" or "msg p/0" to Playdate on state change 82 | void detectBatteryCharging() { 83 | unsigned long currentTime = millis(); 84 | if (currentTime - lastBatteryCheckTime >= BATTERY_CHECK_INTERVAL) { 85 | // Read USB detection pin voltage 86 | int adcValue = analogRead(USB_DETECT_PIN); 87 | float pinVoltage = (adcValue / 1023.0) * 3.3; 88 | 89 | bool newChargingState = (pinVoltage > 1.5); // USB present if > 1.5V 90 | 91 | if (newChargingState != chargingState) { 92 | chargingState = newChargingState; 93 | 94 | // Disable robot motion while charging 95 | MOTION_ENABLED = !chargingState; 96 | 97 | if (chargingState) { 98 | motors.setM1Speed(0); 99 | motors.setM2Speed(0); 100 | ledController.setStatusCharging(); 101 | } else { 102 | ledController.setStatusIdle(); 103 | } 104 | 105 | userial.println(chargingState ? "msg p/1" : "msg p/0"); 106 | DEBUG_PRINT(DEBUG_INFO, "Charging state changed: " + 107 | String(chargingState ? "Connected" : "Disconnected") + 108 | " Pin Voltage: " + String(pinVoltage) + 109 | "V Motion: " + String(MOTION_ENABLED ? "Enabled" : "Disabled")); 110 | } 111 | 112 | lastBatteryCheckTime = currentTime; 113 | } 114 | } 115 | 116 | // Format and send battery status to Playdate 117 | // Returns formatted message string 118 | // Format: "msg b/percent/voltage/charging" 119 | char* getBatteryLevel() { 120 | static char message[40]; // Static buffer for message 121 | 122 | // Only send update if no animation is playing 123 | if (!animationManager.isAnimationPlaying()) { 124 | if (max1704x_initialized) { 125 | float voltage = maxlipo.cellVoltage(); 126 | float percent = maxlipo.cellPercent(); 127 | int alertLevel = 0; 128 | if (percent <= BATTERY_CRITICAL_THRESHOLD) { 129 | alertLevel = 2; 130 | 131 | } else if (percent <= BATTERY_LOW_THRESHOLD) { 132 | alertLevel = 1; 133 | } 134 | 135 | 136 | // Format floating point numbers 137 | char percentBuffer[10]; 138 | char voltageBuffer[10]; 139 | dtostrf(percent, 6, 2, percentBuffer); 140 | dtostrf(voltage, 6, 2, voltageBuffer); 141 | 142 | snprintf(message, sizeof(message), "msg b/%s/%s/%d/%d", 143 | percentBuffer, voltageBuffer, chargingState ? 1 : 0, alertLevel); 144 | DEBUG_PRINT(DEBUG_INFO, "Battery: " + String(voltage) + "V, " + 145 | String(percent) + "%, Charging: " + String(chargingState) + 146 | ", Alert: " + String(alertLevel)); 147 | } else { 148 | snprintf(message, sizeof(message), "msg b/NA/NA/NA/0"); 149 | DEBUG_PRINT(DEBUG_WARNING, "Battery monitoring not available"); 150 | message[0] = '\0'; // Empty message if animation playing 151 | } 152 | 153 | userial.println(message); 154 | } else { 155 | message[0] = '\0'; // Empty message if animation playing 156 | } 157 | 158 | return message; 159 | } 160 | 161 | // Getters for battery state 162 | bool isCharging() const { return chargingState; } 163 | float getVoltage() const { return max1704x_initialized ? maxlipo.cellVoltage() : 0.0; } 164 | }; 165 | 166 | #endif // BATTERY_MANAGER_H -------------------------------------------------------------------------------- /src/PlayBot/CommunicationManager.h: -------------------------------------------------------------------------------- 1 | // CommunicationManager.h 2 | #ifndef COMMUNICATION_MANAGER_H 3 | #define COMMUNICATION_MANAGER_H 4 | 5 | #include "Config.h" 6 | #include "Debug.h" 7 | #include "AnimationManager.h" 8 | #include "BatteryManager.h" 9 | #include "SensorManager.h" 10 | #include "MotorController.h" 11 | 12 | // Manages bidirectional communication between Teensy and Playdate 13 | // Playdate -> Teensy commands: 14 | // - "a/filepath" : Start animation from SD 15 | // - "b" : Request battery status 16 | // - "d" : Request sensor data 17 | // - "v" : Verify connection 18 | // - "t/turns/direction" : Turn robot 19 | // - "x" : Stop animation 20 | // 21 | // Teensy -> Playdate messages: 22 | // - "msg b/percent/voltage/charging" : Battery status 23 | // - "msg p/0|1" : Power state change 24 | // - "msg s/" : Connection confirmation 25 | // - "msg d/..." : Sensor data packet 26 | // - "msg r/1" : Rotation completed 27 | // - "msg e/1" : Edge detected 28 | // - "msg w/1" : Collision detected 29 | // - "msg l/0|1" : Light level change 30 | class CommunicationManager { 31 | private: 32 | // Hardware and subsystem references 33 | USBHost& myusb; // USB host interface 34 | USBSerial_BigBuffer& userial; // Serial communication 35 | AnimationManager& animationManager; // Animation control 36 | BatteryManager& batteryManager; // Battery monitoring 37 | SensorManager& sensorManager; // Sensor readings 38 | MotorController& motorController; // Motor control 39 | 40 | // Communication settings 41 | uint32_t baud; // Current baud rate 42 | uint32_t format; // Serial format 43 | char buffer[512]; // Message buffer 44 | 45 | public: 46 | // Initialize with references to all required subsystems 47 | CommunicationManager( 48 | USBHost& usb, 49 | USBSerial_BigBuffer& serial, 50 | AnimationManager& anim, 51 | BatteryManager& battery, 52 | SensorManager& sensor, 53 | MotorController& motor 54 | ) : myusb(usb), 55 | userial(serial), 56 | animationManager(anim), 57 | batteryManager(battery), 58 | sensorManager(sensor), 59 | motorController(motor), 60 | baud(USBBAUD), 61 | format(USBHOST_SERIAL_8N1) 62 | { 63 | } 64 | 65 | // Initialize USB communication 66 | void initialize() { 67 | DEBUG_PRINT(DEBUG_INFO, "Initializing USB"); 68 | myusb.begin(); 69 | userial.begin(USBBAUD); 70 | DEBUG_PRINT(DEBUG_INFO, "USB initialized"); 71 | delay(500); // Allow USB to stabilize 72 | } 73 | 74 | // Monitor for baud rate changes from Playdate 75 | // Needed to maintain stable communication 76 | void handleBaudRateChange() { 77 | uint32_t cur_usb_baud = Serial.baud(); 78 | if (cur_usb_baud && (cur_usb_baud != baud)) { 79 | baud = cur_usb_baud; 80 | DEBUG_PRINT(DEBUG_INFO, "Baud rate changed: " + String(baud)); 81 | // Special case for 57600 baud - use 58824 instead 82 | userial.begin(baud == 57600 ? 58824 : baud); 83 | } 84 | } 85 | 86 | // Process incoming messages from Playdate 87 | // This is the main communication handler 88 | void readFromUSBHostSerialAndWriteToSerial() { 89 | uint16_t rd = userial.available(); 90 | if (rd > 0) { 91 | // Limit message size to prevent buffer overflow 92 | if (rd > 80) rd = 80; 93 | userial.readBytes((char*)buffer, rd); 94 | 95 | // Ignore 'c/' commands (handled elsewhere) 96 | if (buffer[0] != 'c' || buffer[1] != '/') { 97 | char messageType = buffer[0]; 98 | 99 | switch(messageType) { 100 | case 'a': // Start animation 101 | handleAnimationMessage(); 102 | break; 103 | case 'b': // Battery status request 104 | batteryManager.getBatteryLevel(); 105 | break; 106 | case 'd': // Sensor data request 107 | sensorManager.sendSensorData(); 108 | break; 109 | case 'v': // Connection verification 110 | userial.println("msg s/"); 111 | break; 112 | case 't': // Turn robot command 113 | handleCrankTurns(); 114 | break; 115 | case 'x': // Stop animation 116 | animationManager.stopAnimation(); 117 | motors.setM1Speed(0); 118 | motors.setM2Speed(0); 119 | DEBUG_PRINT(DEBUG_INFO, "Animation stopped by options menu"); 120 | break; 121 | } 122 | } 123 | } 124 | } 125 | 126 | private: 127 | // Handle animation start command from Playdate 128 | // Format: "a/filepath" 129 | void handleAnimationMessage() { 130 | DEBUG_PRINT(DEBUG_INFO, "Handling animation message"); 131 | char* animPath = strchr((char*)buffer, '/'); 132 | if (animPath != NULL) { 133 | animPath++; // Skip the '/' 134 | // Clean non-printable characters 135 | for (int i = 0; animPath[i]; i++) { 136 | if (!isprint(animPath[i])) { 137 | animPath[i] = '\0'; 138 | } 139 | } 140 | animationManager.startAnimation(animPath); 141 | } 142 | DEBUG_PRINT(DEBUG_INFO, "Animation prepared: " + String(animPath)); 143 | } 144 | 145 | // Handle turn command from Playdate 146 | // Format: "t/number_of_turns/direction" 147 | // Direction: 1 = clockwise, -1 = counterclockwise 148 | void handleCrankTurns() { 149 | char* turnData = strchr((char*)buffer, '/') + 1; 150 | int turns = atoi(turnData); 151 | int direction = atoi(strchr(turnData, '/') + 1); 152 | if(turns > 0) { 153 | motorController.rotateRobot(turns, direction); 154 | } 155 | } 156 | }; 157 | 158 | #endif // COMMUNICATION_MANAGER_H -------------------------------------------------------------------------------- /src/PlayBot/Config.h: -------------------------------------------------------------------------------- 1 | // Config.h 2 | #ifndef CONFIG_H 3 | #define CONFIG_H 4 | 5 | #include "PIDConfig.h" 6 | #include "HardwareConfig.h" 7 | 8 | // ================= Communication Settings ================= 9 | #define USBBAUD 115200 10 | #define USB_BUFFER_SIZE 512 11 | 12 | // ================= Light Sensor Configuration ================= 13 | #define LIGHT_CHECK_INTERVAL 200 // ms 14 | #define DARKNESS_THRESHOLD 10 15 | 16 | // ================= Edge Detection Configuration ================= 17 | #define EDGE_THRESHOLD 170 // mm 18 | #define FRONT_SENSOR 1 19 | #define BACK_SENSOR 0 20 | #define EDGE_CHECK_INTERVAL 12 // ms 21 | 22 | // ================= IR Sensor Configuration ================= 23 | #define IR_CHECK_INTERVAL 8 // ms 24 | 25 | // ================= Battery Monitoring Configuration ================= 26 | #define BATTERY_CHECK_INTERVAL 250 // ms 27 | // Battery thresholds 28 | #define BATTERY_LOW_THRESHOLD 15.0f // 15% battery threshold for warning 29 | #define BATTERY_CRITICAL_THRESHOLD 5.0f // 5% battery threshold for shutdown 30 | // ================= Collision Detection Configuration ================= 31 | #define FRONT_COLLISION_THRESHOLD 70 // mm 32 | #define COLLISION_CHECK_INTERVAL 4 // ms 33 | 34 | // ================= Motion Constants ================= 35 | 36 | #define ROTATION_SPEED 200 37 | extern bool MOTION_ENABLED; // Global flag to control all motor movements 38 | // ================= Distance Tracking ================= 39 | #define DISTANCE_UPDATE_INTERVAL 100 // ms 40 | #define DISTANCE_LOG_INTERVAL 10000 // ms 41 | 42 | 43 | #endif // CONFIG_H -------------------------------------------------------------------------------- /src/PlayBot/Debug.h: -------------------------------------------------------------------------------- 1 | // Debug.h 2 | #ifndef DEBUG_H 3 | #define DEBUG_H 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "HardwareConfig.h" 10 | 11 | // Debug system for robot firmware 12 | // Handles logging, error tracking, and setup verification 13 | // Integrates with Playdate communication through 'msg s/' messages 14 | 15 | // ================= Debug Levels ================= 16 | // Hierarchical debug levels from none to verbose 17 | #define DEBUG_NONE 0 // No debug output 18 | #define DEBUG_INFO 1 // Important state changes and events 19 | #define DEBUG_WARNING 2 // Issues that need attention 20 | #define DEBUG_VERBOSE 3 // Detailed operational data 21 | #define CURRENT_DEBUG_LEVEL DEBUG_INFO // Set active debug level 22 | 23 | // ================= Buffer Configuration ================= 24 | // Circular buffer to prevent memory issues with long-running logs 25 | #define LOG_BUFFER_SIZE 50 // Maximum stored log entries 26 | #define LOG_SEND_INTERVAL 1000 // Milliseconds between log transmissions 27 | 28 | // ================= Global Variables ================= 29 | // Circular buffer for runtime logs 30 | static CircularBuffer logBuffer; 31 | 32 | // Timestamp for managing log transmission 33 | static unsigned long lastLogSendTime = 0; 34 | 35 | // Collection of errors encountered during setup 36 | // These trigger 'msg s/' failure messages to Playdate 37 | static std::vector setupErrors; 38 | 39 | // ================= Debug Macro ================= 40 | // Main logging macro - automatically includes level and formats message 41 | // Usage: DEBUG_PRINT(DEBUG_INFO, "Battery voltage: " + String(voltage)); 42 | #define DEBUG_PRINT(level, message) \ 43 | if (level <= CURRENT_DEBUG_LEVEL) { \ 44 | String logMessage = String("DEBUG [") + String(#level) + String("]: ") + String(message); \ 45 | logBuffer.push(logMessage); \ 46 | } 47 | 48 | // ================= Function Implementations ================= 49 | 50 | // Transmits accumulated logs at regular intervals 51 | // Called in main loop to maintain communication flow 52 | inline void sendLogs() { 53 | unsigned long currentTime = millis(); 54 | if (currentTime - lastLogSendTime >= LOG_SEND_INTERVAL) { 55 | while (!logBuffer.isEmpty()) { 56 | Serial.println(logBuffer.shift()); 57 | } 58 | lastLogSendTime = currentTime; 59 | } 60 | } 61 | 62 | // Records setup phase errors 63 | // These errors affect the success/failure message sent to Playdate 64 | inline void recordSetupError(const String& errorMessage) { 65 | setupErrors.push_back(errorMessage.c_str()); 66 | DEBUG_PRINT(DEBUG_WARNING, "Setup Error: " + errorMessage); 67 | } 68 | 69 | // Prints setup error summary and notifies Playdate 70 | // Sends "msg s/1" for success or logs errors for failure 71 | inline void printSetupErrorSummary() { 72 | if (setupErrors.empty()) { 73 | Serial.println("Setup completed successfully with no errors."); 74 | DEBUG_PRINT(DEBUG_INFO, "Setup completed successfully with no errors."); 75 | userial.println("msg s/1"); // Signal success to Playdate 76 | } else { 77 | Serial.println("Setup completed with the following errors:"); 78 | for (const auto& error : setupErrors) { 79 | Serial.println("- " + String(error.c_str())); 80 | } 81 | DEBUG_PRINT(DEBUG_WARNING, "Setup completed with errors:"); 82 | for (const auto& error : setupErrors) { 83 | DEBUG_PRINT(DEBUG_WARNING, "- " + String(error.c_str())); 84 | } 85 | // Note: No explicit failure message sent to Playdate 86 | // Absence of "msg s/1" indicates setup failure 87 | } 88 | } 89 | 90 | #endif // DEBUG_H -------------------------------------------------------------------------------- /src/PlayBot/DistanceTracker.h: -------------------------------------------------------------------------------- 1 | // DistanceTracker.h 2 | #ifndef DISTANCE_TRACKER_H 3 | #define DISTANCE_TRACKER_H 4 | 5 | #include "Config.h" 6 | #include "Debug.h" 7 | #include 8 | 9 | // Class to track total distance traveled by the robot 10 | // Maintains record of distance traveled by both wheels 11 | // and stores/loads the data from persistent storage (SD card) 12 | class DistanceTracker { 13 | private: 14 | // Internal structure to hold distance data 15 | struct DistanceData { 16 | float leftDistance; // Total distance traveled by left wheel (mm) 17 | float rightDistance; // Total distance traveled by right wheel (mm) 18 | float averageDistance; // Average of both wheels (mm) 19 | long lastLeftPosition; // Last encoder position for left wheel 20 | long lastRightPosition; // Last encoder position for right wheel 21 | unsigned long lastUpdateTime; // Timestamp of last update 22 | } totalDistance; 23 | 24 | const char* LOG_FILENAME = "distance.txt"; // File for persistent storage 25 | unsigned long lastDistanceLogTime; // Timestamp for periodic logging 26 | Encoder& encoderLeft; // Reference to left wheel encoder 27 | Encoder& encoderRight; // Reference to right wheel encoder 28 | 29 | public: 30 | // Constructor - initializes tracking with encoders 31 | DistanceTracker(Encoder& left, Encoder& right) 32 | : encoderLeft(left) 33 | , encoderRight(right) 34 | , lastDistanceLogTime(0) { 35 | totalDistance = {0, 0, 0, 0, 0, 0}; 36 | } 37 | 38 | // Initialize distance tracking and load previous data 39 | // Called during system startup to restore previous distance data 40 | void initialize() { 41 | DEBUG_PRINT(DEBUG_INFO, "Starting distance initialization"); 42 | loadFromFile(); // Load previous distance from SD card 43 | // Reset encoder positions for new tracking session 44 | totalDistance.lastLeftPosition = encoderLeft.read(); 45 | totalDistance.lastRightPosition = encoderRight.read(); 46 | totalDistance.lastUpdateTime = millis(); 47 | } 48 | 49 | // Update distance calculations based on encoder readings 50 | // Should be called regularly in the main loop 51 | void update() { 52 | unsigned long currentTime = millis(); 53 | if (currentTime - totalDistance.lastUpdateTime >= DISTANCE_UPDATE_INTERVAL) { 54 | // Get current encoder positions 55 | long currentLeftPos = encoderLeft.read(); 56 | long currentRightPos = encoderRight.read(); 57 | 58 | // Calculate incremental distances since last update 59 | float leftIncrement = abs(currentLeftPos - totalDistance.lastLeftPosition) * MM_PER_TICK; 60 | float rightIncrement = abs(currentRightPos - totalDistance.lastRightPosition) * MM_PER_TICK; 61 | 62 | // Update total distances 63 | totalDistance.leftDistance += leftIncrement; 64 | totalDistance.rightDistance += rightIncrement; 65 | totalDistance.averageDistance = (totalDistance.leftDistance + totalDistance.rightDistance) / 2.0f; 66 | 67 | // Store current positions for next update 68 | totalDistance.lastLeftPosition = currentLeftPos; 69 | totalDistance.lastRightPosition = currentRightPos; 70 | totalDistance.lastUpdateTime = currentTime; 71 | } 72 | } 73 | 74 | // Check if it's time to log distance and do so if needed 75 | void checkAndLog(bool isAnimating) { 76 | unsigned long currentTime = millis(); 77 | if (currentTime - lastDistanceLogTime >= DISTANCE_LOG_INTERVAL && !isAnimating) { 78 | saveToFile(); 79 | lastDistanceLogTime = currentTime; 80 | DEBUG_PRINT(DEBUG_INFO, "Distance auto-logged"); 81 | } 82 | } 83 | 84 | // Get total distance traveled in meters 85 | float getDistanceMeters() const { 86 | return totalDistance.averageDistance / 1000.0f; // Convert mm to m 87 | } 88 | 89 | // Save current distance to SD card 90 | // Handles the persistent storage of distance data 91 | void saveToFile() { 92 | File distanceLog = SD.open(LOG_FILENAME, FILE_WRITE); 93 | if (distanceLog) { 94 | distanceLog.seek(0); 95 | distanceLog.truncate(); 96 | char distanceStr[10]; 97 | dtostrf(getDistanceMeters(), 1, 2, distanceStr); 98 | distanceLog.print(distanceStr); 99 | distanceLog.close(); 100 | DEBUG_PRINT(DEBUG_INFO, "Distance logged: " + String(getDistanceMeters()) + "m"); 101 | } else { 102 | DEBUG_PRINT(DEBUG_WARNING, "Failed to open distance file"); 103 | } 104 | } 105 | 106 | private: 107 | // Load previously saved distance from SD card 108 | // Called during initialization to restore the last saved state 109 | void loadFromFile() { 110 | if (SD.exists(LOG_FILENAME)) { 111 | File distanceLog = SD.open(LOG_FILENAME, FILE_READ); 112 | if (distanceLog) { 113 | String distanceStr = distanceLog.readStringUntil('\n'); 114 | distanceStr.trim(); 115 | float savedDistance = distanceStr.toFloat(); 116 | 117 | // Convert loaded meters to millimeters and set all distances 118 | totalDistance.averageDistance = savedDistance * 1000.0f; 119 | totalDistance.leftDistance = totalDistance.averageDistance; 120 | totalDistance.rightDistance = totalDistance.averageDistance; 121 | 122 | distanceLog.close(); 123 | DEBUG_PRINT(DEBUG_INFO, "Distance loaded: " + String(savedDistance) + "m"); 124 | } 125 | } 126 | } 127 | }; 128 | 129 | #endif // DISTANCE_TRACKER_H -------------------------------------------------------------------------------- /src/PlayBot/HardwareConfig.h: -------------------------------------------------------------------------------- 1 | // HardwareConfig.h 2 | #ifndef HARDWARE_CONFIG_H 3 | #define HARDWARE_CONFIG_H 4 | 5 | #include "PIDConfig.h" 6 | // ================= Pin Assignments ================= 7 | // Motors 8 | #define MOTOR1_PWM_PIN 7 // Left motor PWM 9 | #define MOTOR1_DIR_PIN 6 // Left motor direction 10 | #define MOTOR2_PWM_PIN 9 // Right motor PWM 11 | #define MOTOR2_DIR_PIN 8 // Right motor direction 12 | 13 | // Encoders 14 | #define ENC1_PIN_A 4 // Left encoder A 15 | #define ENC1_PIN_B 5 // Left encoder B 16 | #define ENC2_PIN_A 3 // Right encoder A 17 | #define ENC2_PIN_B 2 // Right encoder B 18 | 19 | // Sensors 20 | #define IR_SENSOR_LEFT_PIN 39 21 | #define IR_SENSOR_RIGHT_PIN 38 22 | #define LIGHT_SENSOR_PIN 27 23 | #define TOF_MULTIPLEXER_ADDR 0x70 // I2C address for ToF multiplexer 24 | 25 | // Status & Control 26 | #define LED_PIN 28 27 | #define LED_COUNT 1 28 | #define SERVO_PIN 23 29 | #define SD_CS_PIN BUILTIN_SDCARD 30 | 31 | // Battery Detection 32 | #define USB_DETECT_PIN 17 33 | #define VOLTAGE_DIVIDER_RATIO (2.0/3.0) // Voltage divider (2k/(1k+2k)) 34 | 35 | // ================= Hardware Constants ================= 36 | // Physical dimensions (mm) 37 | #define WHEEL_DIAMETER 33.5f 38 | #define WHEEL_BASE 81.0f // Distance between wheels 39 | #define ENCODER_PPR 813 // Pulses per revolution 40 | 41 | #define MM_PER_TICK ((PI * WHEEL_DIAMETER) / ENCODER_PPR) 42 | #define TICKS_PER_ROBOT_ROTATION (int)((PI * WHEEL_BASE * ENCODER_PPR) / WHEEL_DIAMETER) 43 | 44 | // Motor characteristics 45 | #define MOTOR_PWM_FREQUENCY 330000 // PWM frequency in Hz 46 | #define MOTOR2_POWER_COMPENSATION 1.0f // Compensation factor for motor 2 47 | #define MOTOR1_POWER_COMPENSATION 1.0f // Compensation factor for motor 1 48 | 49 | // Include libraries 50 | #include 51 | #include 52 | #include 53 | #undef REVERSE // Undefine REVERSE before PID 54 | #include 55 | #include 56 | #include 57 | #include "Adafruit_MAX1704X.h" 58 | #include 59 | 60 | // ================= Hardware Objects & Variables ================= 61 | // PID variables with inline initialization 62 | inline double Input = 0, Output = 0, Setpoint = 0; 63 | inline double Input2 = 0, Output2 = 0, Setpoint2 = 0; 64 | 65 | // Core hardware objects 66 | inline Encoder myEnc(ENC1_PIN_A, ENC1_PIN_B); 67 | inline Encoder myEnc2(ENC2_PIN_A, ENC2_PIN_B); 68 | inline DRV8835MotorShield motors(MOTOR1_PWM_PIN, MOTOR1_DIR_PIN, MOTOR2_PWM_PIN, MOTOR2_DIR_PIN); 69 | 70 | // PID controllers 71 | inline PID myPID(&Input, &Output, &Setpoint, PID_KP, PID_KI, PID_KD, AUTOMATIC); 72 | inline PID myPID2(&Input2, &Output2, &Setpoint2, PID_KP, PID_KI, PID_KD, AUTOMATIC); 73 | 74 | // Peripheral devices 75 | inline WS2812FX ws2812fx(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); 76 | inline Servo headServo; 77 | inline USBHost myusb; 78 | inline USBHub hub1(myusb); 79 | inline USBSerial_BigBuffer userial(myusb, 1); 80 | inline PCA9540BD mux; 81 | inline Adafruit_MAX17048 maxlipo; 82 | 83 | #endif // HARDWARE_CONFIG_H -------------------------------------------------------------------------------- /src/PlayBot/LEDController.h: -------------------------------------------------------------------------------- 1 | // LEDController.h 2 | #ifndef LED_CONTROLLER_H 3 | #define LED_CONTROLLER_H 4 | 5 | #include 6 | #include "Config.h" 7 | 8 | // Controls the WS2812 RGB LED that indicates robot status 9 | // The LED states correspond to messages exchanged between Teensy and Playdate: 10 | // - Charging (Blue breath): Triggered by "msg p/1" message 11 | // - Error (Red fade): Setup or runtime errors 12 | // - Success (Green fade): Successful initialization ("msg s/1") 13 | // - Idle (Pink fade): Default state, robot ready for commands 14 | class LEDController { 15 | private: 16 | WS2812FX& strip; // Reference to WS2812FX LED strip object 17 | uint8_t brightness; // Current LED brightness (5-255) 18 | bool isInitialized; // Tracks if LED has been properly initialized 19 | 20 | public: 21 | // Constructor takes reference to existing WS2812FX object to avoid duplication 22 | explicit LEDController(WS2812FX& ledStrip) 23 | : strip(ledStrip) 24 | , brightness(5) // Start with low brightness to avoid blinding 25 | , isInitialized(false) { 26 | } 27 | 28 | // Initialize LED hardware and set default state 29 | // Returns true if initialization successful 30 | bool initialize() { 31 | strip.init(); 32 | strip.setBrightness(brightness); 33 | strip.setColor(PINK); // Default color - matches Playdate's idle state 34 | // Set up default fade animation 35 | strip.setSegment(0, 0, LED_COUNT-1, FX_MODE_FADE, COLORS(BLACK, PINK), 20, false); 36 | strip.start(); 37 | isInitialized = true; 38 | DEBUG_PRINT(DEBUG_INFO, "LED Controller initialized"); 39 | return true; 40 | } 41 | 42 | // Must be called in main loop to update LED animations 43 | void update() { 44 | if (isInitialized) { 45 | strip.service(); 46 | } 47 | } 48 | 49 | // Set LED to green fade - indicates successful setup 50 | // Called after "msg s/1" is sent to Playdate 51 | void setStatusSuccess() { 52 | if (!isInitialized) return; 53 | strip.setMode(FX_MODE_FADE); 54 | strip.setColor(GREEN); 55 | strip.setSegment(0, 0, LED_COUNT-1, FX_MODE_FADE, COLORS(BLACK, GREEN), 20, false); 56 | strip.start(); 57 | DEBUG_PRINT(DEBUG_INFO, "LED status: Success"); 58 | } 59 | 60 | // Set LED to red fade - indicates error state 61 | // Triggered by setup errors or runtime issues 62 | void setStatusError() { 63 | if (!isInitialized) return; 64 | strip.setMode(FX_MODE_FADE); 65 | strip.setColor(RED); 66 | strip.setSegment(0, 0, LED_COUNT-1, FX_MODE_FADE, COLORS(BLACK, RED), 20, false); 67 | strip.start(); 68 | DEBUG_PRINT(DEBUG_INFO, "LED status: Error"); 69 | } 70 | 71 | // Set LED to blue breathing effect - indicates charging 72 | // Triggered when "msg p/1" is sent to Playdate 73 | void setStatusCharging() { 74 | if (!isInitialized) return; 75 | strip.setMode(FX_MODE_BREATH); 76 | strip.setColor(BLUE); 77 | strip.setSegment(0, 0, LED_COUNT-1, FX_MODE_BREATH, COLORS(BLACK, BLUE), 1000, false); 78 | strip.start(); 79 | DEBUG_PRINT(DEBUG_INFO, "LED status: Charging"); 80 | } 81 | 82 | // Set LED to pink fade - indicates idle/ready state 83 | // Default state when robot is operational 84 | void setStatusIdle() { 85 | if (!isInitialized) return; 86 | strip.setMode(FX_MODE_FADE); 87 | strip.setColor(PINK); 88 | strip.setSegment(0, 0, LED_COUNT-1, FX_MODE_FADE, COLORS(BLACK, PINK), 20, false); 89 | strip.start(); 90 | DEBUG_PRINT(DEBUG_INFO, "LED status: Idle"); 91 | } 92 | 93 | // Adjust LED brightness based on ambient light 94 | // Syncs with Playdate's light sensor handling ("msg l/0" and "msg l/1") 95 | void adjustBrightness(bool isDark) { 96 | setBrightness(isDark ? 50 : 10); 97 | } 98 | 99 | // Set specific brightness level (5-255) 100 | void setBrightness(uint8_t level) { 101 | if (!isInitialized) return; 102 | brightness = level; 103 | strip.setBrightness(brightness); 104 | DEBUG_PRINT(DEBUG_VERBOSE, "LED brightness set to: " + String(brightness)); 105 | } 106 | 107 | // Check if LED controller is ready 108 | bool isReady() const { return isInitialized; } 109 | }; 110 | 111 | #endif // LED_CONTROLLER_H -------------------------------------------------------------------------------- /src/PlayBot/MotorController.h: -------------------------------------------------------------------------------- 1 | // MotorController.h 2 | #ifndef MOTOR_CONTROLLER_H 3 | #define MOTOR_CONTROLLER_H 4 | 5 | #include "Config.h" 6 | #include "Debug.h" 7 | #include "HardwareConfig.h" 8 | 9 | // Manages robot motors, encoders and PID control 10 | // Handles: 11 | // - Direct motor control for animations 12 | // - PID-controlled precise movements 13 | // - Robot rotation (triggered by Playdate "t/turns/direction") 14 | // - Motor safety and power management 15 | class MotorController { 16 | private: 17 | // Hardware references 18 | DRV8835MotorShield& motors; // Motor driver 19 | Encoder& encoderRight; // Right motor encoder 20 | Encoder& encoderLeft; // Left motor encoder 21 | PID& pidRight; // Right motor PID controller 22 | PID& pidLeft; // Left motor PID controller 23 | double& inputRight; // Right encoder input 24 | double& inputLeft; // Left encoder input 25 | double& setpointRight; // Right motor target 26 | double& setpointLeft; // Left motor target 27 | double& outputRight; // Right motor output 28 | double& outputLeft; // Left motor output 29 | long prevT; // Previous update time 30 | 31 | public: 32 | // Initialize controller with all required components 33 | MotorController( 34 | DRV8835MotorShield& motorsRef, 35 | Encoder& encRight, 36 | Encoder& encLeft, 37 | PID& pidRight, 38 | PID& pidLeft, 39 | double& setpRight, 40 | double& setpLeft, 41 | double& inRight, 42 | double& inLeft, 43 | double& outRight, 44 | double& outLeft 45 | ) : motors(motorsRef), 46 | encoderRight(encRight), 47 | encoderLeft(encLeft), 48 | pidRight(pidRight), 49 | pidLeft(pidLeft), 50 | inputRight(inRight), 51 | inputLeft(inLeft), 52 | setpointRight(setpRight), 53 | setpointLeft(setpLeft), 54 | outputRight(outRight), 55 | outputLeft(outLeft), 56 | prevT(0) 57 | { 58 | } 59 | 60 | // Initialize motor hardware and PID controllers 61 | void initialize() { 62 | DEBUG_PRINT(DEBUG_INFO, "Initializing motors and PID controllers"); 63 | 64 | // Configure motor PWM frequency to reduce audible noise 65 | analogWriteFrequency(MOTOR1_PWM_PIN, MOTOR_PWM_FREQUENCY); 66 | analogWriteFrequency(MOTOR1_DIR_PIN, MOTOR_PWM_FREQUENCY); 67 | analogWriteFrequency(MOTOR2_PWM_PIN, MOTOR_PWM_FREQUENCY); 68 | analogWriteFrequency(MOTOR2_DIR_PIN, MOTOR_PWM_FREQUENCY); 69 | 70 | // Set motor directions (LEFT motor is flipped) 71 | motors.flipM1(true); // LEFT 72 | motors.flipM2(false); 73 | 74 | // Configure PID controllers for both motors 75 | pidRight.SetTunings(PID_KP, PID_KI, PID_KD); 76 | pidRight.SetOutputLimits(-1023, 1023); 77 | pidRight.SetMode(AUTOMATIC); 78 | pidRight.SetSampleTime(1); 79 | 80 | pidLeft.SetTunings(PID_KP, PID_KI, PID_KD); 81 | pidLeft.SetOutputLimits(-1023, 1023); 82 | pidLeft.SetMode(AUTOMATIC); 83 | pidLeft.SetSampleTime(1); 84 | 85 | DEBUG_PRINT(DEBUG_INFO, "Motors and PID controllers initialized"); 86 | } 87 | 88 | // Update timing variables for PID calculations 89 | void updateTimers() { 90 | long currT = micros(); 91 | prevT = currT; 92 | DEBUG_PRINT(DEBUG_VERBOSE, "Timers updated"); 93 | } 94 | 95 | // Read current encoder positions 96 | void updateEncoders() { 97 | inputRight = encoderRight.read(); 98 | inputLeft = encoderLeft.read(); 99 | DEBUG_PRINT(DEBUG_VERBOSE, "Encoders updated: InputRight=" + String(inputRight) + 100 | ", InputLeft=" + String(inputLeft)); 101 | } 102 | 103 | // Update motor target positions from animation commands 104 | void updateSetpoints(const String& rightWheel, const String& leftWheel, bool isAnimationPlaying) { 105 | if (isAnimationPlaying) { 106 | setpointRight = rightWheel.toFloat(); 107 | setpointLeft = leftWheel.toFloat(); 108 | } 109 | DEBUG_PRINT(DEBUG_VERBOSE, "Setpoints updated: SetpointRight=" + String(setpointRight) + 110 | ", SetpointLeft=" + String(setpointLeft)); 111 | } 112 | 113 | // Apply motor speeds based on PID output 114 | // Handles motion enable/disable and motor compensation 115 | void controlMotors(bool isAnimationPlaying, bool motionEnabled) { 116 | if (setpointRight == 0) { 117 | motors.setM1Speed(0); 118 | motors.setM2Speed(0); 119 | DEBUG_PRINT(DEBUG_VERBOSE, "Motors stopped - zero setpoint"); 120 | return; 121 | } 122 | 123 | if (isAnimationPlaying) { 124 | if (!motionEnabled) { 125 | motors.setM1Speed(0); 126 | motors.setM2Speed(0); 127 | DEBUG_PRINT(DEBUG_VERBOSE, "Motors stopped - Motion disabled during animation"); 128 | } else { 129 | motors.setM1Speed(outputLeft); // Left motor (M1) 130 | // Apply power compensation to right motor 131 | motors.setM2Speed(outputRight * MOTOR2_POWER_COMPENSATION); 132 | DEBUG_PRINT(DEBUG_VERBOSE, "Motors speed set: M1=" + String(outputLeft) + 133 | ", M2=" + String(outputRight * MOTOR2_POWER_COMPENSATION)); 134 | } 135 | } 136 | } 137 | 138 | // Calculate new PID outputs 139 | void computePID() { 140 | pidRight.Compute(); 141 | pidLeft.Compute(); 142 | DEBUG_PRINT(DEBUG_VERBOSE, "PID computed: OutputRight=" + String(outputRight) + 143 | ", OutputLeft=" + String(outputLeft)); 144 | } 145 | 146 | // Rotate robot in response to Playdate crank turns 147 | // Sends "msg r/1" when rotation complete 148 | void rotateRobot(int numberOfTurns, int direction) { 149 | // Reset encoders for accurate rotation 150 | encoderRight.write(0); 151 | encoderLeft.write(0); 152 | 153 | // Calculate required encoder ticks for rotation 154 | // One 360° turn requires 1854 ticks (2.21 wheel rotations) 155 | const int TICKS_FOR_360 = 1854; 156 | long targetTicks = TICKS_FOR_360 * numberOfTurns; 157 | 158 | DEBUG_PRINT(DEBUG_INFO, "Starting rotation - Direction: " + String(direction) + 159 | " Target: " + String(targetTicks)); 160 | 161 | // Set motor speeds with direction 162 | int m2_speed = direction > 0 ? ROTATION_SPEED : -ROTATION_SPEED; 163 | int m1_speed = direction > 0 ? 164 | -ROTATION_SPEED * MOTOR2_POWER_COMPENSATION : 165 | ROTATION_SPEED * MOTOR2_POWER_COMPENSATION; 166 | 167 | motors.setM1Speed(m1_speed); 168 | motors.setM2Speed(m2_speed); 169 | 170 | // Wait for target rotation 171 | while(abs(encoderRight.read()) < targetTicks) { 172 | delay(1); 173 | } 174 | 175 | // Notify Playdate rotation is complete 176 | userial.println("msg r/1"); 177 | 178 | // Stop and reset 179 | motors.setM1Speed(0); 180 | motors.setM2Speed(0); 181 | encoderRight.write(0); 182 | encoderLeft.write(0); 183 | DEBUG_PRINT(DEBUG_INFO, "Rotation complete - Final positions E1: " + 184 | String(encoderRight.read()) + " E2: " + String(encoderLeft.read())); 185 | } 186 | }; 187 | 188 | #endif // MOTOR_CONTROLLER_H -------------------------------------------------------------------------------- /src/PlayBot/PIDConfig.h: -------------------------------------------------------------------------------- 1 | // PIDConfig.h 2 | #ifndef PID_CONFIG_H 3 | #define PID_CONFIG_H 4 | 5 | #define PID_KP 5 6 | #define PID_KI 0.1 7 | #define PID_KD 0.025 8 | 9 | #endif -------------------------------------------------------------------------------- /src/PlayBot/PlayBot.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Robot Controller - Main Program 3 | * 4 | * This program controls a robot with various sensors and capabilities including: 5 | * - Motor control with encoders and PID 6 | * - Distance sensing (ToF sensors) 7 | * - Edge detection (IR sensors) 8 | * - Battery monitoring 9 | * - LED status indication 10 | * - Light sensing 11 | * - USB communication with host device 12 | * - SD card animation playback 13 | * - Servo control 14 | * 15 | * Hardware requirements: 16 | * - Teensy board 17 | * - DRV8835 motor driver 18 | * - WS2812 LED 19 | * - ToF sensors with PCA9540BD multiplexer 20 | * - IR sensors 21 | * - MAX17048 battery gauge 22 | * - Servo motor 23 | * - SD card 24 | */ 25 | 26 | // ================= Library Includes ================= 27 | #include "Config.h" 28 | #include "HardwareConfig.h" 29 | #include "Debug.h" 30 | #include "StorageManager.h" 31 | #include "DistanceTracker.h" 32 | #include "LEDController.h" 33 | #include "BatteryManager.h" 34 | #include "AnimationManager.h" 35 | #include "SensorManager.h" 36 | #include "MotorController.h" 37 | #include "CommunicationManager.h" 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | // ================= Global Objects ================= 45 | LEDController ledController(ws2812fx); 46 | AnimationManager animationManager(headServo, motors, myEnc, myEnc2); 47 | BatteryManager batteryManager(userial, ledController, animationManager); 48 | DistanceTracker distanceTracker(myEnc, myEnc2); 49 | String rightWheel, leftWheel; 50 | SensorManager sensorManager(userial, animationManager, motors, ws2812fx, mux, 51 | myEnc, myEnc2, batteryManager, distanceTracker); 52 | MotorController motorController( 53 | motors, 54 | myEnc, // Left encoder 55 | myEnc2, // Right encoder 56 | myPID2, // Left PID 57 | myPID, // Right PID 58 | Setpoint, // Right setpoint 59 | Setpoint2, // Left setpoint 60 | Input, // Right input 61 | Input2, // Left input 62 | Output, // Right output 63 | Output2 // Left output 64 | ); 65 | StorageManager storageManager; 66 | CommunicationManager communicationManager( 67 | myusb, 68 | userial, 69 | animationManager, 70 | batteryManager, 71 | sensorManager, 72 | motorController 73 | ); 74 | 75 | // ================= Global Variables ================= 76 | bool MOTION_ENABLED = true; // Initialize as enabled 77 | unsigned long currentMillis = 0; 78 | 79 | // ================= Main Setup Function ================= 80 | void setup() { 81 | // Initialize serial communication 82 | Serial.begin(9600); 83 | delay(1000); 84 | DEBUG_PRINT(DEBUG_INFO, "Setup started"); 85 | //Initialize classes 86 | storageManager.initialize(); 87 | sensorManager.initialize(); 88 | ledController.initialize(); 89 | motorController.initialize(); 90 | batteryManager.initialize(); 91 | communicationManager.initialize(); 92 | distanceTracker.initialize(); 93 | printSetupErrorSummary(); 94 | 95 | DEBUG_PRINT(DEBUG_INFO, "Setup completed"); 96 | 97 | // Set LED status based on setup success 98 | if (setupErrors.empty()) { 99 | ledController.setStatusSuccess(); // Green for success 100 | } else { 101 | ledController.setStatusError(); // Red for error 102 | } 103 | 104 | sendLogs(); 105 | } 106 | 107 | // ================= Main Loop Function ================= 108 | void loop() { 109 | ledController.update(); 110 | 111 | // Update system states 112 | motorController.updateTimers(); 113 | motorController.updateEncoders(); 114 | motorController.computePID(); 115 | 116 | animationManager.getWheelCommands(rightWheel, leftWheel); 117 | motorController.updateSetpoints(rightWheel, leftWheel, animationManager.isAnimationPlaying()); 118 | motorController.controlMotors(animationManager.isAnimationPlaying(), MOTION_ENABLED); 119 | 120 | // Handle animation if active 121 | animationManager.update(); 122 | 123 | // Process USB communication and sensor checks 124 | communicationManager.readFromUSBHostSerialAndWriteToSerial(); 125 | communicationManager.handleBaudRateChange(); 126 | 127 | sensorManager.detectTableEdgeIR(); 128 | sensorManager.checkLightSensor(); 129 | sensorManager.checkFrontCollision(); 130 | 131 | distanceTracker.update(); 132 | distanceTracker.checkAndLog(animationManager.isAnimationPlaying()); 133 | 134 | batteryManager.detectBatteryCharging(); 135 | sendLogs(); 136 | DEBUG_PRINT(DEBUG_VERBOSE, "Loop iteration completed"); 137 | } 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/PlayBot/README.md: -------------------------------------------------------------------------------- 1 | # Robot Controller - Arduino Project 2 | 3 | Firmware for a robot using Teensy microcontroller that communicates with a Playdate device. 4 | 5 | ## Core Components 6 | 7 | ### Main Program (PlayBot.ino) 8 | - Main program loop and initialization 9 | - Orchestrates all subsystems 10 | - Manages communication between components 11 | 12 | ### Managers 13 | 14 | #### AnimationManager (AnimationManager.h) 15 | - Handles animation playback from SD card 16 | - Controls servo movements 17 | - Manages motor coordination for animations 18 | - Real-time frame processing 19 | 20 | #### BatteryManager (BatteryManager.h) 21 | - Battery voltage monitoring via MAX17048 gauge 22 | - Charging state detection 23 | - Power state reporting to Playdate 24 | 25 | #### CommunicationManager (CommunicationManager.h) 26 | - USB serial communication with Playdate 27 | - Message parsing and routing 28 | - Protocol handling for all subsystems 29 | 30 | #### LEDController (LEDController.h) 31 | - WS2812 LED control 32 | - Status indication 33 | 34 | #### MotorController (MotorController.h) 35 | - DRV8835 motor driver control 36 | - PID-based motor control 37 | - Encoder feedback processing 38 | - Rotation and movement commands 39 | 40 | #### SensorManager (SensorManager.h) 41 | - ToF distance sensors management 42 | - IR edge detection 43 | - Light sensor readings 44 | - Collision detection 45 | - Sensor data aggregation and reporting 46 | 47 | #### StorageManager (StorageManager.h) 48 | - SD card initialization 49 | - File system management 50 | 51 | ### Support Files 52 | 53 | #### Config.h 54 | - System-wide configuration parameters 55 | - Pin definitions 56 | - Timing constants 57 | - Sensor thresholds 58 | 59 | #### Debug.h/cpp 60 | - Debugging infrastructure 61 | - Error logging 62 | - Setup error tracking 63 | - Log buffering and transmission 64 | 65 | #### DistanceTracker.h 66 | - Tracks total distance traveled 67 | - Persistent distance logging 68 | - Movement statistics 69 | 70 | #### HardwareConfig.h/cpp 71 | - Hardware-specific configurations 72 | - Pin assignments 73 | - Physical constants 74 | - Hardware object initialization 75 | 76 | ## Communication Protocol 77 | 78 | Message Protocol Details: 79 | 80 | Outgoing (Arduino -> Playdate): 81 | - "msg b/percent/voltage/charging" 82 | Example: "msg b/85.20/3.7/1" (85.20% battery, 3.7V, charging) 83 | 84 | - "msg d/irRight/irLeft/tofFront/tofBack/encRight/encLeft/light/batteryVoltage/charging/distanceM" 85 | Example: "msg d/100/120/150.20/160.50/-1200/1200/500/3.7/1/1.50" 86 | (IR values, ToF in mm, encoder ticks, light level, battery V, charging state, distance in meters) 87 | 88 | - "msg e/1" (Edge detected) 89 | 90 | - "msg l/0" or "msg l/1" (Darkness state change: 0=dark, 1=light) 91 | 92 | - "msg p/0" or "msg p/1" (Power/charging state: 0=unplugged, 1=charging) 93 | 94 | - "msg s/" (Connection confirmation) 95 | 96 | - "msg w/1" (Collision detected) 97 | 98 | - "msg r/1" (Rotation completed) 99 | 100 | Incoming (Playdate -> Arduino): 101 | - "a/filepath" (Start animation from SD card) 102 | 103 | - "b" (Request battery status) 104 | 105 | - "d" (Request sensor data) 106 | 107 | - "v" (Connection verification ping) 108 | 109 | - "t/turns/direction" 110 | Example: "t/2/1" (2 turns, direction 1=clockwise, -1=counterclockwise) 111 | 112 | -------------------------------------------------------------------------------- /src/PlayBot/SensorManager.h: -------------------------------------------------------------------------------- 1 | // SensorManager.h 2 | #ifndef SENSOR_MANAGER_H 3 | #define SENSOR_MANAGER_H 4 | 5 | #include "Config.h" 6 | #include "Debug.h" 7 | #include "BatteryManager.h" 8 | #include "DistanceTracker.h" 9 | #include 10 | #include 11 | 12 | // Manages all robot sensors and communicates with Playdate via messages: 13 | // - "msg d/..." : Complete sensor data packet 14 | // - "msg e/1" : Edge detected 15 | // - "msg w/1" : Collision detected 16 | // - "msg l/0|1" : Dark/Light state change 17 | class SensorManager { 18 | private: 19 | // ToF sensor variables with exponential smoothing 20 | // to handle unreliable sensors 21 | Smoothed TOFsensor1; // Front sensor smoothing 22 | Smoothed TOFsensor2; // Back sensor smoothing 23 | float TOFsensorFront = 0; // Cached front distance 24 | float TOFsensorBack = 0; // Cached back distance 25 | unsigned char i2c_rx_buf[16]; // I2C reading buffer 26 | unsigned short lenth_val = 0; // Current distance value 27 | 28 | // Light sensor state tracking 29 | unsigned long lastLightCheckTime = 0; 30 | bool isInDarkness = false; 31 | bool previousDarknessState = false; 32 | 33 | // Collision detection with enhanced validation 34 | unsigned long lastCollisionCheckTime = 0; 35 | bool collisionMessageSent = false; 36 | 37 | // IR edge detection with adjustable thresholds 38 | int IR_THRESHOLD_LEFT = 15; // Left sensor threshold 39 | int IR_THRESHOLD_RIGHT = 15; // Right sensor threshold 40 | unsigned long lastIRCheckTime = 0; 41 | bool isEdgeDetectedIR = false; 42 | Smoothed smoothedIRLeft; // IR value smoothing 43 | Smoothed smoothedIRRight; // IR value smoothing 44 | 45 | // Hardware interface references 46 | USBSerial_BigBuffer& userial; // Serial communication 47 | AnimationManager& animationManager; // Animation control 48 | DRV8835MotorShield& motors; // Motor control 49 | WS2812FX& ws2812fx; // LED control 50 | PCA9540BD& mux; // ToF multiplexer 51 | Encoder& encoderLeft; // Left wheel encoder 52 | Encoder& encoderRight; // Right wheel encoder 53 | BatteryManager& batteryManager; // Battery monitoring 54 | DistanceTracker& distanceTracker; // Distance tracking 55 | 56 | // Read raw data from ToF sensor via I2C 57 | void SensorRead(unsigned char addr, unsigned char* datbuf, unsigned char cnt) { 58 | Wire.beginTransmission(82); 59 | Wire.write(byte(addr)); 60 | Wire.endTransmission(); 61 | Wire.requestFrom((uint8_t)82, (uint8_t)cnt); 62 | if (cnt <= Wire.available()) { 63 | *datbuf++ = Wire.read(); 64 | *datbuf++ = Wire.read(); 65 | } 66 | } 67 | 68 | // Convert raw ToF data to distance value 69 | int ReadDistance() { 70 | SensorRead(0x00, i2c_rx_buf, 2); 71 | lenth_val = i2c_rx_buf[0]; 72 | lenth_val = lenth_val << 8; 73 | lenth_val |= i2c_rx_buf[1]; 74 | return lenth_val; 75 | } 76 | 77 | public: 78 | // Initialize manager with hardware references 79 | SensorManager(USBSerial_BigBuffer& serial, AnimationManager& anim, 80 | DRV8835MotorShield& mot, WS2812FX& led, PCA9540BD& multiplexer, 81 | Encoder& encLeft, Encoder& encRight, 82 | BatteryManager& battery, DistanceTracker& distance) 83 | : userial(serial) 84 | , animationManager(anim) 85 | , motors(mot) 86 | , ws2812fx(led) 87 | , mux(multiplexer) 88 | , encoderLeft(encLeft) 89 | , encoderRight(encRight) 90 | , batteryManager(battery) 91 | , distanceTracker(distance) { 92 | // Configure smoothing filters 93 | smoothedIRLeft.begin(SMOOTHED_EXPONENTIAL, 1000); 94 | smoothedIRRight.begin(SMOOTHED_EXPONENTIAL, 1000); 95 | TOFsensor1.begin(SMOOTHED_EXPONENTIAL, 2); // Increased smoothing for unreliable ToF 96 | TOFsensor2.begin(SMOOTHED_EXPONENTIAL, 2); 97 | } 98 | 99 | // Initialize sensor hardware 100 | void initialize() { 101 | pinMode(IR_SENSOR_RIGHT_PIN, INPUT_DISABLE); 102 | pinMode(IR_SENSOR_LEFT_PIN, INPUT_DISABLE); 103 | DEBUG_PRINT(DEBUG_INFO, "IR sensors initialized"); 104 | 105 | Wire.begin(); 106 | DEBUG_PRINT(DEBUG_INFO, "ToF sensors initialized"); 107 | } 108 | 109 | // Read distance from specific ToF sensor 110 | // Includes caching for reliability 111 | float readTofSensor(int channel) { 112 | mux.selectChannel(channel); 113 | float rawDistance = ReadDistance(); 114 | 115 | if (rawDistance > 0) { 116 | if (channel == FRONT_SENSOR) { 117 | TOFsensorFront = rawDistance; 118 | return TOFsensorFront; 119 | } else { 120 | TOFsensorBack = rawDistance; 121 | return TOFsensorBack; 122 | } 123 | } 124 | 125 | return (channel == FRONT_SENSOR) ? TOFsensorFront : TOFsensorBack; 126 | } 127 | 128 | // Read averaged IR sensor values 129 | int readIRSensor(int sensorPin) { 130 | long sum = 0; 131 | const int numReadings = 10; 132 | 133 | for (int i = 0; i < numReadings; i++) { 134 | sum += analogRead(sensorPin); 135 | } 136 | return sum / numReadings; 137 | } 138 | 139 | // Monitor ambient light changes 140 | void checkLightSensor() { 141 | unsigned long currentTime = millis(); 142 | if (currentTime - lastLightCheckTime >= LIGHT_CHECK_INTERVAL) { 143 | int lightValue = analogRead(LIGHT_SENSOR_PIN); 144 | bool currentDarknessState = (lightValue < DARKNESS_THRESHOLD); 145 | 146 | if (currentDarknessState != previousDarknessState) { 147 | isInDarkness = currentDarknessState; 148 | userial.println(isInDarkness ? "msg l/0" : "msg l/1"); 149 | previousDarknessState = isInDarkness; 150 | DEBUG_PRINT(DEBUG_VERBOSE, "Light state changed. Is in darkness: " + String(isInDarkness)); 151 | } 152 | 153 | lastLightCheckTime = currentTime; 154 | ws2812fx.setBrightness(isInDarkness ? 50 : 10); 155 | } 156 | } 157 | 158 | // Detect table edges using IR sensors 159 | void detectTableEdgeIR() { 160 | unsigned long currentTime = millis(); 161 | if (currentTime - lastIRCheckTime >= IR_CHECK_INTERVAL) { 162 | float sensorLeftValue = readIRSensor(IR_SENSOR_LEFT_PIN); 163 | float sensorRightValue = readIRSensor(IR_SENSOR_RIGHT_PIN); 164 | 165 | bool currentEdgeDetected = (sensorLeftValue >= IR_THRESHOLD_LEFT || 166 | sensorRightValue >= IR_THRESHOLD_RIGHT); 167 | 168 | if (currentEdgeDetected != isEdgeDetectedIR) { 169 | isEdgeDetectedIR = currentEdgeDetected; 170 | 171 | if (isEdgeDetectedIR) { 172 | animationManager.stopAnimation(); 173 | motors.setM1Speed(0); 174 | motors.setM2Speed(0); 175 | userial.println("msg e/1"); 176 | DEBUG_PRINT(DEBUG_INFO, "Edge detected by IR sensors. Left: " + 177 | String(sensorLeftValue) + ", Right: " + String(sensorRightValue)); 178 | } 179 | } 180 | 181 | lastIRCheckTime = currentTime; 182 | } 183 | } 184 | 185 | // Check for front collisions with enhanced ToF validation 186 | // Uses multiple readings and threshold to handle unreliable sensors --> slow :(, V2 will use an array of IR sensors 187 | void checkFrontCollision() { 188 | unsigned long currentTime = millis(); 189 | if (currentTime - lastCollisionCheckTime >= COLLISION_CHECK_INTERVAL) { 190 | float frontDistance = readTofSensor(FRONT_SENSOR); 191 | 192 | // Strict range validation to filter void detections 193 | if (frontDistance > 0 && frontDistance < 1800) { 194 | TOFsensor2.add(frontDistance); 195 | float smoothedFrontDistance = TOFsensor2.get(); 196 | 197 | static uint8_t collisionCount = 0; 198 | static uint8_t validationThreshold = 30; // Increased for reliability 199 | 200 | if (smoothedFrontDistance < FRONT_COLLISION_THRESHOLD) { 201 | collisionCount++; 202 | if (collisionCount >= validationThreshold && !collisionMessageSent) { 203 | userial.println("msg w/1"); 204 | collisionMessageSent = true; 205 | DEBUG_PRINT(DEBUG_INFO, "Front collision validated. Distance: " + 206 | String(smoothedFrontDistance) + "mm, Raw: " + 207 | String(frontDistance) + "mm"); 208 | } 209 | } else { 210 | collisionCount = 0; 211 | collisionMessageSent = false; 212 | } 213 | } 214 | 215 | lastCollisionCheckTime = currentTime; 216 | } 217 | } 218 | 219 | // Send complete sensor data package to Playdate 220 | void sendSensorData() { 221 | int irRight = readIRSensor(IR_SENSOR_RIGHT_PIN); 222 | int irLeft = readIRSensor(IR_SENSOR_LEFT_PIN); 223 | float tofFront = readTofSensor(FRONT_SENSOR); 224 | Wire.flush(); // Clear I2C bus 225 | float tofBack = readTofSensor(BACK_SENSOR); 226 | long encRight = encoderRight.read(); 227 | long encLeft = encoderLeft.read(); 228 | int lightValue = analogRead(LIGHT_SENSOR_PIN); 229 | float batteryVoltage = batteryManager.getVoltage(); 230 | float distanceInMeters = distanceTracker.getDistanceMeters(); 231 | 232 | char buffer[120]; 233 | snprintf(buffer, sizeof(buffer), 234 | "msg d/%d/%d/%.2f/%.2f/%ld/%ld/%d/%.2f/%d/%.2f", 235 | irRight, irLeft, tofFront, tofBack, encRight, encLeft, lightValue, 236 | batteryVoltage, batteryManager.isCharging() ? 1 : 0, distanceInMeters); 237 | userial.println(buffer); 238 | DEBUG_PRINT(DEBUG_INFO, "Sent sensor data with distance: " + String(buffer)); 239 | } 240 | 241 | // Get current sensor states 242 | float getTOFSensorFront() const { return TOFsensorFront; } 243 | float getTOFSensorBack() const { return TOFsensorBack; } 244 | bool getIsInDarkness() const { return isInDarkness; } 245 | }; 246 | 247 | #endif // SENSOR_MANAGER_H -------------------------------------------------------------------------------- /src/PlayBot/StorageManager.h: -------------------------------------------------------------------------------- 1 | // StorageManager.h 2 | #ifndef STORAGE_MANAGER_H 3 | #define STORAGE_MANAGER_H 4 | 5 | #include "Config.h" 6 | #include "Debug.h" 7 | #include 8 | 9 | // Manages SD card storage for robot animations and data 10 | // The SD card stores: 11 | // - Animation files (loaded when Playdate sends "a/filepath") 12 | // - Distance logs (updated periodically by DistanceTracker) 13 | // Initialization failure will trigger error message to Playdate 14 | class StorageManager { 15 | private: 16 | bool isInitialized; // Tracks if SD card is ready for use 17 | 18 | public: 19 | // Constructor - Sets initial state 20 | StorageManager() : isInitialized(false) {} 21 | 22 | // Initialize SD card system 23 | // Called during setup phase 24 | // Returns true if card is accessible and ready for use 25 | bool initialize() { 26 | DEBUG_PRINT(DEBUG_INFO, "Initializing SD card"); 27 | 28 | // Try to initialize SD card on the hardware pin 29 | if (!SD.begin(SD_CS_PIN)) { 30 | // Card init failed - record error 31 | // This will prevent "msg s/1" success message to Playdate 32 | recordSetupError("SD card initialization failed"); 33 | DEBUG_PRINT(DEBUG_WARNING, "SD card initialization failed!"); 34 | isInitialized = false; 35 | return false; 36 | } else { 37 | // Card successfully initialized 38 | DEBUG_PRINT(DEBUG_INFO, "SD card initialized"); 39 | isInitialized = true; 40 | return true; 41 | } 42 | } 43 | 44 | // Check if SD card is ready for use 45 | // Used by AnimationManager before file operations 46 | bool isReady() const { return isInitialized; } 47 | 48 | 49 | }; 50 | 51 | #endif // STORAGE_MANAGER_H -------------------------------------------------------------------------------- /src/Playbot_SensorsTest/Playbot_SensorsTest.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Adafruit_MAX1704X.h" 4 | #include 5 | #include 6 | #include 7 | 8 | // Pin definitions from HardwareConfig.h 9 | #define IR_SENSOR_LEFT_PIN 39 10 | #define IR_SENSOR_RIGHT_PIN 38 11 | #define LIGHT_SENSOR_PIN 27 12 | #define TOF_MULTIPLEXER_ADDR 0x70 13 | #define ENC1_PIN_A 4 14 | #define ENC1_PIN_B 5 15 | #define ENC2_PIN_A 3 16 | #define ENC2_PIN_B 2 17 | #define MOTOR1_PWM_PIN 7 // Left motor PWM 18 | #define MOTOR1_DIR_PIN 6 // Left motor direction 19 | #define MOTOR2_PWM_PIN 9 // Right motor PWM 20 | #define MOTOR2_DIR_PIN 8 // Right motor direction 21 | #define SERVO_PIN 23 22 | #define SERVO_CENTER 1500 23 | #define SERVO_SWEEP_AMOUNT 200 24 | #define SERVO_DELAY 15 25 | 26 | // Motor test parameters 27 | #define MOTOR_SPEED 200 28 | #define TEST_TICKS 840 29 | 30 | // Welcome message 31 | const char* WELCOME_MESSAGE = R"( 32 | ================================================= 33 | PLAYBOT TEST & DIAGNOSTIC PROGRAM 34 | ================================================= 35 | This program tests all sensors and motors: 36 | 37 | SENSORS: 38 | - IR sensors (left & right) 39 | - Light sensor 40 | - ToF sensors (front & back) 41 | - Encoders (left & right) 42 | - Battery voltage & percentage 43 | 44 | MOTORS TEST (when sending 't'): 45 | - Forward motion: 840 ticks 46 | - Backward motion: 840 ticks 47 | 48 | SERVO TEST: 49 | 'v' - Sweep servo head left/right 50 | 'c' - Center servo head 51 | 52 | COMMANDS: 53 | 's' - Start continuous sensor monitoring 54 | 't' - Test motors movement 55 | 'v' - Test servo sweep 56 | 'c' - Center servo 57 | 'h' - Show this help message 58 | 59 | Please send 's' to start the program... 60 | ================================================= 61 | )"; 62 | 63 | // Initialize objects 64 | Encoder encLeft(ENC1_PIN_A, ENC1_PIN_B); 65 | Encoder encRight(ENC2_PIN_A, ENC2_PIN_B); 66 | Adafruit_MAX17048 maxlipo; 67 | PCA9540BD mux; 68 | DRV8835MotorShield motors(MOTOR1_PWM_PIN, MOTOR1_DIR_PIN, MOTOR2_PWM_PIN, MOTOR2_DIR_PIN); 69 | Servo headServo; 70 | 71 | // ToF variables 72 | unsigned char i2c_rx_buf[16]; 73 | unsigned short lenth_val = 0; 74 | 75 | void showHelp() { 76 | Serial.println(WELCOME_MESSAGE); 77 | } 78 | 79 | // Read ToF sensor helper function 80 | int ReadDistance() { 81 | Wire.beginTransmission(82); 82 | Wire.write(byte(0x00)); 83 | Wire.endTransmission(); 84 | Wire.requestFrom(82, 2); 85 | if (2 <= Wire.available()) { 86 | i2c_rx_buf[0] = Wire.read(); 87 | i2c_rx_buf[1] = Wire.read(); 88 | lenth_val = (i2c_rx_buf[0] << 8) | i2c_rx_buf[1]; 89 | return lenth_val; 90 | } 91 | return 0; 92 | } 93 | 94 | void centerServo() { 95 | Serial.println("Centering servo..."); 96 | headServo.writeMicroseconds(SERVO_CENTER); 97 | delay(500); 98 | Serial.println("Servo centered"); 99 | } 100 | 101 | void sweepServo() { 102 | Serial.println("Testing servo sweep..."); 103 | 104 | // Sweep right 105 | for(int pos = SERVO_CENTER; pos <= SERVO_CENTER + SERVO_SWEEP_AMOUNT; pos += 5) { 106 | headServo.writeMicroseconds(pos); 107 | Serial.print("Servo position: "); 108 | Serial.println(pos); 109 | delay(SERVO_DELAY); 110 | } 111 | 112 | // Small pause at end position 113 | delay(500); 114 | 115 | // Sweep left 116 | for(int pos = SERVO_CENTER + SERVO_SWEEP_AMOUNT; pos >= SERVO_CENTER - SERVO_SWEEP_AMOUNT; pos -= 5) { 117 | headServo.writeMicroseconds(pos); 118 | Serial.print("Servo position: "); 119 | Serial.println(pos); 120 | delay(SERVO_DELAY); 121 | } 122 | 123 | // Small pause at end position 124 | delay(500); 125 | 126 | // Return to center 127 | for(int pos = SERVO_CENTER - SERVO_SWEEP_AMOUNT; pos <= SERVO_CENTER; pos += 5) { 128 | headServo.writeMicroseconds(pos); 129 | Serial.print("Servo position: "); 130 | Serial.println(pos); 131 | delay(SERVO_DELAY); 132 | } 133 | 134 | Serial.println("Sweep complete"); 135 | } 136 | 137 | // Motor test function 138 | void testMotors() { 139 | // Reset encoders 140 | encLeft.write(0); 141 | encRight.write(0); 142 | 143 | // Forward motion 144 | Serial.println("Moving forward..."); 145 | while(abs(encLeft.read()) < TEST_TICKS) { 146 | motors.setM1Speed(MOTOR_SPEED); // Left motor 147 | motors.setM2Speed(MOTOR_SPEED); // Right motor 148 | Serial.print("Encoder Left: "); 149 | Serial.println(encLeft.read()); 150 | } 151 | 152 | // Stop 153 | motors.setM1Speed(0); 154 | motors.setM2Speed(0); 155 | delay(1000); 156 | 157 | // Reset encoders 158 | encLeft.write(0); 159 | encRight.write(0); 160 | 161 | // Backward motion 162 | Serial.println("Moving backward..."); 163 | while(abs(encLeft.read()) < TEST_TICKS) { 164 | motors.setM1Speed(-MOTOR_SPEED); // Left motor 165 | motors.setM2Speed(-MOTOR_SPEED); // Right motor 166 | Serial.print("Encoder Left: "); 167 | Serial.println(encLeft.read()); 168 | } 169 | 170 | // Stop 171 | motors.setM1Speed(0); 172 | motors.setM2Speed(0); 173 | Serial.println("Test complete"); 174 | } 175 | 176 | void setup() { 177 | Serial.begin(115200); 178 | delay(1000); 179 | 180 | // Show welcome message 181 | showHelp(); 182 | 183 | // Initialize I2C 184 | Wire.begin(); 185 | 186 | // Initialize battery gauge 187 | if (!maxlipo.begin()) { 188 | Serial.println("Battery gauge not found!"); 189 | } 190 | 191 | // Initialize IR sensors 192 | pinMode(IR_SENSOR_LEFT_PIN, INPUT_DISABLE); 193 | pinMode(IR_SENSOR_RIGHT_PIN, INPUT_DISABLE); 194 | 195 | // Configure motors 196 | analogWriteFrequency(MOTOR1_PWM_PIN, 330000); 197 | analogWriteFrequency(MOTOR1_DIR_PIN, 330000); 198 | analogWriteFrequency(MOTOR2_PWM_PIN, 330000); 199 | analogWriteFrequency(MOTOR2_DIR_PIN, 330000); 200 | motors.flipM1(true); 201 | 202 | // Initialize servo 203 | headServo.attach(SERVO_PIN); 204 | centerServo(); // Center servo at startup 205 | 206 | // Wait for user input to start 207 | bool started = false; 208 | while (!started) { 209 | if (Serial.available() > 0) { 210 | char cmd = Serial.read(); 211 | if (cmd == 's') { 212 | started = true; 213 | Serial.println("\nStarting sensor monitoring...\n"); 214 | } else if (cmd == 'h') { 215 | showHelp(); 216 | } 217 | } 218 | delay(100); 219 | } 220 | } 221 | 222 | void loop() { 223 | // Check for serial commands 224 | if (Serial.available() > 0) { 225 | char cmd = Serial.read(); 226 | switch(cmd) { 227 | case 't': 228 | testMotors(); 229 | break; 230 | case 'h': 231 | showHelp(); 232 | break; 233 | case 'v': 234 | sweepServo(); 235 | break; 236 | case 'c': 237 | centerServo(); 238 | break; 239 | } 240 | } 241 | 242 | // Read and print IR sensors 243 | Serial.println("\n=== IR Sensors ==="); 244 | Serial.print("Left IR: "); 245 | Serial.println(analogRead(IR_SENSOR_LEFT_PIN)); 246 | Serial.print("Right IR: "); 247 | Serial.println(analogRead(IR_SENSOR_RIGHT_PIN)); 248 | 249 | // Read and print light sensor 250 | Serial.println("\n=== Light Sensor ==="); 251 | Serial.print("Light level: "); 252 | Serial.println(analogRead(LIGHT_SENSOR_PIN)); 253 | 254 | // Read and print ToF sensors 255 | Serial.println("\n=== ToF Sensors ==="); 256 | mux.selectChannel(1); // Front sensor 257 | Serial.print("Front ToF: "); 258 | Serial.println(ReadDistance()); 259 | mux.selectChannel(0); // Back sensor 260 | Serial.print("Back ToF: "); 261 | Serial.println(ReadDistance()); 262 | 263 | // Read and print encoders 264 | Serial.println("\n=== Encoders ==="); 265 | Serial.print("Left encoder: "); 266 | Serial.println(encLeft.read()); 267 | Serial.print("Right encoder: "); 268 | Serial.println(encRight.read()); 269 | 270 | // Read and print battery info 271 | Serial.println("\n=== Battery ==="); 272 | Serial.print("Voltage: "); 273 | Serial.print(maxlipo.cellVoltage(), 3); 274 | Serial.println("V"); 275 | Serial.print("Percent: "); 276 | Serial.print(maxlipo.cellPercent(), 1); 277 | Serial.println("%"); 278 | 279 | delay(1000); // Update every second 280 | } -------------------------------------------------------------------------------- /src/Playbot_SensorsTest/README.md: -------------------------------------------------------------------------------- 1 | # PlayBot Sensor Test Program 2 | 3 | A diagnostic tool for testing and calibrating PlayBot's hardware components. 4 | 5 | ## Hardware Requirements 6 | 7 | - Teensy microcontroller 8 | - DRV8835 motor driver 9 | - 2x Quadrature encoders 10 | - 2x IR sensors 11 | - 2x ToF sensors with PCA9540BD multiplexer 12 | - Light sensor 13 | - MAX17048 battery gauge 14 | - Servo motor for head movement 15 | 16 | ## Features 17 | 18 | - Real-time sensor monitoring 19 | - Motor movement testing 20 | - Servo sweep testing 21 | - Battery monitoring 22 | 23 | ## Commands 24 | 25 | | Command | Description | 26 | |---------|-------------| 27 | | `s` | Start continuous sensor monitoring | 28 | | `t` | Test motors (forward/backward) | 29 | | `v` | Test servo sweep movement | 30 | | `c` | Center servo position | 31 | | `h` | Display help message | 32 | 33 | ## Installation 34 | 35 | 1. Install required libraries: 36 | - Encoder 37 | - Adafruit_MAX1704X 38 | - PCA9540BD 39 | - DRV8835MotorShield 40 | - Servo 41 | 42 | 2. Upload to Teensy using Arduino IDE 43 | 44 | ## Usage 45 | 46 | 1. Open Serial Monitor (115200 baud) 47 | 2. Send 's' to start monitoring 48 | 3. Use commands to test specific components 49 | 50 | --------------------------------------------------------------------------------