My daughter is at that age where she needs a nightlight but also needs to know what time it is — and more importantly, whether it's okay to come wake me up yet. A commercial clock-nightlight combo exists, but none of them felt right. So naturally, as a software engineer, I decided to build one from scratch.
The initial attempt used PlatformIO with C/C++. PlatformIO is a great toolchain, but after a few weeks I found myself fighting the language more than building features, and the project stalled.
Then José Simões showed up in the Unexpected Maker Discord and mentioned.NET nanoFramework. As someone who spends his day job writing C# on the.NET Core stack, this was a revelation. I could write real C# — with dependency injection, generics, interfaces, and all the patterns I use every day — and run it on a microcontroller. The project restarted immediately.
The result is Emily.Clock: a networked nightlight clock that shows the time and date, glows with 47 RGB LEDs in the color of your choice, has dedicated sun and moon indicator LEDs that automatically switch at bedtime and wake-up time, and is fully configurable over WiFi via a REST API and web UI. The entire thing fits inside a custom 3D-printed enclosure that looks like it belongs on a nightstand.
Features- Nightlight with 7 color options (red, orange, yellow, green, blue, indigo, violet) and 5 brightness levels
- Sun and moon indicator LEDs that automatically switch based on configurable bedtime and wake time
- WiFi time sync with POSIX timezone string support for accurate local time
- Dual WiFi mode: connects as a client to your existing network, or hosts its own access point for initial setup
- Web UI for wireless network configuration
- REST API for device info, network status, and configuration management
- Three physical buttons for brightness cycling, color cycling, and alarm toggling
- Custom 3D-printed enclosure with Fusion 360 and STEP files included
- Clean software architecture designed so someone with different hardware can swap in their own implementations
The project is built around the LILYGO TTGO T4 V1.3, which is a convenient all-in-one ESP32 module with a built-in 2.4-inch ILI9341 TFT display and three tactile buttons onboard. This board saved a lot of wiring and keeps the build compact.
The LED strip is wired to GPIO 19 and contains 47 total LEDs. Index 0 is the moon LED (blue), index 1 is the sun LED (orange), and indices 2–46 form the main nightlight bar. For pin details and SPI configuration, see the hardware implementations in Emily.Clock.App/Hardware/.
The 3D-Printed EnclosureThe case was designed in Fusion 360 and printed in white PLA. The design goal was a clean, minimal look with a translucent light bar across the top that diffuses the LED glow evenly.
The enclosure consists of the following printed parts:
- Shell — the main body
- Front Panel — houses the display cutout and button holes
- Back Panel — access panel
- Base — the bottom plate
- Buttons (x3) — tactile button caps
- Light Bars (x5) — the translucent top diffuser sections
- Light Bar Adapters (Left / Right) — connect the light bars to the shell
- Light Tube Caps (x2) — end caps for the light bar assembly
- Display Clip — secures the display module
- Debug Port Cover — covers the USB port access opening
- Speaker Grill Cover — for the future audio upgrade
All Fusion 360, 3MF, and individual STEP files are included in the models/ directory of the repository. You can adapt the case to your own hardware by editing the Fusion 360 source.
Software ArchitectureOne of the main goals going into this project was to write the application in a way that someone else, with different hardware, could reuse the majority of the code. That meant keeping all business logic in a hardware-agnostic core library, with hardware-specific implementations isolated in a separate project.
The solution has three projects:
Emily.Clock - Hardware-agnostic core — all business logicEmily.Clock.App - Device-specific implementations (pin config, display driver, LED chipset)Emily.Clock.UnitTests - Unit tests for the core library
Dependency Injection on a Microcontroller
Using dependency injection on embedded hardware is unusual, but.NET nanoFramework makes it possible via the CCSWE.nanoFramework.Hosting library. The entire entry point is just a few lines:
var builder = DeviceHost.CreateDefaultBuilder()
.ConfigureHardware() // registers hardware-specific singletons
.AddCore(); // registers all core services
using var host = builder.Build();
host.Run();ConfigureHardware() registers the five hardware interface implementations (IButtonManager, IDisplayManager, ILedManager, etc.) as singletons. AddCore() registers everything else: services, windows, configuration, the web server, and the mediator. Someone who wants to run this on different hardware only needs to re-implement those five hardware interfaces and swap out ConfigureHardware().
Three-Phase Initialization
Startup executes three IDeviceInitializer singletons in sequence:
1. DeviceInitialization — initializes the display, buttons, file storage, and LEDs. If any button is held at boot, navigates to the Reset to Defaults window.
2. NetworkInitialization — connects to WiFi. If the connection fails, shows the Network Failure window. If in AP mode, shows the Configuration window and starts the web server.
3. ApplicationInitialization — starts the local time provider, initializes the nightlight, and navigates to the Clock window.
Mediator for Decoupled Events
The application uses the mediator pattern (CCSWE.nanoFramework.Mediator) so that hardware events like button presses don't need to know anything about the UI. The Clock window subscribes to ButtonEvent, TimeChangedEvent, and DateChangedEvent on start, and unsubscribes on stop:
protected override void OnStart()
{
_mediator.Subscribe(typeof(ButtonEvent), this);
_mediator.Subscribe(typeof(DateChangedEvent), this);
_mediator.Subscribe(typeof(TimeChangedEvent), this);
// ... draw initial state
}
public void HandleEvent(IMediatorEvent mediatorEvent)
{
switch (mediatorEvent)
{
case ButtonEvent buttonEvent:
// handle brightness/color/alarm button presses
break;
case TimeChangedEvent timeChangedEvent:
DrawTime(GetBitmap(), timeChangedEvent.Time);
break;
case DateChangedEvent dateChangedEvent:
DrawDate(GetBitmap(), dateChangedEvent.Date, true);
break;
}
}Typed Configuration
Configuration uses typed classes bound to named sections, with optional validators:
services
.AddConfigurationManager()
.BindConfiguration(DateTimeConfiguration.Section, new DateTimeConfiguration(), new DateTimeConfigurationValidator())
.BindConfiguration(NightLightConfiguration.Section, new NightLightConfiguration())
.BindConfiguration(WirelessClientConfiguration.Section, new WirelessClientConfiguration());The four configuration sections are datetime, nightlight, wireless-client, and wireless-access-point. All settings are persisted to the SD card and survive reboots.
WiFi and Web Configuration
The clock supports two WiFi modes:
Client mode (normal operation): connects to your home network on boot, syncs time from the internet using a POSIX timezone string, and makes the REST API available on port 80.
Access Point mode (initial setup): the device broadcasts its own Emily.Clock network at 192.168.4.1. Connect to it and open a browser to enter your home network credentials via the built-in web UI. Once saved, reboot the device to switch to client mode.
The REST API exposes endpoints for device management and configuration:
Method Endpoint Description
GET /api/device Device info (free memory, serial number, uptime)
GET /api/device/network Network interface info
GET /api/device/ping Health check
POST /api/device/reboot Reboot the device
GET /api/configuration List configuration sections
GET /api/configuration/{section} Get configuration for a section
POST /api/configuration/{section} Save configuration for a sectionThe Clock in ActionThe three buttons on the right edge of the front panel control the nightlight:
Button 1 (short press): cycle brightness — 0% -> 25% -> 50% -> 75% -> 100% -> offButton 1 (hold): toggle the nightlight on or off
Button 2: cycle color — Red -> Orange -> Yellow -> Green -> Blue -> Indigo -> Violet
Button 3: toggle the alarm on or off (full alarm functionality coming soon)
The sun and moon LEDs on the left side of the front panel switch automatically. During the configured wake hours a warm orange sun LED glows; from bedtime until wake time the moon LED glows blue. At a glance, my daughter knows whether it's okay to get up.
Development JourneyThe project started in late 2022 as a PlatformIO project in C++. The hardware worked, but progress stalled quickly — debugging embedded C++ without the language ergonomics and tooling I was used to from the.NET world just wasn't fun.
After discovering.NET nanoFramework in early 2023, the restart was fast. Having access to real dependency injection, proper interfaces, and a test framework meant the software could be structured the same way I'd structure a production.NET application. The unit test suite covers the core library and runs in the nanoFramework test runner, catching regressions without needing hardware.
The 3D-printed case went through many iterations. The light bar design in particular took several prints to get the diffusion right — too thick and the individual LED hotspots show through, too thin and the light doesn't spread far enough. The final five-piece light bar assembly hits the balance nicely.
The clock has been running in Emily's room since October 2023. It has survived hundreds of bedtimes, zero reboots from software faults, and countless requests to change it to violet.
What's Next
The hardware for the next round of features is already installed inside the enclosure:
- Alarm with audio — the MAX98357A amplifier and speaker are wired and waiting; the firmware stub exists but the scheduling and audio playback still need to be written
- Battery-backed RTC — the DS3231SN module is installed; once the firmware support is added, the clock will keep accurate time even when WiFi is temporarily unavailable
- Expanded web interface — the current web UI only handles wireless network setup; the plan is to add nightlight, alarm, and time settings as well
- Android companion app — a placeholder exists in the repository; no timeline yet






Comments