2FAC – Two Factor Alarm Clock

f2fac-first-sketch.jpg
Figure 1: First sketch

Description

A wireless and sturdy alarm clock that can be snoozed by putting your hand on top of it, but can be not turned off unless it is placed on its (bolted down?) docking station elsewhere in the house. The docking station also allows setting the clock and alarm time using a selector button/slide for the clock or alarm, and a rotating knobs for hours and minutes. The alarm clock itself only shows the current time. The time is automatically set from the DCF77 atom clock time signal 1].

Origin

A standard alarm clock doesn't work for all teenagers who need to get up early, nor for dads (like me) who want to be able to sleep in late.

Progress

Week 2 — CAD

This week, I played with 7-segment displays and designed the shape for the clock and base station. I did not deviate much from my initial sketch because the pressure to learn FreeCAD and Blender left no room for further creativity.

Week 4 — Embedded Programming

This week, we played with microcontrollers, and I developed strong feelings for the RP2040. My final project will have a base station and the clock itself. One of them will probably run a RP2040 with Internet access to keep time using NTP.

Week 6 — Electronics Design

This week, I learned about Charlieplexing. If I use 7-segment digits on the clock, it will need it to drive many LEDs (4 times 7 per digit and plus 2 for the dots). I am still undecided on whether to use OLED or super fancy e-ink instead.

Week 7 — Computer Controller Machining

Wood is very pretty and a good candidate for the base station or clock, or both.

Week 8 — Electronics Production

This week, I made a working 7-segment LED digit. I am still not sure if I am going this route but it is very cool!

Week 9 — Input Devices

I experimented with all the inputs for my project this week:

DCF77
to get the current time
Rotary Encoder
to change the wake up time
Step Response
to detect a hand on the clock for snoozing

Week 10 — Output Devices

I experimented with making NeoPixel components for the clock digits, and using a piezo for sound. The latter was disappointing, I have decided not to use it.

IMG_20260330_171448_DRO.jpg

Week 11 — Networking

More playing with NeoPixels; not applicable to my final project but I learned a lot about them.

Week 16 — System Integration

I finally started designing the entire project in this week.

clock-on-dock.png

2026-05-21 Milled PCBs for the clock/dock connectors

IMG_20260521_122233.jpg
IMG_20260522_151912.jpg
IMG_20260522_151935.jpg
IMG_20260522_151853.jpg

2026-05-23 Clock connector added to the main interior

I printed a stand that will be slotted into the housing which holds the clock connector to connect to the dock.

IMG_20260523_163801.jpg
IMG_20260523_163810.jpg
IMG_20260523_163839.jpg
IMG_20260523_163851.jpg

2026-05-25 Broke connector off the main board

While connecting the bottom part with the clock connector to the main board, I ripped off one of the connectors. I immediately re-milled the board and resoldered it.

IMG_20260525_163650.jpg
IMG_20260525_170457.jpg

While soldering, I printed some diffusers for the display to try out.

IMG_20260525_210326.jpg

2026-05-26 Audio not working on 5V

Last night, I found out the DFPlayer Mini does not work when on battery power. The 5V pin does not supply 5V when the battery is attached. I destroyed the RP2350 while testing the voltage with a multimeter.

I also spent some time with V-Carve and OpenSCAD to create toolpaths for milling the housing. I am planning on doing that next Friday.

2026-05-27 Switching to MAX98357A

I received the DFRobot MAX98357 I2S Amplifier Module - 2.5W in the mail today and immediately started testing it. Works great on 3V3!

The pin names on the board do not correspond to the I2S names in the MicroPython library. Here is the translation:

Pin MicroPython
BCLK sck
LRCLK ws
DIN sd

Also interesting: not all pin combinations are allowed. I got a ValueError: invalid ws (must be sck+1) error on the first try. Eventually I was able to produce a tone and play a WAV file I uploaded to the RP2350 using mpremote. After confirming it worked, I spent (a lot of) time making a footprint and adjusting the symbol in KiCad to update the main PCB.

During class, I decided to drop using the Shopbot for the housing because the multiplex I have is too uneven to get reliable pockets for the acrylic windows and PCB frame. I discussed it with Henk and will go with fully laser-cut acrylic.

2026-05-28 Milling another main board

I milled a new main board with the MAX98357 on it today but alas, the footprint I made yesterday is 2.54mm too wide. Next time, I will print the PCB on paper first when including self-made footprints to see if they actually fit. So, I corrected the footprint and milled yet another board.

IMG_20260528_131018.jpg
Figure 2: All 4 iterations (at this moment)

2026-05-28 Laser cutting the housing

Today was "LightBurn and burn-smell day"! It was more fun than I imagined. I did a cardboard test first, then the faces in wood (very pretty!), then the windows in clear acrylic, and finally the rest of the shell in white because the cardboard was not sturdy enough.

The first cardboard test failed. Somehow, DeepNest messed up the scale of the SVGs, so I dropped DeepNest and nested the parts myself in LightBurn.

IMG_20260529_132427.jpg
IMG_20260529_132722.jpg

Settings used:

  Power % min/max Speed
Cardboard 20/30 100
Wood 90/100 25
Acrylic 65/75 10
IMG_20260529_164026.jpg
IMG_20260529_164032.jpg

I mismeasured the thickness of the acrylic, so the intermediate slices are too thick. I can probably fix the interior to make it fit (new frame and base), so I'll look into that this weekend.

IMG_20260529_164216.jpg

2026-05-30 Acrylic housing and dock drafting

Today I cleaned up the acrylic slices for the body (removing the protective foil and clean them with soap to get rid of the smell left by laser cutting). Although the slices are too thick, they are already usable without reprinting the internals. I was afraid the white of these rings would not be as pretty as the cardboard, but it looks very nice.

IMG_20260530_104142.jpg
IMG_20260530_104152.jpg
IMG_20260530_104205.jpg

The rest of the day I spent on designing a dock. Extra points for being able to print it on my Prusa Mini (180x180x180mm).

screenshot-2026-05-31_10-15-52.png.jpg
dock-draft-top-20260530.png
dock-draft-bottom-20260530.png

2026-05-31 Printing the dock and changing the connector pin order

Printed the model I designed yesterday and updated the connector boards to have a more logical pin order, making it harder to flip power and ground when docking it the wrong way.

IMG_20260531_125213.jpg

2026-06-01 Creating new connectors, shopping for parts and DCF77 trouble

Got some threaded 5mm rod to bolt the housing slices together (unfortunately, the shop was out of bolts long enough). I also worked on the 3D models of the clock and dock base because I need USB access without taking it apart all the time. I milled the dock connectors to get a better pin layout and created new wires to match.

During the long 3D print, I looked at why the DCF77 does not work on RP2350 (it does on the RP2040!). I found some hints on an old blog post (hurrah for the wayback machine!). But, after adding a (real) pull up resistor and a capacitor as in the example, still nothing happened. I tried switching to 5V, nothing. More pull up by enabling the onboard pull too, nothing. I'm giving up for now, printing a new clock base with USB access should allow me to set the time (many with Web Serial page to do so?).

Annoying, low-progress day..

2026-06-02 Finishing the hardware and starting video editing

It has been a long and busy day. I printed (and reprinted) the dock base for the new pin layout and connector position. In the second print, I made the footprint of the dock slightly narrower to avoid pins bridging each other when connecting (I probably need diodes on the V and GND pins).

I sawed the threaded rod to match the clock thickness and filmed putting the clock together in a couple of takes. Kdenlive is a bit harder to use than I imagined, but I can probably make a short video.

For music I found Ambient Cinematic by AtlasAudio on pixabay.

The USB worked awesomely but broke after putting the clock together for the 5th (?) time…

2026-06-03 Software and step response

I spent a lot of time writing the firmware today. All the logic is in place, but when it's packaged up and the sound is playing, the step response is very unreliable. It is unworkable because it is impossible to detect a hand on the clock. Need sleep, will fix tomorrow.

2026-06-04 Fixed it with aluminum foil and made a movie

I fixed the step response by extending the pads to the outside of the housing using aluminum foil. A hand on the clock is now a lot easier to detect!

Plenty more tweak of the firmware and I downloaded some alarm sounds from Pixabay:

In the evening (low light), I filmed my son using the alarm clock, and edited it into the presentation video.

Technical overview

Bill of Materials

The following parts are used to build this project:

Amount Description Price Sum Function
1 SEEED STUDIO XIAO 5.75 5.75 Main / RP2350
1 DCF77 2.69 2.69 Main / Radio Clock Receiver
1 DFR0954 7.02 7.02 Main / Audio source
1 Speaker 1.50 1.50 Main / Audio output
1 RES 0 OHM JUMPER 1/4W 1206 0.10 0.10 Main / Electrical
1 RES 1K OHM 1% 1/4W 1206 0.10 0.10 Main / Electrical
1 Makerfocus 3.7V 3000mAh Lithium 15.00 15.00 Main / Battery
20 SMD Socket headers 0.75 15.00 Main / Connectors
1 SMD Pin headers 3.33 3.33 Main / Connectors
1 USB-C extension 3.65 3.65 Main / Connectors
0.1 ColorFabb PLA Highspeed PRO Jet Black 21.55 2.15 Main / Frame
30 ADDRESS LED SERIAL RGB 0.25 7.50 Display / Light
4 CAP CER 1UF 50V X7R 1206 0.25 1.00 Display / Electrical
0.05 ColorFabb XT Clear 21.55 1.08 Display / Diffusion
1 3mm Clear Acrylic 5.00 5.00 Clock / Housing
1 4mm White Acrylic 20.00 20.00 Clock / Housing
1 KY-040 Rotary Encoder 2.44 2.44 Dock / Time selection
1 CONN SPRING MOD MALE 6POS SMD 1.95 1.95 Dock / Connector dock
1 CONN SPRING MOD FEMALE 6POS SMD 1.92 1.92 Dock / Connector clock
4 Magnets 0.70 2.80 Dock / Placement
1 USB-C PD Fast Charging 1.67 1.67 Dock / Power
1 PCB FR-1 stock 20.00 20.00 Main, Clock and Dock
  Total ex. VAT   €121.65  

The SBOM (Software Bill of Materials) is not that interesting. I used:

Clock

The clock contains all the "live" components: display time, sound alarm, and run on battery.

Main board

The main board has sockets for:

  • Microcontroller (RP2350)
  • DCF77 (clock receiver)
  • MAX98357A (audio amplifier)

and connectors for:

  • Display
  • Power (5V)
  • Speaker
  • Rotary Encoder
  • Snooze detection (Step Response)
screenshot-2026-06-05_12-05-57.png.jpg
Figure 3: Schematics
screenshot-2026-06-05_12-13-42.png.jpg
Figure 4: PCB

Sources:

Time keeping

To keep the clock time accurate the DCF77 transmission will be used. Time will be sync on power-on, and daily readings should be enough to keep the clock in sync for an alarm clock.

Unfortunately, I could not get it to work on the XIAO RP2350. Weirdly, it works fine using a XIAO RP2040, but that XIAO does not support batteries out of the box.

Display

The digits will be NeoPixel modules based on the design for week 10. I wanted to have the pixels shine through wood, but decided that would be an awful time drain, and making a box that looks interesting is quite hard. The PCBs are quite pretty, so I'll make them visible instead.

The improved design is a 35x65mm board with a 25x45mm digit. It also has one 1µF capacitor between the VDD (5V) and VSS (ground) as instructed in the datasheet (100nF per LED). The "dots" board will not have any capacitors because (I hope) the capacitors on the digit boards will be more than enough.

screenshot-2026-06-05_12-24-51.png.jpg
Figure 5: Schematics
screenshot-2026-06-05_12-27-28.png.jpg
Figure 6: PCB

Sources:

Segment mask / diffusion

For diffusing the light of the NeoPixels, I printed the model below using ColorFabb XT Clear (a PET-like filament).

led-diffuser.png

The resulting diffusion is pretty bad, but I do not have time to experiment with this.

Wake up Sound

For sound, I first decided to use the DFPlayer Mini. It can play MP3 files, has a build in amplifier, and supports TF cards (aka mini SD) for storage. For output, I got a 57mm, 8Ω, 0.5W speaker. Unfortunately, running it on battery power failed because the player needs 5V to operate and attaching the XIAO RP2350 to a battery makes the 5V pin useless.

So, I switched to the much simpler MAX98357.

Snooze

The alarm can be snoozed by triggering a step response change using your hand via two conductive pads hidden in the top of the clock.

IMG_20260513_143315.jpg
Figure 7: Basic test

Actually, when I was putting the clock together, I found out it is very hard to detect a change when the alarm sound is playing because that triggers a lot of noise in the step-response setup. I fixed this (MacGyvered it) by enlarging the pads with aluminum foil, which sticks out of the top of the clock. There was not enough time to change the housing to fix this properly.

Power

The XIAO RP2350 has onboard battery management (XIAO RP2040 does not) and can be connected to a lithium battery and charge it. When USB is disconnected, it will automatically switch to battery power. The battery level can be read from GPIO29 (see also pin map). The battery must have power protection (PCM) to avoid over(dis)charging.

The V5/VUSB/VBUS pin can be used to power the XIAO without using the USB port. To protect the VBUS it is smart to add a diode but that's already on the XIAO circuit (see also XIAO with Lipo Battery Charging Circuit).

The 5V power for charging will from the docking station.

Connect/disconnect stability

I used the code below to test power breaks. This code was installed as main.py, so it starts up after a reset.

import machine, time, ws2812

machine.Pin(23, machine.Pin.OUT).value(1)
led = ws2812.WS2812(22, 1)

led.pixels_fill((128, 0, 0))
led.pixels_show()

time.sleep_ms(2000)
led.pixels_fill((0, 0, 128))
led.pixels_show()

For testing, I connected 5V through a PD Board (B0DPHHK5ZV) with a lithium battery (Makerfocus 3.7V 3000mAh Lithium) attached. Running the code above, it was easy to detect whether the RP2350 resets because the LED will turn red for 2 seconds and then back to blue.

IMG_20260507_144258_DRO.jpg
Figure 8: XIAO RP2350 on 5V and battery

Findings:

  • (Dis)connecting USB power causes no breaks.
  • (Dis)connecting 5V power causes no breaks.
  • Disconnecting the battery on USB/5V power only breaks power on the first disconnect.
Detecting on USB power

The GPIO29 pin can be used to gauge the battery power state (don't forget to set GPIO19 to high). Below is the code I used to see if it is possible to detect whether the board is powered by battery or from an external source.

import machine, time

# turn on battery power reading
machine.Pin(19, machine.Pin.OUT).value(1)

# pin to read battery power values
adc = machine.ADC(29)

n = 0
while True:
    v = adc.read_u16()
    print(n, v)

    time.sleep_ms(1000)
    n += 1

Unfortunately, it seems impossible to reliably detect, with the code above, to detect whether the external power source is attached using the code above. I was expecting to see a big power drop when the external source is detached but the value just keeps fluctuating as it did before.

Interestingly, the code example in the Seeed OSHW repository does not return realistic voltage values.

from machine import Pin, ADC
import time

# Function to initialize the GPIO pin for enabling battery voltage reading
def init_gpio():
    enable_pin = Pin(19, Pin.OUT)
    enable_pin.value(1)  # Set the pin to high to enable battery voltage reading

def main():
    print("ADC Battery Example - GPIO29 (A3)")

    init_gpio()  # Initialize the enable pin
    adc = ADC(Pin(29))  # Initialize the ADC on GPIO29

    conversion_factor = 3.3 / (1 << 12)  # Conversion factor for 12-bit ADC and 3.3V reference

    while True:
        result = adc.read_u16()  # Read the ADC value
        voltage = result * conversion_factor * 2  # Calculate the voltage, considering the voltage divider (factor of 2)
        print("Raw value: 0x{:03x}, voltage: {:.2f} V".format(result, voltage))
        time.sleep(0.5)  # Delay for 500 milliseconds

if __name__ == '__main__':
    main()

The above outputs:

ADC Battery Example - GPIO29 (A3)
Raw value: 0x8b98, voltage: 57.58 V
Raw value: 0x8ab8, voltage: 57.22 V
Raw value: 0x8aa8, voltage: 57.20 V
...

I created a bug report on GitHub and the friendly people of SEEED fixed it a week later!

Housing

The housing of the clock consists of laser-cut slices of acrylic and triplex held together with a 5mm bolt and lock nuts. Inside, the PCBs are mounted to a 3D printed-frame. The bottom of the clock is also 3D printed; it holds the frame and a PCB for the connector to dock.

All prints and cuts are derived from a single all-in-one OpenSCAD file.

all-in-one.png
Figure 9: All-in-one
clock-frame.png
Figure 10: 3D model for frame
speaker-frame.png
Figure 11: 3D model for speaker
clock-base.png
Figure 12: 3D model for clock base
sliced-housing.png
Figure 13: 3mm cuts for clock housing
dock.png
Figure 14: 3D model for dock
dock-side.png
Figure 15: 4mm cut for dock sides

Here are the STL and SVG files:

Firmware

I used MicroPython to write the software. It is very easy to use and allows REPL (Read-Evaluate-Print Loop) development, which I prefer for experimentation. Apart from the stock MicroPython library, I only used the steptime.py library written by Neil Gershenfeld.

The code is split into modules:

  • display.py

    Allows setting the NeoPixel digits and dots on the display in different colors.

  • snooze.py

    Runs a periodic timer to probe the value of the snooze step-response detection (using steptime.py) and triggers a callback when the threshold is exceeded. This polling can be enabled/disabled to preserve power.

  • rotary_encoder.py

    Uses an IRQ trigger on rising and falling pin values for the rotary encoder data pin. These values are translated to key pressed / released and (counter) clockwise rotation callbacks.

  • sound.py

    Drives the MAX98357 amplifier using I2S to play sounds in the background in a loop, and allows stopping them.

  • events.py

    A simple wrapper around a Python list to push and pop events. The pop function has the ability to timeout and thus return None when nothing happened. The callbacks in the snooze and rotary encoder modules push events onto this list.

  • twofac.py

    The main logic is implemented here. It's an event loop which supports the following modes:

    • sleep

      The alarm is set and the display shows the current time with the dots blinking every second in a soft color. When wake-up time matches the current time, move to alarm mode.

    • alarm

      The alarm is sounding and the display shows the current time in a bright color. When the snooze is triggered and the maximum amount of snoozes is not yet reached, turn off the alarm sound and move to the snooze mode. When the rotary encoder switch is pressed, turn off the alarm sound and move to dock mode.

    • snooze

      The time is displayed in a soft color, but the dots are blinking brightly. When the snooze time is up, move to the alarm mode. When the rotary encoder switch is pressed, move to dock mode.

    • dock

      Displays the current time in a soft color without blinking the dots. When the rotary encoder switch is pressed, move to the wake up hours mode.

    • wake up hours

      The wake up time is displayed in a soft color with the hours in the bright color. Rotating the encoder changes the hours, pressing the switc,h moves the mode to wake up minutes.

    • wake up minutes

      The minutes are highlighted and can be adjusted using the rotary encoder. Pressing the encoder, moves the mode to sleep.

    Pressing the rotary encoder switch for more than 5 seconds resets the microcontroller. When it starts up, the current time must be set in a similar fashion to the wake-up time, but using another bright color (green). This should be replaced by setting the time using DCF77.

  • main.py

    Load twofac.py and start the event loop.

Apart from the code, the following sounds are also part of the firmware:

Docking station

The dock is a simple 3D-printed box with acrylic screwed onto the sides to guide the clock into the right position. I wanted it to be bigger, but I printed it at home on my Prusa Mini, so I was constrained by the 18x18cm print bed.

Connector

The connector passes power and exposes the rotary encoder pin to the clock. I used the following parts:

Parts:

I created PCBs for the clock and dock side of this:

screenshot-2026-06-09_16-43-12.png.jpg
Figure 16: Clock connector PCB
screenshot-2026-06-09_16-44-09.png.jpg
Figure 17: Dock connector PCB

Power

Power is provided with a PD Board (B0DPHHK5ZV) to use a USB-C cable to provide 5V to the clock for charging the battery. It is embedded in the dock and provides power through the connector. At first, I was afraid to mount the clock on the dock with the USB cable connected for fear of smoke, but it works very well!

Encoder

A commonly used rotary encoder registers rotation and clicks from the dock to the clock.

Evaluation

Having spent much time on this project, I am pretty happy with the result, but there is still a lot of room for improvement. I finished it a couple of days before the deadline but have no energy left to work on these issues. Here are a couple of things I would like to improve:

The DCF77 does not work on the RP2350. I spent hours trying to get it to work but nothing success. I may try a XIAO ESP32 I have laying around, which also supports battery charging, to see if that works, or use an RP2040 and add some charging circuitry. This is really bugging me.

The step-response approach I envisioned for the snooze function is now "MacGyvered" using aluminum foil. Experimenting with step response in the input devices week already gave me an uncertain feeling about this technique, and having it in a box packed with electronics confirmed that step response can be quite challenging.

Making something pretty requires more care in material selection than I gave this project. The PCB copper is on full display because I think it looks nice, but there is a lot of color variation and fingerprints on it. Also, the PCBs and the rest of the interior are stained by superglue fumes; it is very ugly, and it is still outputting this white coating after several days.

The NeoPixels vary in color and brightness because they come from three different batches. They are also too bright, so I should have experimented with adding a resistor to the power line. I did introduce a 0Ω resistor in the schematic and the PCB to do that, but did not try changing it because it works now and I don't want to break it. Also, the diffusion parts I printed don't work very well. It is hard to see what time it is in a bright room. I need more experimentation on that, too.

USB access to the microcontroller is very convenient for debugging but at least as convenient is the ability to reset and put it in boot mode (to allow flashing it with a new UF2-file) without taking it about would be really nice too. In a next iteration that would be very nice.

The dock is too lightweight and should be mounted on something like wood to make it more stable when operating the knob.

Reflection

Fab Academy was awesome! It's been a wonderful experience in which I met amazing people, and learned an incredible amount of new skills. A big thank you hug to everybody involved!

Footnotes:

1

Currently not working due to issue with DCF77 module and the RP2350

Copyright © 2026 Remco van 't Veer

Licensed under a
CC BY 4.0

Build using
GNU Emacs, Org Mode and GNU Guix

Source code hosted at
gitlab.fabcloud.org