├── .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 | [](https://youtu.be/pZ3aDawW1vo?si=pN9pur77LKVZNdHx)
 9 | 
10 | ⚡ Battery life: ~45 min of autonomous operation per charge.
11 | 
12 |  
13 |  
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 | 
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 | 
--------------------------------------------------------------------------------