[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/097/original/running.gif?1766119706","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003ePyBadge as a CircuitPython Learning Platform\u003c/h1\u003e\n\u003cp\u003eThe PyBadge is one of the most feature‑packed ways to experiment and learn CircuitPython. With an integrated TFT display, eight buttons, five NeoPixels, a light and an acceleration sensor, audio with an on-board speaker and amplifier for an external speaker, LiPo charging, and multiple expansion ports including a FeatherWing socket, it’s practically a handheld programming lab.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eThen last week JP had Adalogger FeatherWing with its built‑in RTC on as his product pick of the week the idea clicked: let’s turn the PyBadge into a portable, multi‑function timing tool!\u003c/p\u003e\n\u003ch1\u003eComponents\u003c/h1\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4200","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/2922","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4227","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/258","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/380","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eAssemble the parts\u003c/h1\u003e\n\u003cp\u003ePutting it all together is quite simple. The only soldering needed is attaching header pins to the AdaLogger.\u0026nbsp;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eSolder header pins to the FeatherWing. Install the headers on the bottom of the board facing downward.\u003c/li\u003e\n\u003cli\u003eInsert the coin cell into the FeatherWing.\u003c/li\u003e\n\u003cli\u003eAttach the FeatherWing to the socket on the back of the PyBadge.\u003c/li\u003e\n\u003cli\u003ePlug in the speaker\u003c/li\u003e\n\u003cli\u003ePlug in the battery\u003c/li\u003e\n\u003cli\u003eConnect to USB and you are ready to roll!\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1\u003eInstall CircuitPython\u003c/h1\u003e\n\u003cp\u003eNow install CircuitPython on your PyBadge - follow the instructions in the Learn Guide:\u0026nbsp;\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"guide","content":"https://learn.adafruit.com/adafruit-pybadge/installing-circuitpython","metadata":{"guide_id":"2528","page_id":"15987"}},{"element_type":"alert","content":"\u003cp\u003eTake a few minutes to read through the Learn Guide to better learn about the board. For example, if you have an older PyBadge you may need to update the boot loader - this is all covered in the guide. \u003c/p\u003e","metadata":{"markdown":"Take a few minutes to read through the Learn Guide to better learn about the board. For example, if you have an older PyBadge you may need to update the boot loader - this is all covered in the guide. ","alert_type":"danger","icon":"info"}},{"element_type":"text","content":"\n  \n  \n  \n        \u003ch1\u003eInstall Libraries \u0026amp; Peripheral Files\u003c/h1\u003e\n\u003cp\u003eYou will also need to install a few libraries on your PyBadge - again this process is covered in the Learn Guide. The specific libraries needed for this project are:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eadfruit_pybadger (main helper library)\u003c/li\u003e\n\u003cli\u003eadafruit_pcf8523 (for using the RTC on the FeatherWing)\u003c/li\u003e\n\u003cli\u003eadafruit_bitmap_font\u003c/li\u003e\n\u003cli\u003eadafruit_display_text\u003c/li\u003e\n\u003cli\u003eadafruit_display_shapes\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIn addition to these libraries, you will also need several font files and place them in a folder \"/fonts\". Here is a good Learn Guide on font files for CircuitPython: \u003ca href=\"https://learn.adafruit.com/custom-fonts-for-pyportal-circuitpython-display\"\u003eOverview | Custom Fonts for CircuitPython Displays | Adafruit Learning System\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eYou will also need some wav files for the various sound effects if you want to add sound - these go in a folder \"/wav\".\u003c/p\u003e\n\u003ch1\u003eOn to coding\u003c/h1\u003e\n\u003cp\u003eThe original concept was just a simple stopwatch, but that quickly morphed in the final project which includes:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eStopwatch Mode - timing to tenths of a second with lap capability\u003c/li\u003e\n\u003cli\u003eTimer Mode - counting in seconds including noting the original start time\u003c/li\u003e\n\u003cli\u003eCountdown Timer Mode - set the number of minutes/seconds and count down to zero\u003c/li\u003e\n\u003cli\u003eClock Mode - Display the time and date\u003c/li\u003e\n\u003cli\u003eBattery voltage monitoring in the lower right corner\u003c/li\u003e\n\u003cli\u003eEach screen shows the current time in the upper right corner\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHere is what each mode looks like:\u003c/p\u003e\n      \n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/100/original/Stopwatch600.jpg?1766119509","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/101/original/Timer600.jpg?1766119547","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/102/original/Countdown600.jpg?1766119586","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/103/original/Clock_600.jpg?1766119615","metadata":{}},{"element_type":"text","content":"\n  \n  \n        \u003ch1\u003eBasic Program Structure \u0026amp; Concepts\u003c/h1\u003e\n\u003cp\u003eThis project is useful for learning to use different elements to put together a complete project. Here are code snippets for the overall structure of the project. The complete code is posted at the end of the article.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSetting up the display:\u003c/strong\u003e The PyBadge has the on-board display code built into the CP build, so it is quite simple: \u003ccode\u003edisplay = board.DISPLAY\u003c/code\u003e The PyBadge's display is a \u003cspan\u003e160x128 color TFT. \u003c/span\u003eWe will use\u0026nbsp;\u003cstrong\u003edisplayio, display_text, display_shapes\u003c/strong\u003e\u0026nbsp;and \u003cstrong\u003ebitmap_font\u0026nbsp;\u003c/strong\u003e to layout the display.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSet up the RTC:\u003c/strong\u003e First you need to define the I2C bus and then set up the RTC:\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003e\u003cspan\u003ei2c\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eboard\u003c/span\u003e\u003cspan\u003e.I2C()\u003cbr\u003e\u003c/span\u003e\u003c/code\u003e\u003ccode\u003e\u003cspan\u003ertc_pcf\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e PCF8523(\u003c/span\u003e\u003cspan\u003ei2c\u003c/span\u003e\u003cspan\u003e)\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eYou need \"one time\" code to set the current on the RTC. Once set it should maintain the time as long as there is a coin cell battery installed. Time setting code:\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e\u003cspan class=\"hljs-keyword\"\u003e\u003cstrong\u003eif\u003c/strong\u003e\u003c/span\u003e\u003cstrong\u003e \u003cspan class=\"hljs-literal\"\u003eFalse\u003c/span\u003e\u003c/strong\u003e\u003cspan\u003e\u003cstrong\u003e:\u003c/strong\u003e \u003c/span\u003e\u003cem\u003e\u003cspan class=\"hljs-comment\"\u003e# change to True if you want to set the time!\u003c/span\u003e\u003c/em\u003e\u003cspan\u003e \u003cbr\u003e\u003c/span\u003e\u003cspan class=\"hljs-comment\"\u003e\u0026nbsp; \u0026nbsp; \u003cem\u003e# year, mon, date, hour, min, sec, wday, yday, isdst\u003c/em\u003e\u003c/span\u003e\u003cspan\u003e \u003cbr\u003e\u0026nbsp; \u0026nbsp; \u003cstrong\u003et = time.struct_time((\u003c/strong\u003e\u003c/span\u003e\u003cstrong\u003e\u003cspan class=\"hljs-number\"\u003e2025\u003c/span\u003e, \u003cspan class=\"hljs-number\"\u003e12\u003c/span\u003e, \u003cspan class=\"hljs-number\"\u003e19\u003c/span\u003e, \u003cspan class=\"hljs-number\"\u003e10\u003c/span\u003e, \u003cspan class=\"hljs-number\"\u003e45\u003c/span\u003e, \u003cspan class=\"hljs-number\"\u003e0\u003c/span\u003e, \u003cspan class=\"hljs-number\"\u003e4\u003c/span\u003e, -\u003cspan class=\"hljs-number\"\u003e1\u003c/span\u003e, -\u003cspan class=\"hljs-number\"\u003e1\u003c/span\u003e\u003c/strong\u003e\u003cspan\u003e\u003cstrong\u003e))\u003c/strong\u003e \u003cbr\u003e\u003c/span\u003e\u003cspan class=\"hljs-comment\"\u003e\u0026nbsp; \u0026nbsp; \u003cem\u003e# you must set year, mon, date, hour, min, sec and weekday\u003cbr\u003e\u003c/em\u003e\u003c/span\u003e\u003cspan\u003e\u0026nbsp; \u0026nbsp;\u0026nbsp;\u003cem\u003e# day of the week starts with 0 = Monday through 6 = Sunday\u0026nbsp;\u003c/em\u003e\u003cbr\u003e\u003c/span\u003e\u003cspan class=\"hljs-comment\"\u003e\u0026nbsp; \u0026nbsp; \u003cem\u003e# yearday is not supported, isdst can be set but we don't do anything with it at this time\u003c/em\u003e\u003c/span\u003e\u003cspan\u003e \u003cbr\u003e\u003c/span\u003e\u003cspan class=\"hljs-built_in\"\u003e\u0026nbsp; \u0026nbsp; \u003cstrong\u003eprint\u003c/strong\u003e\u003c/span\u003e\u003cstrong\u003e(\u003cspan class=\"hljs-string\"\u003e\"Setting time to:\"\u003c/span\u003e\u003c/strong\u003e\u003cspan\u003e\u003cstrong\u003e, t)\u003c/strong\u003e \u003c/span\u003e\u003cem\u003e\u003cspan class=\"hljs-comment\"\u003e# uncomment for debugging\u003c/span\u003e\u003c/em\u003e\u003cspan\u003e \u003cbr\u003e\u0026nbsp; \u0026nbsp; \u003cstrong\u003ertc.datetime = t\u003c/strong\u003e \u003cbr\u003e\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eTo set the time on the first run c\u003cspan\u003ehange the\u0026nbsp;\u003c/span\u003e\u003cstrong\u003eFalse\u003c/strong\u003e\u003cspan\u003e\u0026nbsp;to\u0026nbsp;\u003c/span\u003e\u003cstrong\u003eTrue\u003c/strong\u003e\u003cspan\u003e in the first line and update\u0026nbsp; the\u003c/span\u003e\u003ccode\u003et = time.struct_time\u003c/code\u003e\u003cspan\u003e line to have the current time starting from\u0026nbsp;\u003c/span\u003e\u003ccode\u003eyear\u003c/code\u003e\u003cspan\u003e\u0026nbsp;to\u0026nbsp;\u003c/span\u003e\u003ccode\u003eweekday\u003c/code\u003e\u003cspan\u003e. The last two entries can stay at -1. Typically I write this in a short file first and run it before I do all of the complex coding in a project. I have included a separate, short time setting program at the end of this article that I use when setting up my RTCs.\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eTo read time in your RTC use the code:\u003c/span\u003e\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003e\u003cspan\u003et\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003ertc_pcf\u003c/span\u003e\u003c/code\u003e\u003cspan\u003e\u003ccode\u003e.datetime\u003c/code\u003e\u003cbr\u003e\u003ccode\u003eprint(f\"The date is {t.tm_mon}/{t.tm_mday}/{t.tm_year}\")\u003c/code\u003e\u003cbr\u003e\u003ccode\u003eprint(f\"The time is {t.tm_hour}:{t.tm_min:02}:{t.tm_sec:02}\")\u003c/code\u003e\u003cbr\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003eYou can learn all about the PFC8523 RTC in this Learn Guide: \u003ca class=\"title\" href=\"https://learn.adafruit.com/adafruit-pcf8523-real-time-clock\"\u003eAdafruit PCF8523 Real Time Clock\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSet up SD card:\u003c/strong\u003e Although not really used for much in the project, since the FeatherWing has an SC card slot let's set this up as well. Set up the SPI and then the SC card:\u0026nbsp;\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003e\u003cspan\u003espi\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eboard\u003c/span\u003e\u003cspan\u003e.SPI()\u003cbr\u003e\u003c/span\u003e\u003cspan\u003ecs\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eboard\u003c/span\u003e\u003cspan\u003e.D10\u003c/span\u003e\u003c/code\u003e\u003cbr\u003e\u003ccode\u003e\u003cspan\u003etry\u003c/span\u003e\u003cspan\u003e:\u003c/span\u003e\u003cspan\u003e \u003cbr\u003e\u003c/span\u003e\u003cspan\u003e\u0026nbsp; \u0026nbsp; sdcard\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003esdcardio\u003c/span\u003e\u003cspan\u003e.SDCard(\u003c/span\u003e\u003cspan\u003espi\u003c/span\u003e\u003cspan\u003e, \u003c/span\u003e\u003cspan\u003ecs\u003c/span\u003e\u003cspan\u003e)\u003c/span\u003e\u003cspan\u003e\u0026nbsp; \u0026nbsp; \u003cbr\u003e\u003c/span\u003e\u003cspan\u003e\u0026nbsp; \u0026nbsp; vfs\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003estorage\u003c/span\u003e\u003cspan\u003e.VfsFat(\u003c/span\u003e\u003cspan\u003esdcard\u003c/span\u003e\u003cspan\u003e)\u003c/span\u003e\u003cspan\u003e\u0026nbsp; \u0026nbsp; \u003cbr\u003e\u003c/span\u003e\u003cspan\u003e\u0026nbsp; \u0026nbsp; storage\u003c/span\u003e\u003cspan\u003e.mount(\u003c/span\u003e\u003cspan\u003evfs\u003c/span\u003e\u003cspan\u003e, \u003c/span\u003e\u003cspan\u003e\"/sd\"\u003c/span\u003e\u003cspan\u003e)\u003c/span\u003e\u003cspan\u003e\u0026nbsp; \u0026nbsp; \u003cbr\u003e\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan\u003eexcept\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eException\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eas\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003ee\u003c/span\u003e\u003cspan\u003e:\u003cbr\u003e\u003c/span\u003e\u003cspan\u003e\u0026nbsp; \u0026nbsp; print\u003c/span\u003e\u003cspan\u003e(\u003c/span\u003e\u003cspan\u003ef\u003c/span\u003e\u003cspan\u003e\"SD card not available: \u003c/span\u003e\u003cspan\u003e{\u003c/span\u003e\u003cspan\u003ee\u003c/span\u003e\u003cspan\u003e}\u003c/span\u003e\u003cspan\u003e\"\u003c/span\u003e\u003cspan\u003e)\u003c/span\u003e\u003cspan\u003e\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eIt is useful to use try/except blocks in case there is a failure mounting the SD card the rest of the program can still run.\u003c/p\u003e\n\u003ch2\u003ePyBadger Helper Library\u003c/h2\u003e\n\u003cp\u003eMany functions are accessed using the adafruit_pybadger library. At the top of the program we import this library:\u0026nbsp;\u003ccode\u003e\u003cspan\u003efrom\u003c/span\u003e\u003cspan\u003e adafruit_pybadger \u003c/span\u003e\u003cspan\u003eimport\u003c/span\u003e\u003cspan\u003e pybadger\u0026nbsp;\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eThe following components all use this library.\u003c/p\u003e\n\u003ch4\u003e\n\u003cstrong\u003eSet up the Neopixels\u003c/strong\u003e:\u003c/h4\u003e\n\u003cp\u003eWe use the pybadger helper library use the five Neopixels.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eThe to use the Neopixels set the brightness and set the fill to 0 (off):\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003e\u003cspan\u003epybadger.pixels.brightness \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e0.1\u003cbr\u003e\u003c/span\u003e\u003cspan\u003epybadger.pixels.fill(\u003c/span\u003e\u003cspan\u003e0\u003c/span\u003e\u003cspan\u003e)\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eIf you wanted to fill them all with red:\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003epybadger.pixels.fill(255,0,0)\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eYou can also address them individually using their index (0-4). To turn the middle pixel green use:\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003epybadger.pixels[2] = (0,255,0)\u003c/code\u003e\u003c/p\u003e\n\u003ch4\u003e\u003cstrong\u003eRead the light sensor:\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003eAgain, not currently used in this project, but it is very simple to read the value of the light sensor:\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003e\u003cspan\u003eprint\u003c/span\u003e\u003cspan\u003e(pybadger.light)\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003ePrints the current value of the light sensor.\u003c/p\u003e\n\u003ch4\u003e\u003cstrong\u003eRead the battery voltage value:\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003eThere is a voltage divided connected to pin A6. Set this up as an analog in and calculate the voltage:\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003e\u003cspan\u003ebattery_pin\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003e=\u003c/span\u003e\u003cspan\u003e \u003c/span\u003e\u003cspan\u003eanalogio\u003c/span\u003e\u003cspan\u003e.AnalogIn(\u003c/span\u003e\u003cspan\u003eboard\u003c/span\u003e\u003cspan\u003e.A6)\u003cbr\u003eraw_value = battery_pin.value\u003cbr\u003evoltage = (raw_value / 65535.0) * 3.3\u003cbr\u003ebattery_voltage = voltage * 2\u003cbr\u003e\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003ch4\u003e\u003cspan\u003e\u003cstrong\u003eUsing the PyBadge buttons:\u003c/strong\u003e \u003c/span\u003e\u003c/h4\u003e\n\u003cp\u003e\u003cspan\u003eThe PyBadge has eight buttons that can use used in your project. These are also read using the pybadger helper library. They can be read using\u0026nbsp;\u003cstrong\u003epybadger.button.xx\u003c/strong\u003e where\u0026nbsp;xx is:\u003c/span\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cspan\u003eselect\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003estart\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003ea\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003eb\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003eup\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003edown\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003eleft\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003eright\u003c/span\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4\u003e\u003cstrong\u003ePlaying sounds - wav files:\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003eThe project uses several wav files for different sound effects - essentially beeps and blips to give feedback. Playing a wav file with the helper library is simple:\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003e\u003cspan\u003epybadger.play_file(\"/wav/xx.wav\")\u003c/span\u003e\u003c/code\u003e\u003c/p\u003e\n\u003ch4\u003e\n\u003cspan\u003e\u003c/span\u003e\u003cstrong\u003ePlaying tones:\u003c/strong\u003e\n\u003c/h4\u003e\n\u003cp\u003eThe helper library also makes it simple to play tones - an alternate to playing a wav file. In this project I play a series of tones when the countdown timer reaches zero. To play a tone:\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003epybadger.play_tone(f, d)\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eWhere 'f' is the frequency of the tone (i.e. 440 for \"A\") and 'd' is the duration in seconds.\u003c/p\u003e\n\u003ch1\u003ePutting it all together\u003c/h1\u003e\n\u003cp\u003eNow that the concept is defined and the core CircuitPython elements are in place, the real work begins. The project uses six different text areas across multiple screens, each updated dynamically depending on the active mode. Several font sizes and styles help make the most of the PyBadge’s limited screen space, keeping everything readable and visually appealing. From there, the challenge becomes managing the logic for each mode and its sub‑functions, all while using non‑blocking timing loops to keep the interface responsive.\u003c/p\u003e\n\u003cp\u003eIf you’ve seen my other Playground projects, you know I use VS Code along with a Copilot Pro subscription. Having access to multiple AI agents has become an essential part of my workflow, and this project was no exception — much of the scaffolding and iterative refinement came from AI assistance.\u003c/p\u003e\n\u003cp\u003eUsing AI to develop CircuitPython projects has been a huge time‑saver for me, but it still requires a solid understanding of the program’s intent and flow. Clear prompting is key: I make a point to reference specific text areas, variable names, and behaviors to avoid ambiguity. And while AI can handle about 90% of the coding, it occasionally makes illogical choices, so I often step in to adjust and fine‑tune the generated code to ensure everything works as intended.\u003c/p\u003e\n\u003ch1\u003eBuild an Enclosure\u003c/h1\u003e\n\u003cp\u003eTo really make this a finished project I designed and 3D printed a case for the PyBadge. Although there are a number of designs out in the world, I didn't find exactly what I was looking for. I find that \u003ca href=\"https://www.tinkercad.com/\"\u003eTinkercad\u003c/a\u003e is a great, fairly simple to use free resource for designing 3D prints. Although not as feature rich or robust as more common design software (think Fusion 360, FreeCAD, Blender, SolidWorks), with a little inspiration and innovation you can accomplish a lot with combining the various standard shapes in Tinkercad.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eFor this project I designed a lid with cutouts to match the PyBadge buttons and screen and openings for the USB, switch and ports. This lid is then friction fit into a matching cutout in the box. A couple dabs of hot glue keeps it all together.\u003c/p\u003e\n\u003ch1\u003eCode\u003c/h1\u003e\n\u003ch2\u003eTime setting code\u003c/h2\u003e\n\u003cp\u003eName this code 'code.py' and copy to your board. Once the RTC clock is set you can replace this with your full project code.\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u0026nbsp;\u003c/p\u003e\n      \n\n","metadata":{}},{"element_type":"code","content":"# Simple code.py file to set the initial time on a PCF8523 RTC\n# Change the if False to if True to set the time once, then change it back to False\n# to run normally after that.\n\nimport time\nimport board\nfrom adafruit_pcf8523.pcf8523 import PCF8523\n\ndays = (\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\")\n\ni2c = board.I2C()\nrtc_pcf = PCF8523(i2c)\n\nif False:  # change to True if you want to set the time!\n    #                     year, mon, date, hour, min, sec, wday, yday, isdst\n    t = time.struct_time((2025, 12, 19, 9, 48, 0, 4, -1, -1))\n    # you must set year, mon, date, hour, min, sec and weekday\n    # yearday is not supported, isdst can be set but we don't do anything with it at this time\n    print(\"Setting time to:\", t)\n    rtc.datetime = t\n    print()\n\nprint(\"Current time:\", rtc_pcf.datetime)\nwhile True:\n    now = rtc_pcf.datetime\n    print(\n        \"{:04}-{:02}-{:02} {:02}:{:02}:{:02}\".format(\n            now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec\n        )\n    )\n    time.sleep(1)","metadata":{"language":"python","linenums":false,"filename":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n        \u003ch1\u003eFull Timer Project Code\u003c/h1\u003e\n      \n\n\n\n\n","metadata":{}},{"element_type":"code","content":"import board\nfrom adafruit_pybadger import pybadger\nfrom adafruit_bitmap_font import bitmap_font\nfrom adafruit_display_text import label\nfrom adafruit_display_shapes.rect import Rect\nfrom adafruit_pcf8523.pcf8523 import PCF8523\nimport sdcardio\nimport storage\nimport time\nimport displayio\nimport gc\nimport analogio\n\n# Constants\nMIN_COUNTDOWN = 10  # Minimum countdown time in seconds\nMAX_COUNTDOWN = 5940  # Maximum countdown time in seconds (99 minutes)\nDEFAULT_COUNTDOWN = 600  # Default countdown time in seconds (5 minutes)\nNUM_NEOPIXELS = 5  # Number of NeoPixels on the PyBadge\nSTOPWATCH_COLOR_INTERVAL = 10  # Change color every 10 seconds in stopwatch mode\n\n# Countdown color progression thresholds\nCOUNTDOWN_TWO_THIRDS = 0.67  # Change to yellow at 2/3 time remaining\nCOUNTDOWN_ONE_THIRD = 0.33  # Change to magenta at 1/3 time remaining\n\n# Color constants\nRESET_COLOR = (255, 0, 0)  # Red color for reset state\nCOLOR_OFF = (0, 0, 0)  # Off/Black\nCOLOR_GREEN = (0, 255, 0)  # Green\nCOLOR_BLUE = (0, 0, 255)  # Blue\nCOLOR_YELLOW = (255, 255, 0)  # Yellow\nCOLOR_MAGENTA = (255, 0, 255)  # Magenta\n\n# Mode names\nMODE_NAMES = ('Stopwatch', 'Countdown', 'Timer', 'Clock')\n\ndef convert_to_12hour(hour):\n    \"\"\"Convert 24-hour time to 12-hour format with AM/PM\"\"\"\n    if hour == 0:\n        return 12, \"AM\"\n    elif hour \u003c 12:\n        return hour, \"AM\"\n    elif hour == 12:\n        return 12, \"PM\"\n    else:\n        return hour - 12, \"PM\"\n\ndef play_song():\n    for i in range(len(tns)):\n        print(\"Playing tone:\", tns[i])\n        pybadger.play_tone(tns[i], 0.1)\n\ndef adjust_countdown_time(amount):\n    global countdown_duration\n    pybadger.play_file(wvs[1])\n    countdown_duration += amount\n    # Clamp between minimum and maximum\n    if countdown_duration \u003c MIN_COUNTDOWN:\n        countdown_duration = MIN_COUNTDOWN\n    elif countdown_duration \u003e MAX_COUNTDOWN:\n        countdown_duration = MAX_COUNTDOWN\n    print(f\"Countdown set to {countdown_duration} seconds\")\n\ndef time_display():\n    t = rtc_pcf.datetime\n    hour_12, am_pm = convert_to_12hour(t.tm_hour)\n    return f\"{hour_12}:{t.tm_min:02d} {am_pm}\"\n\ndef rotate_neopixel(current_time, color, interval):\n    \"\"\"Rotate NeoPixel animation - non-blocking with configurable interval\"\"\"\n    global current_pixel, last_pixel_update\n    if current_time - last_pixel_update \u003e= interval:\n        # Turn off current pixel\n        pybadger.pixels[current_pixel] = COLOR_OFF\n        # Move to next pixel\n        current_pixel += 1\n        if current_pixel \u003e NUM_NEOPIXELS - 1:\n            current_pixel = 0\n        # Turn on new pixel\n        pybadger.pixels[current_pixel] = color\n        last_pixel_update = current_time\n\ndisplay = board.DISPLAY\n\ni2c = board.I2C()\nrtc_pcf = PCF8523(i2c)\n\nspi = board.SPI()\ncs = board.D10\n\n# Try to mount SD card with error handling\ntry:\n    sdcard = sdcardio.SDCard(spi, cs)\n    vfs = storage.VfsFat(sdcard)\n    storage.mount(vfs, \"/sd\")\n    sd_available = True\n    print(\"SD card mounted successfully\")\nexcept Exception as e:\n    sd_available = False\n    print(f\"SD card not available: {e}\")\n\nt = rtc_pcf.datetime\n\n# Log startup time to SD card if available\nif sd_available:\n    try:\n        on_time = f\"{t.tm_year}{t.tm_mon:02}{t.tm_mday:02}_{t.tm_hour:02}{t.tm_min:02}{t.tm_sec:02}\"\n        print(on_time)\n        with open(\"/sd/starts.csv\", \"a\") as f:\n            f.write(on_time + \"\\r\\n\")\n    except Exception as e:\n        print(f\"Failed to write to SD card: {e}\")\n\nprint(f\"The date is {t.tm_mon}/{t.tm_mday}/{t.tm_year}\")\nprint(f\"The time is {t.tm_hour}:{t.tm_min:02}:{t.tm_sec:02}\")\n\npybadger.pixels.brightness = 0.1\npybadger.pixels.fill(0)\nprint(pybadger.light)\n\nwvs = [                   # This is a list of wav files available - to use a file refer to the list index i.e. wvs[0] for the first file\n    \"/wav/click3.wav\",\n    \"/wav/pop2.wav\",\n    \"/wav/blip2.wav\",\n    \"/wav/snare_01.wav\",\n    \"/wav/beep.wav\",\n    \"/wav/confirmation_001.wav\",\n    \"/wav/confirmation_002.wav\",\n    \"/wav/confirmation_003.wav\",\n    \"/wav/confirmation_004.wav\",\n    \"/wav/threeTone1.wav\",\n    \"/wav/threeTone2.wav\",\n    \"/wav/twoTone1.wav\",\n    \"/wav/twoTone2.wav\",\n    \"/wav/crash_01.wav\",\n    \"/wav/chimes.wav\",\n    \"/wav/wand.wav\",\n]\n\ntns = [      # This is a little ditty of notes to play when the countdown timer expires - know the tune?\n    659.3, 622.3, 659.3, 622.3, 659.3, \n    493.9, 587.3, 523.3, 440.0, 440, 440, 440, 440, 0, 0, 0, 494, 494, 494, 494, 494, 0, 0, 0, 523, 523, 523, 523, 523\n]\n\nfont8 = bitmap_font.load_font(\"/fonts/Calibri-12.pcf\")\nfont15 = bitmap_font.load_font(\"/fonts/Calibri-15.pcf\")\nfont12 = bitmap_font.load_font(\"/fonts/ComicSansMS-10.pcf\")\nfont38 = bitmap_font.load_font(\"/fonts/Calibri-38.pcf\")\n\nmain_group = displayio.Group()\n\nbackground = Rect(0, 0, display.width, display.height, fill=0x000088)\n\n# Display layout positions\nLABEL_X_OFFSET = 5\nTITLE_Y_POS = 5\nMAIN_DISPLAY_Y_POS = 25\nSTATUS_Y_POS = 65\nINFO_Y_POS = 100\n\nbv_label = label.Label(font12, anchored_position=(display.width - 5, display.height - 5), anchor_point=(1.0, 1.0), text=\"\", color=0x9999FF)\nline_1 = label.Label(font15, anchored_position=(LABEL_X_OFFSET, TITLE_Y_POS), anchor_point=(0.0, 0.0), text=\"\", color=0xFFAA33)\nline_1a = label.Label(font38, anchored_position=(80, MAIN_DISPLAY_Y_POS), anchor_point=(0.5, 0.0), text=\"\", color=0xFFFF00)\nline_2 = label.Label(font15, anchored_position=(LABEL_X_OFFSET, STATUS_Y_POS), anchor_point=(0.0, 0.0), text=\"\", color=0xFFFFFF)\nline_3 = label.Label(font12, anchored_position=(LABEL_X_OFFSET, INFO_Y_POS), anchor_point=(0.0, 0.0), text=\"\", color=0x00FF00)\nline_clk = label.Label(font8, anchored_position=(display.width - 3, 3), anchor_point=(1.0, 0.0), text=\"\", color=0xFFFFFF)\n\nmain_group.append(background)\nmain_group.append(line_1)\nmain_group.append(line_1a)\nmain_group.append(line_2)\nmain_group.append(line_3)\nmain_group.append(bv_label)\nmain_group.append(line_clk)\n\ndisplay.root_group = main_group\n\n# Mode: 0 = Stopwatch, 1 = Countdown, 2 = Clock, 3 = Timer\nmode = 0\n\n# Stopwatch variables\nstopwatch_running = False\nstopwatch_start_time = 0\nstopwatch_elapsed = 0\nstopwatch_lap_mode = False  # True when displaying lap time\nstopwatch_lap_time = 0  # The frozen lap time to display\n\n# Timer variables\ntimer_running = False\ntimer_start_time = 0\ntimer_elapsed = 0\ntimer_clock_start_hour = 0\ntimer_clock_start_min = 0\n\n# Countdown variables\ncountdown_running = False\ncountdown_paused = False\ncountdown_start_time = 0\ncountdown_pause_elapsed = 0\ncountdown_duration = DEFAULT_COUNTDOWN\ncountdown_remaining = 0\nlast_minute_notified = -1  # Track last minute we played sound for\n\n# Button state tracking for debouncing\nstart_pressed = False\nselect_pressed = False\na_pressed = False\nb_pressed = False\nup_pressed = False\ndown_pressed = False\nleft_pressed = False\nright_pressed = False\n\n# Color cycling for NeoPixels\ncolors = [(0, 255, 0), (255, 0, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255)]\n# Green, Red, Blue, Yellow, Magenta, Cyan\ncolor_index = 0\n\n# Display update tracking - only update when values change\nprev_stopwatch_time = \"\"\nprev_stopwatch_status = \"\"\nprev_countdown_time = \"\"\nprev_countdown_status = \"\"\nprev_countdown_set = \"\"\nprev_mode = -1  # Track mode changes\n\n# NeoPixel animation tracking\ncurrent_pixel = 0\npcolor = colors[0]\nlast_pixel_update = 0  # Timestamp of last NeoPixel update\nPIXEL_UPDATE_INTERVAL = 1.0  # Update NeoPixels every 1 second in countdown mode\n\n# Memory monitoring\nlast_gc_time = 0\nGC_INTERVAL = 5.0  # Run garbage collection every 5 seconds\nlast_mem_update = 0\nMEM_UPDATE_INTERVAL = 2.0  # Update memory display every 2 seconds\n\n# Clock display monitoring\nlast_clk_update = 0\nCLK_UPDATE_INTERVAL = 30.0  # Update clock display every 30 seconds\n\n# Battery voltage monitoring\nbattery_voltage_cached = 0.0\nlast_battery_update = 0\nBATTERY_UPDATE_INTERVAL = 10.0  # Update battery voltage every 10 seconds\n\n# Clock mode optimization\nprev_clock_minute = -1  # Track last displayed minute in clock mode\n\n# Initial garbage collection\ngc.collect()\nprint(f\"Initial free memory: {gc.mem_free()} bytes\")\n\n# Set up battery voltage monitoring on A6\nbattery_pin = analogio.AnalogIn(board.A6)\n\ndef get_battery_voltage():\n    \"\"\"Read the battery voltage from A6 (connected to voltage divider)\"\"\"\n    # Read the raw analog value (0-65535)\n    raw_value = battery_pin.value\n    # Convert to voltage (reference voltage is typically 3.3V)\n    voltage = (raw_value / 65535.0) * 3.3\n    # If using a voltage divider, multiply by the divider ratio\n    # For PyBadge, the divider is typically 2:1, so multiply by 2\n    battery_voltage = voltage * 2\n    return battery_voltage\n\n# Initialize and print battery voltage\nbattery_voltage_cached = get_battery_voltage()\nprint(f\"Battery voltage: {battery_voltage_cached:.2f}V\")\n\n# Loop forever so you can enjoy your text\nwhile True:\n    # Get current time once at start of loop\n    current_time = time.monotonic()\n    \n    # SELECT button - toggle between modes with debouncing\n    if pybadger.button.select:\n        bv_label.text = f\"{battery_voltage_cached:.2f}v\"\n        if not select_pressed:\n            select_pressed = True\n            # Only allow mode switch when nothing is running\n            if not stopwatch_running and not countdown_running and not timer_running:\n                pybadger.play_file(wvs[8])\n                mode = (mode + 1) % 4  # Cycle through 0, 1, 2, 3\n                print(f\"Mode: {MODE_NAMES[mode]}\")\n    else:\n        select_pressed = False\n    \n    if mode == 0:  # STOPWATCH MODE\n        # Update mode title if mode changed\n        if mode != prev_mode:\n            line_1.text = \"STOPWATCH\"\n            line_3.text = \"START=Run/Stop - B for Lap\\nSELECT=Mode   A=Reset\"\n            line_1a.text = \"00:00.0\"\n            line_2.text = \"STOPPED\"\n            prev_stopwatch_time = \"00:00.0\"\n            prev_stopwatch_status = \"STOPPED\"\n            prev_mode = mode\n        \n        # START button - toggle start/stop\n        if pybadger.button.start:\n            if not start_pressed:\n                start_pressed = True\n                pybadger.play_file(wvs[2])\n                if not stopwatch_running:\n                    stopwatch_running = True\n                    stopwatch_start_time = time.monotonic() - stopwatch_elapsed\n                    print(\"Stopwatch started\")\n                else:\n                    stopwatch_running = False\n                    stopwatch_elapsed = time.monotonic() - stopwatch_start_time\n                    print(\"Stopwatch stopped\")\n        else:\n            start_pressed = False\n        \n        # A button - reset\n        if pybadger.button.a:\n            if not a_pressed and not stopwatch_running:  # Only allow reset when stopped\n                a_pressed = True\n                pybadger.play_file(wvs[9])\n                stopwatch_running = False\n                stopwatch_elapsed = 0\n                stopwatch_start_time = 0\n                stopwatch_lap_mode = False\n                stopwatch_lap_time = 0\n                print(\"Stopwatch reset\")\n                pcolor = RESET_COLOR\n        else:\n            a_pressed = False\n        \n        # B button - lap timing (only when running)\n        if pybadger.button.b:\n            if not b_pressed and stopwatch_running:\n                b_pressed = True\n                pybadger.play_file(wvs[7])\n                if not stopwatch_lap_mode:\n                    # Freeze display at current time\n                    stopwatch_lap_time = time.monotonic() - stopwatch_start_time\n                    stopwatch_lap_mode = True\n                    print(f\"Lap time: {stopwatch_lap_time:.1f}s\")\n                else:\n                    # Resume showing current time\n                    stopwatch_lap_mode = False\n                    print(\"Lap resumed\")\n        else:\n            b_pressed = False\n        \n        # Update elapsed time if running\n        if stopwatch_running:\n            stopwatch_elapsed = time.monotonic() - stopwatch_start_time\n            # Change color based on interval\n            color_index = int(stopwatch_elapsed // STOPWATCH_COLOR_INTERVAL) % len(colors)\n            pcolor = colors[color_index]\n        \n        # Display stopwatch time\n        # Use lap time if in lap mode, otherwise use current elapsed time\n        display_time = stopwatch_lap_time if stopwatch_lap_mode else stopwatch_elapsed\n        tenths = int(display_time * 10)\n        seconds = tenths // 10\n        remaining_tenths = tenths % 10\n        minutes = seconds // 60\n        remaining_seconds = seconds % 60\n        \n        # Only update display if time changed\n        display_time_str = f\"{minutes:02d}:{remaining_seconds:02d}.{remaining_tenths}\"\n        if display_time_str != prev_stopwatch_time:\n            line_1a.text = display_time_str\n            prev_stopwatch_time = display_time_str\n        \n        # Only update status if changed\n        if stopwatch_lap_mode:\n            current_status = \"LAP - B to RESUME\"\n        else:\n            current_status = \"RUNNING\" if stopwatch_running else \"STOPPED\"\n        if current_status != prev_stopwatch_status:\n            line_2.text = current_status\n            prev_stopwatch_status = current_status\n        \n        # Animate NeoPixels while running (non-blocking)\n        if stopwatch_running:\n            rotate_neopixel(current_time, pcolor, 0.1)\n        \n        # Small sleep to prevent tight looping\n        time.sleep(0.01)\n    \n    elif mode == 1:  # COUNTDOWN MODE\n        # Update mode title if mode changed\n        if mode != prev_mode:\n            line_1.text = \"COUNTDOWN\"\n            # Display countdown in MM:SS format\n            total_seconds = int(countdown_duration)\n            minutes = total_seconds // 60\n            seconds = total_seconds % 60\n            line_1a.text = f\"{minutes:02d}:{seconds:02d}\"\n            line_2.text = \"STOPPED\\nUP/DOWN to set time\"\n            prev_countdown_time = f\"{minutes:02d}:{seconds:02d}\"\n            prev_countdown_status = \"STOPPED\\nUP/DOWN to set time\"\n            prev_mode = mode\n        \n        # UP button - increase countdown time (when not running)\n        if pybadger.button.up:\n            if not up_pressed and not countdown_running:\n                up_pressed = True\n                adjust_countdown_time(60)  # Add 1 minute\n        else:\n            up_pressed = False\n        \n        # DOWN button - decrease countdown time (when not running)\n        if pybadger.button.down:\n            if not down_pressed and not countdown_running:\n                down_pressed = True\n                adjust_countdown_time(-60)  # Subtract 1 minute\n        else:\n            down_pressed = False\n        \n        # RIGHT button - increase countdown time by 10 seconds (when not running)\n        if pybadger.button.right:\n            if not right_pressed and not countdown_running:\n                right_pressed = True\n                adjust_countdown_time(10)  # Add 10 seconds\n        else:\n            right_pressed = False\n        \n        # LEFT button - decrease countdown time by 10 seconds (when not running)\n        if pybadger.button.left:\n            if not left_pressed and not countdown_running:\n                left_pressed = True\n                adjust_countdown_time(-10)  # Subtract 10 seconds\n        else:\n            left_pressed = False\n        \n        # B button - reset countdown time to default (when not running)\n        if pybadger.button.b:\n            if not b_pressed and not countdown_running:\n                b_pressed = True\n                pybadger.play_file(wvs[4])\n                countdown_duration = DEFAULT_COUNTDOWN\n                print(f\"Countdown reset to {DEFAULT_COUNTDOWN // 60}:00\")\n        else:\n            b_pressed = False\n        \n        # START button - toggle start/pause countdown\n        if pybadger.button.start:\n            if not start_pressed:\n                start_pressed = True\n                pybadger.play_file(wvs[5])\n                if not countdown_running:\n                    # Start or resume countdown\n                    countdown_running = True\n                    countdown_paused = False\n                    countdown_start_time = time.monotonic() - countdown_pause_elapsed\n                    print(\"Countdown started\")\n                elif countdown_paused:\n                    # Resume from pause\n                    countdown_paused = False\n                    countdown_start_time = time.monotonic() - countdown_pause_elapsed\n                    print(\"Countdown resumed\")\n                else:\n                    # Pause the countdown\n                    countdown_paused = True\n                    countdown_pause_elapsed = time.monotonic() - countdown_start_time\n                    print(\"Countdown paused\")\n        else:\n            start_pressed = False\n        \n        # A button - reset countdown (only when stopped or paused)\n        if pybadger.button.a and (not countdown_running or countdown_paused):\n            if not a_pressed:\n                a_pressed = True\n                pybadger.play_file(wvs[6])\n                countdown_running = False\n                countdown_paused = False\n                countdown_start_time = 0\n                countdown_pause_elapsed = 0\n                last_minute_notified = -1\n                print(\"Countdown reset\")\n                pcolor = RESET_COLOR\n        else:\n            a_pressed = False\n        \n        # Update countdown\n        if countdown_running and not countdown_paused:\n            elapsed = time.monotonic() - countdown_start_time\n            countdown_remaining = countdown_duration - elapsed\n            \n            # Check for even minute milestones\n            minutes_remaining = int(countdown_remaining // 60)\n            if minutes_remaining \u003e 0 and minutes_remaining % 2 == 0 and minutes_remaining != last_minute_notified:\n                pybadger.play_file(wvs[12])\n                last_minute_notified = minutes_remaining\n                print(f\"Even minute reached: {minutes_remaining}\")\n            \n            if countdown_remaining \u003c= 0:\n                countdown_remaining = 0\n                countdown_running = False\n                countdown_paused = False\n                countdown_pause_elapsed = 0\n                print(\"Countdown finished!\")\n                # Play song 3 times with 2 second pauses, allowing cancellation\n                for i in range(3):\n                    play_song()\n                    if i \u003c 2:  # Don't pause after the last song\n                        # Check for button press to cancel alarm\n                        cancel_start = time.monotonic()\n                        while time.monotonic() - cancel_start \u003c 2:\n                            if pybadger.button.a or pybadger.button.start or pybadger.button.select:\n                                print(\"Alarm cancelled\")\n                                break\n                            time.sleep(0.05)\n                        else:\n                            continue\n                        # If we broke out of while loop, break out of for loop too\n                        break\n                pcolor = RESET_COLOR  # Red when done\n            else:\n                # Color based on time remaining\n                if countdown_remaining \u003c= 60:  # Last minute\n                    pcolor = RESET_COLOR  # Red\n                elif countdown_remaining \u003c= countdown_duration * COUNTDOWN_ONE_THIRD:\n                    pcolor = COLOR_MAGENTA\n                elif countdown_remaining \u003c= countdown_duration * COUNTDOWN_TWO_THIRDS:\n                    pcolor = COLOR_YELLOW\n                else:\n                    pcolor = COLOR_GREEN\n        elif countdown_paused:\n            # When paused, set color to blue and preserve remaining time\n            pcolor = COLOR_BLUE\n            countdown_remaining = countdown_duration - countdown_pause_elapsed\n        else:\n            countdown_remaining = countdown_duration\n        \n        # Display countdown time\n        total_seconds = int(countdown_remaining)\n        minutes = total_seconds // 60\n        seconds = total_seconds % 60\n        \n        # Only update display if time changed\n        display_time_str = f\"{minutes:02d}:{seconds:02d}\"\n        if display_time_str != prev_countdown_time:\n            line_1a.text = display_time_str\n            prev_countdown_time = display_time_str\n        \n        # Only update status if changed\n        if countdown_paused:\n            current_status = \"PAUSED\"\n        elif countdown_running:\n            current_status = \"RUNNING\"\n        else:\n            current_status = \"STOPPED\\nUP/DOWN to set time\"\n        \n        if current_status != prev_countdown_status:\n            line_2.text = current_status\n            prev_countdown_status = current_status\n        \n        # Only update set time if changed\n        set_minutes = countdown_duration // 60\n        set_seconds = countdown_duration % 60\n        current_set = f\"Set: {set_minutes:02d}:{set_seconds:02d}\"\n        if current_set != prev_countdown_set:\n            line_3.text = current_set\n            prev_countdown_set = current_set\n        \n        # Animate NeoPixels while running (non-blocking, updates every second)\n        if countdown_running and not countdown_paused:\n            rotate_neopixel(current_time, pcolor, PIXEL_UPDATE_INTERVAL)\n        elif countdown_paused:\n            # Keep current pixel lit blue when paused\n            if current_pixel \u003e NUM_NEOPIXELS - 1:\n                current_pixel = NUM_NEOPIXELS - 1\n            pybadger.pixels[current_pixel] = pcolor  # Light up blue\n        \n        # Small sleep to prevent tight looping\n        time.sleep(0.01)\n    \n    elif mode == 2:  # TIMER MODE\n        # Update mode title if mode changed\n        if mode != prev_mode:\n            line_1.text = \"TIMER\"\n            line_3.text = \"START=Run/Stop   A=Reset\\nSELECT=Mode\"\n            line_1a.text = \"00:00\"\n            line_2.text = \"Ready\"\n            prev_mode = mode\n        \n        # START button - toggle start/stop\n        if pybadger.button.start:\n            if not start_pressed:\n                start_pressed = True\n                pybadger.play_file(wvs[2])\n                if not timer_running:\n                    timer_running = True\n                    timer_start_time = time.monotonic() - timer_elapsed\n                    # Capture the current clock time only when starting from zero\n                    if timer_elapsed == 0:\n                        t = rtc_pcf.datetime\n                        timer_clock_start_hour = t.tm_hour\n                        timer_clock_start_min = t.tm_min\n                        print(f\"Timer started at {timer_clock_start_hour}:{timer_clock_start_min:02d}\")\n                    else:\n                        print(\"Timer resumed\")\n                else:\n                    timer_running = False\n                    timer_elapsed = time.monotonic() - timer_start_time\n                    print(\"Timer stopped\")\n        else:\n            start_pressed = False\n        \n        # A button - reset\n        if pybadger.button.a:\n            if not a_pressed and not timer_running:  # Only allow reset when stopped\n                a_pressed = True\n                pybadger.play_file(wvs[9])\n                timer_running = False\n                timer_elapsed = 0\n                timer_start_time = 0\n                timer_clock_start_hour = 0\n                timer_clock_start_min = 0\n                print(\"Timer reset\")\n                pcolor = RESET_COLOR\n        else:\n            a_pressed = False\n        \n        # Update elapsed time if running\n        if timer_running:\n            timer_elapsed = time.monotonic() - timer_start_time\n            # Change color based on time\n            color_index = int(timer_elapsed // 10) % len(colors)\n            pcolor = colors[color_index]\n        \n        # Display timer time\n        total_seconds = int(timer_elapsed)\n        hours = total_seconds // 3600\n        remaining_seconds = total_seconds % 3600\n        minutes = remaining_seconds // 60\n        seconds = remaining_seconds % 60\n        \n        # Display format changes based on elapsed time\n        if hours \u003e 0 or total_seconds \u003e= 3600:  # Show hours:minutes after 60 minutes\n            timer_display = f\"{hours:02d}:{minutes:02d}\"\n        else:  # Show minutes:seconds\n            timer_display = f\"{minutes:02d}:{seconds:02d}\"\n        \n        line_1a.text = timer_display\n        \n        # Display start time instead of RUNNING/STOPPED\n        if timer_running or timer_elapsed \u003e 0:\n            # Convert to 12-hour format\n            hour_12, am_pm = convert_to_12hour(timer_clock_start_hour)\n            start_time_str = f\"Started: {hour_12}:{timer_clock_start_min:02d} {am_pm}\"\n            line_2.text = start_time_str\n        else:\n            line_2.text = \"Ready\"\n        \n        # Animate NeoPixels while running (rotate every second)\n        if timer_running:\n            rotate_neopixel(current_time, pcolor, 1.0)\n        else:\n            time.sleep(0.01)\n    \n    elif mode == 3:  # CLOCK MODE\n        # Update mode title if mode changed\n        if mode != prev_mode:\n            line_1.text = \"CLOCK\"\n            line_3.text = \"SELECT=Mode\"\n            prev_mode = mode\n            prev_clock_minute = -1  # Force update on mode entry\n        \n        # Get current time from RTC\n        t = rtc_pcf.datetime\n        \n        # Only update display when minute changes\n        if t.tm_min != prev_clock_minute:\n            # Convert to 12-hour format with AM/PM\n            hour_12, am_pm = convert_to_12hour(t.tm_hour)\n            \n            # Format time string\n            time_str = f\"{hour_12}:{t.tm_min:02d} {am_pm}\"\n            line_1a.text = time_str\n            \n            # Format date string\n            date_str = f\"{t.tm_mon}/{t.tm_mday}/{t.tm_year}\"\n            line_2.text = date_str\n            \n            prev_clock_minute = t.tm_min\n        \n        # Set NeoPixels to off\n        pcolor = COLOR_OFF\n        pybadger.pixels.fill(pcolor)\n        \n        # Small sleep\n        time.sleep(0.5)\n    \n    # Periodic garbage collection\n    if current_time - last_gc_time \u003e= GC_INTERVAL:\n        gc.collect()\n        last_gc_time = current_time\n    \n    # Periodic memory monitoring\n    if current_time - last_mem_update \u003e= MEM_UPDATE_INTERVAL:\n        mem_free_kb = gc.mem_free() // 1024\n        last_mem_update = current_time\n    \n    # Update clock display\n    if current_time - last_clk_update \u003e= CLK_UPDATE_INTERVAL:\n        line_clk.text = time_display()\n        last_clk_update = current_time\n    \n    # Update battery voltage cache\n    if current_time - last_battery_update \u003e= BATTERY_UPDATE_INTERVAL:\n        battery_voltage_cached = get_battery_voltage()\n        last_battery_update = current_time","metadata":{"language":"python","linenums":false,"filename":""}}]