Project · September 2026 · in progress

Curtain drive: sensorless end stops on a single-sided board

A motorised curtain drive built from a hobby servo and an ESP32-C3. It finds the end of travel from the motor current alone. The PCB is routed on one copper layer with no vias, for a fiber-laser build.

KiCadPCB designPCB automationFreeRoutingESP32Home AssistantBlender3D printingPython

Personal project. A small box above the window pulls both curtain panels open and closed with a GT2 belt along the ceiling track. It is controlled from Home Assistant. The motor is one of my 20 kg·cm hobby servos, converted to continuous rotation and driven as a geared DC motor. There are no limit switches. The board works out that the curtain has reached the end of the track by watching the motor current, the same idea as sensorless homing on a 3D printer.

Render of the off-white drive box with a black lid mounted at the end of a ceiling curtain track: status LED lit, belt pulley underneath, power lead running down the wall
The drive at the end of the track. The servo shaft comes down through the floor of the box to a GT2 pulley, and the belt runs along the track. This is a design render; the drive has not been built yet.

How it will look

A living-room window with a ceiling-hung track and two linen panels that part from the centre. One motor moves both panels: each panel’s lead carrier clamps to the opposite run of the same belt loop. The box, board and servo are the real design geometry. The room, track, bracket and carriers are illustrative.

Render of a bright living-room window with linen curtains partly open on a ceiling track, and the drive box at the left end of the track
Installed, curtains partly open. The drive sits at the left end of the track, with the power lead running down the wall.
Looping animation of both curtain panels closing across the window from the centre, pausing, and opening again as the room darkens and brightens
Closing and opening. The belt pulls the two panels in opposite directions. The animation runs about 14× faster than the real drive would. Download GIF.
Looping close-up animation of the pulley turning and the belt pulling a curtain carrier along the track until it stops against the end stop, with the status LED going out
The end stop. The pulley drives the belt until the lead carrier hits the stop, the motor stalls, and the current spike tells the firmware it has arrived. Download GIF.

How it finds the end stop

A TI DRV8876 H-bridge drives the motor. Its IPROPI pin mirrors the load current at 1000 µA per amp, and a 1 kΩ resistor turns that into 1 V per amp at an ESP32-C3 ADC pin. The firmware (ESPHome) runs a 20 ms loop:

That stall is the end stop. It re-homes the position to 0 % or 100 %, so the time-based estimate resyncs at every end. The hardware current limit (ITRIP, set by a VREF divider) is 2.7 A. The software threshold sits below it on purpose, so the firmware stops the motor before the driver’s own chopping ever engages. The threshold is still a placeholder at 1.8 A. It has to be set from logged running and stalled current once the board exists.

A 12 V barrel jack powers the motor. An AP63203 buck makes 3.3 V from 12 V or from USB-C, which is also the ESP32-C3’s native USB for flashing. A second ADC input watches the 12 V rail.

One copper layer, no vias

I mill or laser my own boards. This one is for an xTool F1 Ultra fiber laser (following sphawes’ fiber-laser PCB process), which etches one side only. So the whole board is on F.Cu with zero vias, and anything that would normally cross on the bottom layer has to go somewhere else.

KiCad 3D render of the 80 by 62 mm single-sided board from above: ESP32-C3 module at the top, DRV8876 driver on the left, buck converter bottom right, USB-C on the right edge and the barrel jack on the left
The routed board, 80 × 62 mm, every track on the top layer. The plain grey blocks are placeholder bodies for connectors KiCad ships no 3D model for.

The first redesign was a sensible-looking placement handed to the autorouter. It left 15 connections open. Every one of them came back to crossings that a single layer cannot make. What closed the board was planning the topology first:

A Python script places every part, lays a locked skeleton of the tracks that decide the topology, and then lets FreeRouting fill in the rest on a one-layer board. KiCad’s DRC finishes at 0 violations, 0 unconnected items and 0 schematic mismatches, and the result held across repeated routing runs.

What went wrong and how it was diagnosed

Printable enclosure

The enclosure is modelled in Blender from a parametric script: a base holding the board and the servo, shaft down through the floor to the belt pulley, and a screw-on lid. The populated board is imported from KiCad as a 3D model. The script checks that each printed part is a single watertight solid, and that nothing collides with the lid closed. The jack and USB-C openings are sized for the plug overmoulds, not only the connector bodies.

Render of the drive box with its black lid set aside, showing the green PCB and the servo seated inside, next to a GT2 pulley and belt
Lid off: the real KiCad board model on its standoffs beside the servo. The box is 86 × 98 × 44.5 mm closed. Its openings for the barrel jack and USB-C are sized for the plugs.

Status

Designed, not built. The board has not been etched, and the firmware has not run on hardware. Still to do:

The ESP32 firmware and much of the layout scripting were written with AI to my spec. I set the requirements, reviewed the design decisions and will verify it all on the bench.

Tools and skills

KiCad 10 with Python-scripted schematic and board generation, FreeRouting, single-sided DFM for laser fabrication, TI DRV8876 current sensing, ESPHome on the ESP32-C3, Home Assistant, Blender scripting for enclosure design and verification, and FreeCAD for placeholder STEP models.

← All projects