System architecture overview
The Pill Dispenser is organized around a central XIAO ESP32-C6 controller that orchestrates
six independent subsystems. The diagram below shows how they connect:
Actuation
Servo SG90
Opens the day-pod trapdoor on schedule
Motion
28BYJ-48 Stepper
Rotates carousel to the correct day-pod
Sensing
IR + Load cell
Detects whether pills were actually taken
Controller
XIAO ESP32-C6
Custom PCB
I²C bus · GPIO · PWM
WiFi AP
Display
OLED SSD1306
Shows time, dose status, and alerts (I²C)
Time
RTC DS3231
Real-time clock with CR2032 backup (I²C)
Alert
Buzzer + LED RGB
Audible and visual alarm at dose time
Modular pod concept
Each day-pod is a 3D-printed rectangular container that holds up to four pills
(one full day's dosage pre-sorted by a caregiver). The pod snaps onto a circular carousel with 31 slots -
one per day. A single SG90 servo is mounted on the base station at a fixed dispensing position;
when the carousel rotates to the active pod, the servo arm reaches into the pod and flips the trapdoor open.
Pills fall through a chute into a collection tray. The pod is then either re-filled or discarded.
This keeps the electronic complexity minimal: one stepper, one servo, one ESP32
- regardless of how many types of pills the user takes.
A caregiver batch-fills all 31 pods once a month in about 10 minutes.
Packaging integration
The enclosure combines FDM-printed PLA for complex 3D geometry with laser-cut acrylic
for flat structural parts. All joints use M3 screws and standard connectors , no glue. The collection tray and chute
clip out without tools for cleaning.
Bill of materials
Controller
XIAO ESP32-C6
Seeed Studio
Main microcontroller. WiFi AP for web interface, I²C master, 11 GPIO, USB-C programming. Mounted on custom PCB.
Actuation
Servo motor SG90
Tower Pro SG90
Controls the trapdoor latch of the active day-pod. 180° range, PWM signal from GPIO, limited to 90° travel via mechanical stop.
Motion
Stepper motor
28BYJ-48 + ULN2003
Rotates the 31-slot carousel to the correct day-pod. Full-step mode, 64 steps/rev × 64 reduction = 4096 steps/rev. Home switch at position 0.
Time
Real-time clock
DS3231 SN
Maintains accurate time even during power cuts thanks to a CR2032 coin cell. ±2 ppm accuracy. I²C address 0x68. Triggers alarm interrupt to ESP32.
Display
OLED screen
SSD1306 0.96" 128×64
Shows current time, today's day number, dose status (taken / pending / missed), and RTC battery warning. I²C address 0x3C.
Alert
Buzzer + LED RGB
Passive 5V + WS2812
Passive buzzer driven via NPN transistor from GPIO. WS2812 LED shows green (taken), amber (pending), red (missed). Driven from single data pin.
Sensor - auto confirm
IR reflective sensor
TCRT5000
Mounted inside the collection tray. Detects the presence or absence of pills after dispensing. Confirms the tray is empty when pills are taken. No user action required.
Sensor - backup
Load cell
50 g + HX711
Secondary sensor on the collection tray. Detects total pill weight before and after dispensing. Works as a cross-check against the IR sensor for higher confidence.
Power
5 V DC + LiPo backup
TP4056 + 2000 mAh
Wall adapter (5 V 2 A). TP4056 charger module with a 2000 mAh LiPo cell for up to 6 h of backup during power outages. Diode OR-ing for seamless switching.
PCB
Custom PCB
Milled FR4 (FabLab)
Houses the ESP32-C6, decoupling caps, JST connectors for all peripherals, I²C pull-ups (4.7 kΩ), NPN driver for buzzer, and 100 µF bypass cap on motor rail.
Housing
3D-printed enclosure
PLA, 0.2 mm layer
Two-part base station + 31 day-pods + dispensing chute + collection tray. All parts designed in Onshape; tolerances 0.3 mm for smooth fit.
Fasteners
M3 screws + standoffs
Hex M3 × 8 mm
All mechanical joints use standard M3 hardware. No adhesives. Designed so the enclosure can be fully disassembled with a single screwdriver.
Housing - laser cut
Acrylic sheet parts
2-3 mm clear, laser cut
Three parts cut from clear acrylic: the front panel (3 mm, cutouts for OLED and buttons, labels engraved directly), the dispensing chute (2 mm, transparent so a jammed pill is visible without opening the device), and the carousel base plate (3 mm, flatter and more dimensionally stable than FDM for the rotating shaft seat). Tolerances ±0.1 mm.
Operational logic
The firmware runs a simple state machine on the ESP32-C6.
On every boot, the device performs a homing sequence (stepper returns carousel to position 0),
then syncs with the RTC and enters the Idle state.
At the scheduled dose time the alarm fires and the device transitions through
Dispense , Confirm and Log before returning to idle.
Boot + homing
ESP32 powers up, stepper runs to home switch (position 0), OLED shows "Homing...". RTC time is read and validated.
Idle state
OLED shows current time and day number. WiFi AP is active. Buzzer and LED are off. ESP32 sleeps in light-sleep, waking every 30 s to check RTC alarm flag.
Alarm trigger
DS3231 alarm interrupt wakes ESP32. Buzzer sounds a 3-tone sequence every 60 s. LED turns amber. OLED shows the dose reminder for the current day.
Dispense
Stepper rotates carousel to the current day-pod. Servo opens trapdoor for 2 s. Pills fall into collection tray through chute. Servo closes trapdoor.
Confirm (automatic)
IR sensor and load cell monitor the collection tray.
IR detects empty tray + load cell reads near-zero weight, pills confirmed taken.
Timeout: if tray is not cleared within 30 min, the alarm escalates (faster beeping, red LED)
and the event is logged as "missed". The user can also tap a physical button on the device
or press "Taken" in the web interface to manually confirm.
Log + return to idle
Result (taken / missed / manually confirmed) is written to EEPROM with timestamp.
OLED turns green briefly. LED turns green. Buzzer plays a soft confirmation tone.
Stepper parks carousel at home position. ESP32 sets next RTC alarm and returns to idle.
Dual-sensor pill confirmation
The IR reflective sensor (TCRT5000) is mounted at the bottom of the collection tray and
detects reflected light: pills present = high reflectance, tray empty = low reflectance.
The load cell (50 g, HX711 amplifier) provides a weight reading.
Together they form a two-factor confirmation: the software marks a dose as taken only when
both sensors agree. If they disagree (e.g. one pill fell outside the tray),
the system logs a warning and prompts the caregiver via the web interface.
This is more reliable than either sensor alone and does not require the user to do anything.
Interaction interfaces
The Pill Dispenser supports three interaction modes: the physical device itself,
a smartphone web interface (no app install needed), and a desktop/tablet web interface.
On-device interface
Three physical buttons are mounted on the front panel, accessible without glasses or a phone:
- Taken - manual confirmation that pills were taken (backup for sensor failure)
- Skip - marks today's dose as intentionally skipped (e.g. doctor's order)
- Snooze - silences the alarm for 10 minutes (up to 3 times per dose)
The OLED shows: current time (large), today's day number, dose status, and a battery/RTC indicator.
Long-pressing Skip + Taken together opens a simple on-device menu to adjust the alarm time.
Web interface
The ESP32-C6 broadcasts a WiFi access point (password on label).
Open http://192.168.4.1 in any browser, no app, no internet required, like week 11's assignment. The interface adapts to the screen size: on a phone you get a simplified view focused on daily confirmation, while on desktop/tablet you get a full dashboard with calendar and settings.
- Large "Taken" confirmation button (touch-friendly, 72 px tap target)
- Schedule editor: set up to 3 alarm times per day with a time picker
- Dose history: scrollable log of last 31 days with taken / missed / skipped status
- Current day pod status: how many doses remain today
- RTC battery and device power status indicator
- Refill reminder: shows which day-pods need to be refilled
Testing plan
Testing is divided into three phases: unit testing (each subsystem alone),
integration testing (subsystems talking to each other), and
user acceptance testing (the full device with a real user).
Phase 1 - Unit
Mechanical
- Carousel completes 31 positions without skipping steps (100 cycles)
- Servo opens and closes trapdoor cleanly in 9 out of 10 attempts
- Day-pod snaps in/out without tools, 50 cycles, no wear
- Pills fall through chute without jamming (10 different pill sizes)
Phase 1 - Unit
Electronics
- Power draw measured with stepper active: must stay below 900 mA
- I²C bus stable with OLED + RTC simultaneously (logic analyser)
- Load cell reads pill weights within ±0.5 g of reference scale
- IR sensor correctly distinguishes full tray from empty tray in 10/10 cases
- LiPo backup activates within 200 ms of wall power loss
Phase 2 - Integration
Firmware + hardware
- Full dispense cycle (alarm, rotate, open, close, confirm) in under 45 s
- RTC alarm fires within ±30 s of set time after 24 h running
- EEPROM write verified with checksum after simulated power cut during write
- Web interface loads in under 3 s from cold phone connection
- Dual-sensor agreement: 20 test cycles with pills, 20 without - 0 false positives
Phase 2 - Integration
Durability
- 72-hour burn-in: device runs 3 dispense cycles per day, no reset or hang
- Drop test: device dropped from 60 cm onto carpet, functionality verified after
- Vibration test: device shaken 30 s, no connector disconnection
- Thermal test: 6 h at 35 °C ambient (summer conditions), no component overheating
Phase 3 - UAT
User acceptance
- User can identify the "Taken" button without instructions
- Caregiver can reload all 31 pods in under 15 min
- Web interface navigation completed by a non-technical user in under 5 min
- Device runs for 7 days in a real home environment with no errors
QA
Pre-assembly checks
- Multimeter continuity check on all PCB tracks before power-on
- Verify JST connector polarity with key visual marker
- Flash firmware and confirm serial output before closing enclosure
- Check OLED display with test pattern before final assembly
Failure modes and s
The table below documents the most likely failure scenarios, their severity, and how the design prevents or recovers from them.
| Failure |
Severity |
Root cause |
Fix |
| Carousel misaligns (stepper skips steps) |
High |
Insufficient driver current; mechanical friction |
Home switch at position 0 runs every boot + daily re-home. ULN2003 current set to motor spec. PTFE washer on carousel shaft. |
| RTC loses time after power cut |
High |
CR2032 battery flat |
OLED shows an RTC battery warning when voltage drops below threshold. Device still dispenses; time re-syncs when caregiver opens web interface. |
| IR sensor false negative (pills not detected) |
Medium |
Dark-coloured pill absorbs IR; sensor dirty |
Load cell provides independent confirmation. If sensors disagree, event logged as "uncertain" and caregiver is notified via web. Sensor window is easily wiped clean. |
| WiFi AP unreachable |
Medium |
ESP32 WiFi stack crash; SSID conflict |
AP is restarted automatically every 30 min if no client connects. Physical reset button on side panel restores AP immediately without losing data. |
| EEPROM data corruption |
Medium |
Power cut during write; wear after ~100 k writes |
Every write includes a CRC-8 checksum. On boot, checksum is verified; if corrupt, the record is flagged and caregiver is notified. Wear-levelling across EEPROM pages. |
| Servo fails to open trapdoor |
Medium |
Stall from tight PLA tolerance; PWM noise |
1.5 mm fillet on all trapdoor guides. Decoupling cap on servo power line. Firmware detects stall (no pill in tray after 5 s) and retries once, then logs failure. |
| User misses alarm |
Low |
Buzzer not loud enough; user away from device |
Buzzer volume 80 dB at 10 cm. Alarm repeats every 10 min for up to 30 min. LED flashes red. Web interface shows "Pending" in real time if caregiver checks. |
| Pod not inserted for current day |
Low |
Caregiver forgot to refill that slot |
IR sensor detects empty pod before dispensing. OLED shows a refill prompt. Buzzer gives a different 2-tone signal to distinguish from the dose alarm. |
Repair
The Pill Dispenser is designed to be fully serviceable by anyone with a screwdriver.
No adhesives, no press-fit electronics, no proprietary connectors.
Opening the enclosure
4 × M3 hex screws on the bottom panel. Remove them and the top shell lifts off.
No clips that break, no hidden fasteners.
Replacing a module
Every peripheral connects via a JST-PH 2-pin connector with a locking tab.
Disconnect, swap, reconnect. Servo, display, RTC, buzzer - all hotswappable with power off.
Reprinting parts
All STL and STEP files are published on GitLab. Any broken plastic part
(pod, chute, carousel, trapdoor) can be reprinted in 2-4 h on a standard FDM printer.
Reflashing firmware
USB-C port is accessible without opening the enclosure. Hold BOOT + press RESET,
then flash with esptool or the Arduino IDE. All user data is stored in EEPROM, not flash -
it survives a reflash.
Battery replacement
LiPo cell is held by a velcro strap inside the base. Unplug JST connector, swap cell.
CR2032 for RTC is in a through-hole holder on the PCB - coin-press to replace.
Cleaning
Collection tray and dispensing chute clip out without tools. Both are food-safe PLA,
hand-washable. Wipe the IR sensor window with a dry cotton swab.
Lifecycle
Manufacture
FabLab mill PCB, 3D print parts, source components
Assembly
Solder, wire harness, test, close enclosure
Use
Daily dispense, monthly pod refill by caregiver, OTA firmware updates
Repair
Replace failed components; reprint broken parts; reflash firmware
End of life
PLA recycled; PCB + electronics to WEEE recycling; motors reused
Expected service life with basic maintenance: 5+ years.
The most likely replacement items are the LiPo cell (every 2-3 years),
the CR2032 RTC backup (every 2-3 years), and the day-pods (printed on demand, as needed).