SYSTEM INTEGRATION
Assignment
- Design and document the system integration for your final project
What is System Integration
System integration is the phase where individual subsystems stop being separate experiments and start being a product. Up until this point, it's fine for each piece to work in isolation: a circuit that drives a servo, a firmware loop that checks a schedule, a 3D-printed shell that fits together. Integration means all of those things have to coexist in the same physical enclosure, share power, communicate over the same bus, and behave correctly under the same conditions, at the same time.
MediBee
MediBee is a smart automatic pill dispenser built as my Fab Academy final project. The hardware is a custom PCB built around a XIAO ESP32C6, driving a 28BYJ-48 stepper motor (rotating a medication disc), a servo (controlling the dispensing gate), a solenoid lock, a passive buzzer, and an SSD1306 OLED display. On the software side, a React Native Android app communicates with the device over WiFi via a REST API hosted on the ESP32.
Each subsystem had been developed separately across previous weeks. System integration meant wiring them into a single enclosure, resolving hardware conflicts, and validating end-to-end behaviour from the phone app down to physical actuation.
System Architecture
The full system has four layers:
- Mechanical layer — the 3D-printed enclosure, the medication disc, the gate mechanism, and the solenoid lock housing
- Electronics layer — the custom PCB carrying the XIAO ESP32C6, motor drivers, and all peripheral connections
- Firmware layer — Arduino C++ running on the ESP32, managing hardware state and the HTTP REST API
- Application layer — the React Native Android app communicating with the device over local WiFi
The key integration requirement was that no layer should need to know the implementation details of another. The app talks to named HTTP endpoints — it doesn't know whether the device uses a stepper or a servo. The firmware drives hardware pins — it doesn't know what phone screen triggered the request. This separation meant I could test and iterate each layer independently before wiring them together.
Mechanical Integration
The enclosure was designed in Fusion 360 and printed in PLA. The main challenge during physical assembly was fitting all of the components into the available space with real wiring, not the idealised routing from the CAD model.
Disc and Motor Alignment
The medication disc was designed to grip the stepper motor shaft by friction alone, but under real load inside the enclosure it would slip and end up half a compartment off — lining the wrong pill slot up with the dispensing gate. The fix was a small protruding boss at the centre of the disc with an M3 screw pressing against the flat of the motor shaft, locking the disc in place mechanically. After that, the disc and shaft moved together with zero slip.
The Stepper motor will rotate only once during the day. Preferable in the morning, right before the first dose.
Servo Gate Mechanism
The servo controls the dispensing gate, a hinged flap that opens to drop a pill into the output chute.
With this mechanism, I can control which slot must dispense at one time. From the image, you can see that the cmpartment at the center is not covered. This dispenses the morning dose.
At noon, the servo rotated the door to the center, which opens the compartment at the side, and the noon dose dispenses.
Solenoid Mounting
The solenoid lock is mounted to the enclosure using two M2 screws through a printed bracket. Getting the angle right was important — if the solenoid was even slightly off, the plunger would catch on the latch instead of sliding through cleanly.
When the solenoid is activated, the plunger retracts and releases the door. The servo rotates the other door to the center. Then rotates back to the original position, which opens the compartment at the side, and the noon dose dispenses.
This is possible by attaching magnets to the side of the doors.
Wire Routing
This was the biggest mechanical surprise. The PCB, stepper motor, servo, solenoid, buzzer, OLED, and Hall sensors together produce about 30 cm of wiring inside an enclosure that was designed with essentially zero consideration for harness routing.
Electronics Integration
The custom PCB carried the XIAO ESP32C6 plus driver circuits for all the peripherals. Board-level testing had verified each peripheral independently. Integration testing found two issues that only appeared when everything was running simultaneously.
Power Rail Noise
When the stepper and solenoid fired at the same time, the 5 V rail dropped briefly and the OLED either blanked or displayed garbage for one frame. Both the stepper driver (ULN2003) and the solenoid driver (a single transistor) are inductive loads, and neither had a flyback diode on the schematic — I had planned to add them and then forgot. Adding a 1N4007 across each coil terminal and a 100 µF electrolytic cap across the 5 V input header resolved the rail drop. The OLED stopped misbehaving.
I2C Bus Reliability
The OLED is the only I2C device, so bus contention was not an issue, but the OLED occasionally failed to initialise on power-up when the ESP32 was also connecting to WiFi. The WiFi stack startup on the ESP32C6 is aggressive — it temporarily preempts other tasks including the I2C peripheral clock. The fix was simple: move the OLED begin() call to after WiFi connection is confirmed, with a 100 ms delay before the first display(). That gave the I2C bus time to settle and the OLED has initialised cleanly on every power cycle since.
Hall Sensor Calibration
The disc home position is detected by a Hall effect sensor mounted beneath the track. During integration I found the sensor was triggering at slightly different disc positions depending on temperature — the PCB sitting next to the stepper motor warms up over a 20-minute dispensing session and the Hall threshold drifts. The firmware already had a polarity auto-detect at boot (sampling 20 readings), but I extended it to also sample and store the background field level, then use a 10% hysteresis band around that level rather than a fixed threshold. The homing became consistent across the full operating temperature range.
Firmware Integration
The firmware was written in Arduino C++ and structured into independent modules — each hardware subsystem has its own functions and the main loop coordinates them. Integration testing revealed ordering and timing issues that weren't visible in unit tests.
Startup Sequence Order
The original startup sequence initialised the WiFi stack before homing the stepper disc. This created a 5-10 second window where the disc was in an unknown position and the HTTP server was already accepting requests. If the app happened to call GET /status during that window, the firmware returned a slot position of 0 even when the disc hadn't homed yet, and a subsequent POST /dispense could fire before the disc was ready.
The fix was to restructure the startup sequence so the disc always homes before WiFi connects:
- Configure all GPIO pins
- Lock solenoid and close gate to known state
- Init I2C and bring up the OLED
- Load saved data from NVS
- Auto-detect Hall sensor polarity
- Home the disc — stepper runs until Hall sensor triggers
- Connect to WiFi
- Sync NTP time
- Register HTTP routes and start server
- Double buzz — ready
With this order, the device only starts accepting requests when it is fully initialised. A banner on the OLED shows "HOMING…" then "CONNECTING…" then "READY" so the user can see the startup progress.
Dispense Concurrency
The HTTP server runs on a background task and the dispense logic runs on the main task. When a POST /dispense request came in while an automatic scheduled dispense was already running, both the request handler and the scheduler tried to call dispenseSlot() simultaneously. On the bench this never happened because test requests were sent manually between scheduled doses. In integration it was easy to trigger — open the app while a scheduled dispense is in progress and tap Dispense Now.
The fix was a boolean flag dispensing set at the start of any dispense call and cleared at the end. Both the HTTP handler and the scheduler check this flag before calling dispenseSlot(). If a dispense is already in progress, the HTTP handler returns HTTP 409 and the app shows a brief message — "Device busy, try again shortly."
NTP Time Sync
The device syncs time over NTP at startup and then again every 6 hours. During integration, I found the re-sync was happening mid-dispense — the NTP update momentarily blocks the main loop long enough to miss a dose check in the 10-second polling interval. Moving the NTP sync to fire only when no dispense is in progress (using the same dispensing flag) resolved the conflict.
App Integration
The React Native app was developed and tested against a mock API running on a laptop. When tested against the real device over WiFi, two issues came up.
IP Address Discovery
The app requires the user to manually enter the device's IP address. In a home network this IP is usually stable (routers tend to hand out the same lease), but in the lab the device got a different address each session because multiple devices share the same pool. The workaround in the short term was to configure the lab router to assign a static IP lease to the ESP32's MAC address. The longer-term solution would be mDNS — the ESP32 can advertise itself as medibee.local and the app can resolve that hostname — but that's a post-integration improvement.
Offline State Handling
The app polls GET /status every 15 seconds to determine whether the device is online. During integration I found that when the device was rebooting (e.g., after pushing new WiFi credentials), the 5-second fetch timeout in apiFetch meant the app could spend up to 20 seconds showing "Online" before the poll caught the device going offline. This wasn't a functional problem but it confused users who expected the badge to update immediately after hitting Reset Device.
The fix was to immediately set online = false in the Settings screen when the user confirms the reset, without waiting for the next poll cycle. The badge updates within one render frame of the button press.
End-to-End Test
Once all three integration fixes were in place, I ran a full end-to-end test covering the primary user flow:
- Power on device — observe HOMING → CONNECTING → READY on OLED
- Open app on phone — confirm Online badge appears within 15 seconds
- Navigate to Schedule — confirm current slot list loads from device
- Add a new slot 2 minutes in the future — save to device
- Wait for automatic dispense — observe stepper rotate, gate open, buzzer sound, OLED update
- Navigate to History — confirm dispense event appears with correct timestamp and Taken status
- Navigate to Home — tap Dispense Now on a slot — confirm manual dispense fires immediately
- Navigate to Settings — push new WiFi credentials — tap Reset — confirm device reconnects
All eight steps passed on the first run after integration. Steps 5 and 6 (automatic dispense + history) are the highest-risk path because they span firmware timing, hardware actuation, NVS writes, and an HTTP response — four subsystems in sequence. Getting that path clean is what system integration week is for.
Issues Encountered and Resolved
| Issue | Root Cause | Fix |
|---|---|---|
| Disc slips during dispense | Friction-fit shaft connection had play from print tolerance | Set-screw boss on disc hub, M2 bolt pins to motor shaft flat |
| Gate servo drags on enclosure wall | CAD clearance gap too small after print tolerance | Reprinted gate housing with 1.5 mm slot instead of 0.5 mm gap |
| OLED blanks during stepper + solenoid fire | Missing flyback diodes on inductive loads caused 5V rail drop | Added 1N4007 across each coil, 100 µF cap across 5V input |
| OLED fails to init on power-up | WiFi startup preempts I2C peripheral clock | Moved OLED init to after WiFi connection, added 100 ms delay |
| Hall sensor triggers at inconsistent positions | Background field level drifts with temperature | Extended polarity detect to store baseline field, applied 10% hysteresis band |
| HTTP server accepts requests before disc homes | WiFi connected before homing in startup sequence | Restructured startup: home disc first, then connect WiFi |
| Concurrent dispense from HTTP handler and scheduler | No mutex on dispenseSlot() between tasks |
dispensing flag checked by both callers before entering dispense |
| NTP sync blocks dispense loop | Re-sync timer fires regardless of device state | NTP re-sync gated on !dispensing |
| App badge slow to update after device reset | Badge driven by poll cycle, not by button action | Immediately set online = false on reset confirmation in UI |
What the Integrated System Looks Like
At the end of integration week, MediBee is a single self-contained device that:
- Powers on and homes itself to a known state before accepting any requests
- Runs a dose schedule automatically, triggering stepper rotation, gate actuation, and buzzer alert at the configured times
- Logs every dispense event (taken or missed) to non-volatile storage
- Hosts a REST API over WiFi that the Android app can call from the same network
- Lets the user view status, configure the schedule, trigger manual dispenses, and review history from the app
- Recovers cleanly from power loss — on reboot it restores the saved schedule and re-homes the disc
What's Left
Integration is not the same as done. A few things are working but not polished, and a few are still outstanding:
- Lid Hall sensor — the lid tamper detection is implemented in firmware but the physical magnet placement needs tuning. Currently it triggers false positives if the device is moved quickly.
- mDNS —
medibee.localhostname resolution would eliminate the manual IP entry step in the app. Planned post-integration improvement. - Enclosure lid fitment — the top half of the enclosure needs one more reprint. The current version has a slight warp on the long axis that makes the lid seam visible. Not functional, but not great either.
- App APK distribution — the APK is currently side-loaded by USB. A proper install flow (QR code or direct download link from the device) is on the backlog.
Key Learnings
System integration week produced more real fixes than all the unit testing weeks combined. The things that failed were not the things I expected to fail — they were interactions between subsystems that you can only find by running everything together. Some specific observations:
- Power supply design matters more in an integrated system than on a bench. Inductive loads sharing a rail with a microcontroller need flyback protection and decoupling. Design this in from the start, not after the board is assembled.
- Startup sequence order is a system-level decision. Each subsystem was fine starting in any order in isolation. Together, the order was the only thing that mattered.
- Physical tolerances in CAD are not physical tolerances in reality. A 0.5 mm clearance in a model needs to be at least 1.5 mm in a print.
- Concurrent task access to shared hardware is a real concurrency problem even on a single-core microcontroller, because interrupt-driven peripheral operations can interleave with cooperative task switching in the HTTP server stack.