Explore the homelab

Personal projects & experiments

ElixirNervesReactReact NativeESP32IoTHobby

Robine Wake – Sound and Light Alarm Clock

A local-first Raspberry Pi alarm clock combining gradual sound, Philips Hue lighting, and a physical stop button.

Context:Hobby
Period:2026

Locally controlled alarm · React and React Native interfaces · Physical ESP32 controller

Robine Wake: bedroom speakers on a HiFiBerry-equipped Raspberry Pi, talking over Wi-Fi to a small ESP32 stop button

An everyday device

I wanted an alarm clock that could wake me gradually without depending on a phone or a cloud service. The starting point was a product question: how do you combine a gentle wake-up with a reliable fallback, while keeping the controls simple?

A loud alarm can make waking up unpleasant. A gentle alarm can go unnoticed. I wanted a gradual transition, with a stronger signal only if the first one went unanswered. So I built Robine Wake, a local-first alarm clock combining sound, light and a physical stop button — a personal project exploring the intersection of software, audio and hardware.

The engineering lesson was straightforward: reliability does not come from one feature. It comes from explicit priorities, observable state and safeguards that remain useful when another component fails.

The challenge: a gradual wake-up, an explicit stop

Playing a sound at the scheduled time solves only part of the problem. Reaching the end of a volume curve does not confirm that anyone has responded to the alarm.

A separate physical button makes that response explicit. The phone remains a configuration interface; it does not need to stay connected for the wake-up sequence to work.

I needed two things that seem contradictory:

  • a gradual transition that did not make the day begin with an attack;
  • a hard boundary that prevented the gentle wake-up from becoming an optional suggestion.

That distinction became the product's core rule: gentle first, uncompromising only when necessary.

A gentle path with a real fallback

Robine Wake runs on a Raspberry Pi 3 with Nerves, an Elixir platform for embedded systems. A HiFiBerry board owns the bedroom speakers. The clock, alarm schedule and current wake session all live on the device. Losing the Internet does not cancel the morning.

The alarm starts before the requested wake time. A chosen melodic track appears at a low volume and rises progressively while Philips Hue lights create a sunrise in the room. At the target time, sound and light have reached their configured level.

Then comes the important part: the system does not assume that "the curve is over" means "the alarm has been acknowledged." After a plateau, an unanswered alarm switches to a second, deliberately more salient sound. It is not simply the same track turned up forever; it changes meaning.

pre-wake time     requested time          no response
      |                  |                     |
gentle sound ─── progressive rise ─── plateau ─── strong fallback
soft light   ─────── Hue sunrise ────────┘

The volume has a separate safety ceiling, snooze is limited, and an active session is persisted so that a restart does not conveniently erase it. If an accessory fails, it must degrade without silencing the local alarm. These rules matter more to me than adding another smart feature.

The project also experiments with an infrared camera for private, on-device bed-presence detection. It can eventually confirm that the bed has remained empty, but uncertain vision is never allowed to declare victory. When the system does not know, the alarm continues.

The ESP32 moves the decision out of reach

The strongest safeguard is physical.

I built a separate stop control around a Heltec WiFi Kit 32 V1: an ESP32, a small OLED screen and a real button. It displays the time, Wi-Fi quality and whether the alarm is active. When Robine Wake rings, the screen lights up and shows Robine running with a few hearts. Pressing the button sends a versioned dismissal command over Wi-Fi directly to the Raspberry Pi's local network — there is no wired link between the two.

The control can be placed away from the bed, so stopping the alarm requires getting up. The interaction stays simple: reach the control, press one button and receive clear confirmation.

There is no cloud round-trip in the middle. The ESP32 talks to robine-wake.local, and the screen distinguishes between a stopped alarm, an already quiet room and a network failure. A physical press during an active alarm is handled immediately; it does not ask a barely awake person to perform a long-press ritual correctly.

This is the difference between friction and a guardrail. Friction is annoying every day. A guardrail stays almost invisible when the gentle path works and intervenes only when it does not.

AirPlay, but never at the expense of the alarm

A dedicated box in the bedroom should be useful outside the few minutes when it wakes me. Robine Wake therefore also appears on my local network as Robine Wake Bedroom, a classic AirPlay audio receiver powered by Shairport Sync.

I can send music or a podcast from an iPhone or a Mac and use the HiFiBerry setup as a normal bedroom speaker. But this convenience has an explicit priority rule: the alarm always owns the audio output.

ALSA access is exclusive. When an alarm starts, the Elixir application suspends the AirPlay receiver before opening the sound device. The current stream may disconnect — and that is intentional. Once the wake session ends, AirPlay starts and advertises itself again.

This small detail captures the whole design philosophy. "Smart" functionality is welcome only if it cannot weaken the primary promise. A malformed stream, unavailable Wi-Fi or crashed AirPlay process must never delay, attenuate or replace the alarm.

Philips Hue for both ends of the night

The Hue integration is not limited to sunrise. In the evening, Robine Wake offers a guided 4/6 breathing session: four seconds of inhaling, six seconds of exhaling.

The selected Hue room or zone grows brighter during the inhale and fades during the exhale, changing colour with the phase. At the same time, the Raspberry Pi can generate a continuous soundscape of soft noise, light rain and a stable drone whose volume follows the same rhythm.

The phone starts or stops the session, but it is not the clock. The cadence belongs to the Raspberry Pi and uses monotonic deadlines, so it continues if the app is locked or disconnected and does not accumulate a little more drift on every breath. Robine Wake also measures local Hue latency to send transitions slightly ahead of their intended boundary.

At the end, the previous light state is restored. If an alarm begins during the session, the breathing ritual stops and gives back both Hue and the speakers. Again, the hierarchy is explicit.

I use the familiar term "cardiac coherence" to describe the 4/6 ritual, but the product makes no therapeutic claim. It is a light and sound pacing guide that helps me slow down before sleep, not a medical device.

From firmware to interfaces

Elixir was a natural fit because an alarm clock is mostly a collection of concurrent states and failures disguised as a simple object. The scheduler, audio output, Hue bridge, AirPlay receiver and active wake session should be isolated from one another. OTP supervision means a failed accessory does not have to take the clock down with it.

Nerves turns that model into an appliance. I control the complete firmware rather than maintaining a general-purpose Linux installation by hand, while retaining the BEAM's supervision model on the Raspberry Pi.

The web interface uses React, and the mobile application uses React Native with Expo. Both present explicit device state: the next alarm, the current phase, device health and Hue state. Scheduling and audio control stay on the Raspberry Pi, independently of the interface.

The two ecosystems meet through a small, versioned local protocol connecting these interfaces to the Raspberry Pi and the Wi-Fi-connected ESP32 controller. The interface can disappear, reconnect or be replaced without becoming the owner of the alarm clock.

Building it with ChatGPT

I built Robine Wake with ChatGPT as a programming partner. That sentence can sound as though I typed "make me an alarm clock" and waited for a finished product. The actual work was much more interesting.

I brought the product goals, the constraints and the final decisions. ChatGPT helped me move quickly between layers: Elixir state machines, Nerves firmware, React and React Native interfaces, ALSA, Hue's local API and ESP32 code. More importantly, the conversation forced implicit assumptions to become explicit scenarios.

What happens if the Raspberry Pi restarts while the alarm is sounding? If Hue is unavailable? If AirPlay already owns the speakers? If the stop command is sent twice? If wall-clock time jumps while a snooze timer is running?

Those questions became persisted state, idempotent commands, monotonic timers, safety ceilings and tests. AI did not remove engineering responsibility; it increased the surface area I could explore, and made it practical to build across stacks that would otherwise have demanded a much longer detour.

The hardware still has the final word. A generated test cannot tell me how loud a speaker is at the pillow, how the OLED looks at night or how a Hue transition feels in the room. Those decisions require calibration, observation and real mornings.

An everyday device with clear priorities

Robine Wake brings several functions into one device, with a clear order of priorities: the alarm comes first, and every accessory must be able to fail without preventing it from ringing.

Its success criterion is concrete: start gently, then use a stronger signal only when the first one goes unanswered. Be useful as an AirPlay speaker during the day. Offer guided light and sound in the evening. Keep every essential decision local. And never confuse an elegant animation with confirmation that the wake-up sequence is complete.

Building an alarm clock turned into an exercise in system design: coordinating independent components, handling failures and testing the complete experience on real hardware. Hardware validation is ongoing — sound levels, screen readability at night, and button behaviour in the room.

The source code and hardware files live in the Robine Wake repository. It is still private while I finish validating the hardware, but I intend to document and open the reusable parts progressively.