[{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eA Little Rest-Interval Timer\u003c/h2\u003e\n\u003ch4\u003eUsing CircuitPython on a Particle Argon (or any CP Feather really)\u003c/h4\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"image_url","content":"https://github.com/user-attachments/assets/b733185c-fc51-4e9e-acb0-97bfc02ac590","metadata":{"alt":"Hero shot. The finished timer being held in a hand showing 1:30 on the 7-segment display, with the knob and button in frame.","caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eInspiration\u003c/h2\u003e\n\u003cp\u003eThis project was born from two desires. One, I'm sick of having to pull out my phone to operate workout timers. They technically work, but reaching for my phone means \u003cem\u003elooking\u003c/em\u003e at my phone, and looking at my phone means thirty seconds of rest becomes a four-minute scroll through who-knows-what. I wanted a single-purpose gadget had one button so I could always use it correctly even when exhausted.\u003c/p\u003e\n\u003cp\u003eTwo, I had a \u003cstrong\u003eParticle Argon\u003c/strong\u003e (an nRF52840 board) gathering dust ever since I got it on a massive sale. It came with a breakout board and a bunch of hardware add-ons that could be connected quickly with Grove connector cables, so I figured I'd see if \u003cstrong\u003eCircuitPython\u003c/strong\u003e could give it a second life as something gloriously, defiantly offline.\u003c/p\u003e\n\u003cp\u003eSpoiler: it can, and the result is perhaps one of the \u003cem\u003eleast\u003c/em\u003e connected IoT devices I've ever built. Which, after the gym-phone problem above, is exactly the point. ;-)\u003c/p\u003e\n      \n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eMaterials\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eParticle Argon (nRF52840)\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eSalvaged from my own drawer of abandoned IoT ambitions\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca title=\"Grove Shield\" href=\"https://www.adafruit.com/product/4309\" target=\"_blank\"\u003e\u003cstrong\u003eGrove Shield for Particle Mesh\u003c/strong\u003e\u003c/a\u003e (\u003ca href=\"https://www.adafruit.com/product/2926\"\u003ethis would also work well if you don't like Grove connectors\u003c/a\u003e)\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eSaves me a breadboard and a fistful of jumper wires\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eTM1637 4-digit 7-segment display\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eFrom the parts bin; the cheap red clock-style module\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003ePiezo buzzer\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eParts bin\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003ePotentiometer\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eParts bin\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eMomentary push button\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eParts bin\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eLipo Battery\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eFrom a junk swap table at a conference\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eEnclosure\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eLOLOL, enclosure? Nah, I just wrapped the thing in some electrical tape\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cem\u003eSalvage percentage ~10%. I'll be honest, this is my *least* salvage-y build (I did by everything but the battery at some point), but a useful gym gadget built in under an hour from an orphaned dev board and the contents of a parts bin?! Cool!\u003c/em\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n        \u003ch2\u003eA dumb timer is a good timer\u003c/h2\u003e\n\u003cp\u003eLet me get ahead of the obvious question: \u003cem\u003ewhy a whole nRF52840 for a countdown timer?\u003c/em\u003e Easy, because I already had it, and because the whole appeal is that it does \u003cstrong\u003eone thing\u003c/strong\u003e. No Wi-Fi, no app, no account, no firmware-update nag, no notification ever. You turn a knob to dial in a rest interval, press a button, and it counts down. When time's up, it beeps three times. That's the entire feature set, and I love it for that. Also, this project was an exercise in rapid development. I wanted to spend as little time on this as possible so I grabed something that would require zero soldering and could be one-shot vibe coded.\u003c/p\u003e\n\u003cp\u003eThe interface is exactly two controls:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eThe knob (potentiometer)\u003c/strong\u003e picks the rest interval, anywhere from \u003cstrong\u003e0:15 to 2:00 in 5-second steps\u003c/strong\u003e, and shows it on the display as \u003ccode class=\"inline\"\u003eMM:SS\u003c/code\u003e while you're idle.\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eThe button\u003c/strong\u003e starts the countdown. Press it \u003cem\u003eagain\u003c/em\u003e mid-countdown to pause/unpause (the display blinks while paused so you know it's not just frozen). Need to bail entirely? Tap the board's RESET.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWhen it hits \u003ccode class=\"inline\"\u003e0:00\u003c/code\u003e, the buzzer fires three short beeps over about a second and it drops back to idle, ready for the next set.\u003c/p\u003e\n      \n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n        \u003ch2\u003eBit-banging the TM1637 (it looks like I2C, but it lies)\u003c/h2\u003e\n\u003cp\u003eThe one genuinely fiddly bit is the display. The TM1637 has two data pins labeled \u003ccode class=\"inline\"\u003eCLK\u003c/code\u003e and \u003ccode class=\"inline\"\u003eDIO\u003c/code\u003e, which makes it look \u003cem\u003eexactly\u003c/em\u003e like an I2C device, and it is absolutely \u003cstrong\u003enot\u003c/strong\u003e I2C. It's a custom two-wire protocol with its own timing, and if you try to talk to it with a hardware I2C peripheral you'll get nothing but confusion.\u003c/p\u003e\n\u003cp\u003eIf you're also using this part, check out the tiny, self-contained driver (\u003ccode class=\"inline\"\u003etm1637.py\u003c/code\u003e) that bit-bangs the protocol directly with two GPIO pins, no external library to install. The upside of doing it by hand is there's nothing to go wrong in a dependency you didn't write; the downside is you get to learn the TM1637's data sheet whether you wanted to or not. (You didn't want to. It's fine.)\u003c/p\u003e\n      \n\n\n","metadata":{}},{"element_type":"image_url","content":"https://github.com/user-attachments/assets/70a9dc10-b8e2-42cc-9a4a-1ff9e8b27083","metadata":{"alt":"The parts and wiring used in the device layed out on a workout bench","caption":""}},{"element_type":"text","content":"\n        \u003ch2\u003eSetup Challenges\u003c/h2\u003e\n\u003cp\u003eHonestly, the hardware is forgiving, but here are the things most likely to trip you up. The hardest part is getting CircuitPython onto a Particle board (if you're using one of those):\u003c/p\u003e\n\u003ch4\u003e\u003cstrong\u003eGetting CircuitPython onto the Argon (pain in the... advanced)\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003eThe Argon doesn't ship with CircuitPython, it ships expecting Particle's world. You have to use a special programmer dongle like a \u003ca href=\"https://www.adafruit.com/product/3571\"\u003eJ-LINK (edu is fine)\u003c/a\u003e to flash the CircuitPython \u003ccode class=\"inline\"\u003e.uf2\u003c/code\u003e bootloader. \u003ca href=\"https://learn.adafruit.com/circuitpython-on-the-nrf52/build-flash-particle\"\u003e(See this Adafruit guide)\u003c/a\u003e Annoying, but from then on it shows up as a \u003ccode class=\"inline\"\u003eCIRCUITPY\u003c/code\u003e USB drive. The lovely part: this whole project uses \u003cstrong\u003eonly built-in modules\u003c/strong\u003e (\u003ccode class=\"inline\"\u003eboard\u003c/code\u003e, \u003ccode class=\"inline\"\u003eanalogio\u003c/code\u003e, \u003ccode class=\"inline\"\u003epwmio\u003c/code\u003e, \u003ccode class=\"inline\"\u003ekeypad\u003c/code\u003e, \u003ccode class=\"inline\"\u003edigitalio\u003c/code\u003e), there's no \u003ccode class=\"inline\"\u003elib/\u003c/code\u003e folder to populate. Deploy = drag three files onto the drive. The board reboots and runs. That's it.\u003c/p\u003e\n\u003ch4\u003e\u003cstrong\u003e\"My digits are scrambled\" (beginner)\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003eYou wired CLK and DIO backwards. Swap A4 and A5. I left a comment in the code pointing right at this because I \u003cem\u003eguarantee\u003c/em\u003e somebody (possibly future-me) does it.\u003c/p\u003e\n\u003ch4\u003e\u003cstrong\u003e\"The buzzer is silent\" / \"the knob does nothing\" (beginner)\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003ePin assignments live in one \u003ccode class=\"inline\"\u003eCONFIG\u003c/code\u003e block at the top of \u003ccode class=\"inline\"\u003ecode.py\u003c/code\u003e. If the buzzer's quiet, try moving it from \u003ccode class=\"inline\"\u003eD4\u003c/code\u003e to \u003ccode class=\"inline\"\u003eD5\u003c/code\u003e; if the pot's unresponsive, try \u003ccode class=\"inline\"\u003eA1\u003c/code\u003e instead of \u003ccode class=\"inline\"\u003eA0\u003c/code\u003e. Two-line fixes.\u003c/p\u003e\n\u003ch4\u003e\u003cstrong\u003e\"The button is backwards\" (intermediate)\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003eThis one's sneaky. A plain push button wired between the pin and ground reads \u003cstrong\u003eLOW\u003c/strong\u003e when pressed (internal pull-up). But some \u003cstrong\u003eGrove button modules drive the line HIGH\u003c/strong\u003e when pressed, the opposite polarity. So instead of hard-coding it, the firmware reads a \u003ccode class=\"inline\"\u003eBUTTON_ACTIVE_HIGH\u003c/code\u003e flag from \u003ccode class=\"inline\"\u003esettings.toml\u003c/code\u003e. If your button does the opposite of what you expect, flip that flag instead of rewiring anything.\u003c/p\u003e\n\u003ch2\u003eCode\u003c/h2\u003e\n\u003cp\u003eThe whole program is a small, readable state machine, \u003ccode class=\"inline\"\u003eIDLE → RUNNING → PAUSED\u003c/code\u003e, that fits comfortably on one screen. Here it is in full, because half the joy of a project this size is that you \u003cem\u003ecan\u003c/em\u003e show the whole thing:\u003c/p\u003e\n      ","metadata":{}},{"element_type":"code","content":"# code.py ## Workout Rest-Interval Helper (CircuitPython, Particle Argon)\nimport time\nimport os\nimport board\nimport analogio\nimport pwmio\nimport keypad\nfrom tm1637 import TM1637\n\n# CONFIG — adjust pins here if your wiring differs\nCLK_PIN = board.A4        # TM1637 clock\nDIO_PIN = board.A5        # TM1637 data   (swap A4/A5 if digits look scrambled)\nPOT_PIN = board.A0        # potentiometer wiper (try board.A1 if no response)\nBUZZER_PIN = board.D4     # piezo buzzer  (try board.D5 if silent)\nBUTTON_PIN = board.D2     # push button\n\n# Some Grove button modules drive the line HIGH when pressed — flip this if so.\nBUTTON_ACTIVE_HIGH = os.getenv(\"BUTTON_ACTIVE_HIGH\", \"0\") == \"1\"\n\n# Interval range: 0:15 ... 2:00 in 5s steps\nMIN_SECONDS = 15\nMAX_SECONDS = 120\nSTEP_SECONDS = 5\nNUM_STEPS = (MAX_SECONDS - MIN_SECONDS) // STEP_SECONDS\n\nBRIGHTNESS = int(os.getenv(\"DISPLAY_BRIGHTNESS\", \"5\"))    # 0-7\nBEEP_HZ = int(os.getenv(\"BEEP_HZ\", \"2300\"))\n\ndisplay = TM1637(CLK_PIN, DIO_PIN, brightness=BRIGHTNESS)\npot = analogio.AnalogIn(POT_PIN)\nbuzzer = pwmio.PWMOut(BUZZER_PIN, frequency=BEEP_HZ, duty_cycle=0,\n                      variable_frequency=True)\nkeys = keypad.Keys((BUTTON_PIN,), value_when_pressed=BUTTON_ACTIVE_HIGH,\n                   pull=True)\n\n\ndef read_selected_seconds():\n    \"\"\"Average a few ADC samples and quantise to the nearest 5s step.\"\"\"\n    total = 0\n    for _ in range(8):\n        total += pot.value\n    raw = total // 8\n    step = (raw * NUM_STEPS + 32768) // 65535      # round to nearest step\n    step = max(0, min(NUM_STEPS, step))\n    return MIN_SECONDS + step * STEP_SECONDS\n\n\ndef beep_done():\n    \"\"\"Three short beeps spanning ~1 second.\"\"\"\n    for i in range(3):\n        buzzer.frequency = BEEP_HZ\n        buzzer.duty_cycle = 32768          # 50% -\u003e tone on\n        time.sleep(0.15)\n        buzzer.duty_cycle = 0              # tone off\n        if i \u003c 2:\n            time.sleep(0.20)\n\n\nIDLE, RUNNING, PAUSED = range(3)\nstate = IDLE\nremaining = 0\nlast_tick = 0.0\ncolon = True\nlast_pause_blink = 0.0\npause_visible = True\n\nprint(\"Workout helper ready. Turn the pot to set a rest interval, press to start.\")\n\nwhile True:\n    event = keys.events.get()\n    pressed = bool(event and event.pressed)\n    now = time.monotonic()\n\n    if state == IDLE:\n        display.show_time(read_selected_seconds(), colon=True)\n        if pressed:\n            remaining = read_selected_seconds()\n            last_tick = now\n            colon = True\n            display.show_time(remaining, colon=colon)\n            state = RUNNING\n\n    elif state == RUNNING:\n        if now - last_tick \u003e= 1.0:\n            last_tick += 1.0\n            remaining -= 1\n            colon = not colon                      # blink the colon each second\n            if remaining \u003c= 0:\n                display.show_time(0, colon=True)\n                beep_done()\n                state = IDLE\n                continue\n            display.show_time(remaining, colon=colon)\n        if pressed:\n            display.show_time(remaining, colon=True)   # steady colon = paused\n            last_pause_blink = now\n            pause_visible = True\n            state = PAUSED\n\n    elif state == PAUSED:\n        if now - last_pause_blink \u003e= 0.5:\n            last_pause_blink = now\n            pause_visible = not pause_visible\n            if pause_visible:\n                display.show_time(remaining, colon=True)\n            else:\n                display.clear()\n        if pressed:\n            display.show_time(remaining, colon=True)\n            last_tick = now                            # resume cleanly\n            state = RUNNING\n\n    time.sleep(0.01)","metadata":{"language":"python","linenums":false,"filename":"code.py"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eA couple of small touches I'm fond of: the knob reading is the \u003cstrong\u003eaverage of 8 ADC samples\u003c/strong\u003e so the displayed time doesn't jitter while your hand hovers near a step boundary, and the colon \u003cstrong\u003eblinks once per second\u003c/strong\u003e while running — a subtle \"yes, it's actually counting\" heartbeat that costs nothing.\u003c/p\u003e\n\u003ch3\u003eOther Code Files (tm1637.py and settings.toml)\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# tm1637.py — minimal bit-banged driver for the TM1637 4-digit 7-segment display.\n#\n# The TM1637 uses a 2-wire protocol that *looks* like I2C but is not: there is\n# no device address and the data line is read on a fixed clock edge. So we\n# bit-bang it on two ordinary GPIO pins (CLK + DIO) with digitalio.\n#\n# Adapted for CircuitPython from the widely used MicroPython tm1637 driver\n# (Mike Causer, MIT). Push-pull outputs are used and the ACK bit is clocked but\n# ignored, which is reliable on the Grove TM1637 modules (they have pull-ups).\n\nimport time\nfrom digitalio import DigitalInOut, Direction\n\n# Command bytes\n_CMD_DATA = const(0x40)   # data command: automatic address increment\n_CMD_ADDR = const(0xC0)   # address command: 0xC0 | position (0-3)\n_CMD_DISP = const(0x80)   # display control: 0x80 | on-bit | brightness\n_DISP_ON = const(0x08)    # display-on bit (OR with brightness 0-7)\n\n_COLON = const(0x80)      # high bit of digit 1 drives the center colon\n\n# 7-segment patterns for 0-9 then A-F (gfedcba bit order)\n_SEGMENTS = (\n    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07,\n    0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71,\n)\n\n# Short delay so we stay under the TM1637's ~250 kHz clock limit. The call\n# overhead alone is usually enough, but the explicit sleep keeps it safe.\ndef _tick():\n    time.sleep(5e-6)\n\n\nclass TM1637:\n    def __init__(self, clk, dio, brightness=5):\n        self.clk = DigitalInOut(clk)\n        self.dio = DigitalInOut(dio)\n        self.clk.direction = Direction.OUTPUT\n        self.dio.direction = Direction.OUTPUT\n        self.clk.value = True\n        self.dio.value = True\n        self.brightness = max(0, min(7, brightness))\n        self.clear()\n\n    # --- low-level protocol ---------------------------------------------\n    def _start(self):\n        self.dio.value = False\n        _tick()\n        self.clk.value = False\n        _tick()\n\n    def _stop(self):\n        self.clk.value = False\n        _tick()\n        self.dio.value = False\n        _tick()\n        self.clk.value = True\n        _tick()\n        self.dio.value = True\n        _tick()\n\n    def _write_byte(self, b):\n        for _ in range(8):\n            self.clk.value = False\n            _tick()\n            self.dio.value = bool(b \u0026 1)\n            _tick()\n            self.clk.value = True\n            _tick()\n            b \u003e\u003e= 1\n        # 9th clock = ACK (we clock it but don't read it back)\n        self.clk.value = False\n        _tick()\n        self.clk.value = True\n        _tick()\n        self.clk.value = False\n        _tick()\n\n    # --- public API ------------------------------------------------------\n    def write(self, segments, pos=0):\n        \"\"\"Write raw segment bytes (a list of up to 4 ints) starting at pos.\"\"\"\n        self._start()\n        self._write_byte(_CMD_DATA)\n        self._stop()\n\n        self._start()\n        self._write_byte(_CMD_ADDR | (pos \u0026 0x03))\n        for seg in segments:\n            self._write_byte(seg)\n        self._stop()\n\n        self._start()\n        self._write_byte(_CMD_DISP | _DISP_ON | self.brightness)\n        self._stop()\n\n    def clear(self):\n        self.write([0, 0, 0, 0])\n\n    def show_time(self, total_seconds, colon=True):\n        \"\"\"Render seconds as MM:SS (zero-padded) across the four digits.\"\"\"\n        if total_seconds \u003c 0:\n            total_seconds = 0\n        mm = total_seconds // 60\n        ss = total_seconds % 60\n        segs = [\n            _SEGMENTS[(mm // 10) % 10],\n            _SEGMENTS[mm % 10] | (_COLON if colon else 0),\n            _SEGMENTS[(ss // 10) % 10],\n            _SEGMENTS[ss % 10],\n        ]\n        self.write(segs)","metadata":{"language":"python","linenums":false,"filename":"tm1637.py"}},{"element_type":"code","content":"# Workout helper runtime configuration.\n# Read at boot with os.getenv(\"KEY\", \"default\"); all keys are optional.\n\nDISPLAY_BRIGHTNESS = \"5\"     # 0 (dim) .. 7 (brightest)\nBEEP_HZ = \"2300\"             # buzzer tone frequency in Hz\n\n# Set to \"1\" only if your button drives the line HIGH when pressed\n# (e.g. a Grove button module). Leave at \"0\" for a button wired to GND.\nBUTTON_ACTIVE_HIGH = \"0\"","metadata":{"language":"python","linenums":false,"filename":"settings.toml"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eEnd Result\u003c/h2\u003e\n\u003cp\u003eIt's not winning any industrial-design awards, but it's earned a permanent spot in my gym bag. Dial in the rest, do your set, slap the button, repeat. And my phone stays in my pocket the entire time, which was the whole point.\u003c/p\u003e\n\u003cp\u003eIf you've got an orphaned dev board and a parts bin, CircuitPython is a great way to resurrect it as something single-purpose and genuinely useful. No cloud, no app, no account. Just a knob, a button, and three little beeps. Sometimes that's the entire spec, and it's a great one.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"image_url","content":"https://github.com/user-attachments/assets/06f12125-2139-4b00-bc92-6110ee64559f","metadata":{"alt":"Final image of the device on a workout bench","caption":""}}]