INTERFACE AND APPLICATION PROGRAMMING
This week was about building a software interface that lets a person actually control or monitor the hardware they have made.
Individual Assignment
- Write an application that interfaces a user with an input &/or output device that you made
Group Assignment
- Compare as many tool options as possible
What is Interface and Application Programming
A microcontroller on its own is a black box. It reads sensors, drives actuators, and communicates over serial or network connections, but unless something is translating those signals into something a human can read and respond to, the device is effectively invisible. Interface programming is the layer that bridges the machine and the person.
In practice this means writing software that can send commands to a device and receive data back, then present that data in a form the user can understand such as numbers, charts, buttons, indicators, and let them act on it without needing to know the underlying protocol.
The interface could run in a browser, on a desktop, or on a phone. The device could talk over serial, Bluetooth, or WiFi. The specific tools matter less than the architecture: there is always a hardware side, a communication channel, and a software side, and all three have to agree on how data is structured and exchanged.
MediBee
For this week I built the control interface for MediBee, a smart pill dispenser I am developing as my final project. The dispenser is built around a XIAO ESP32C6 microcontroller. It has a rotating disc that holds pills in separate compartments, a servo-controlled gate, a solenoid lock, a buzzer, and an OLED display. Dosing happens automatically on a schedule, but the user needs a way to configure that schedule, monitor whether doses were taken, and trigger a manual dispense if needed.
The interface I built is an Android app written in React Native. The app communicates with the ESP32 over WiFi using a simple HTTP REST API running on the device. Both the phone and the dispenser need to be on the same WiFi network, and the app stores the device IP address so it reconnects automatically every time it opens.
Architecture Overview
The system has three layers:
- Hardware layer — the XIAO ESP32C6 board, connected to the stepper motor, servo, solenoid, buzzer, Hall sensors, and OLED display
- Communication layer — an HTTP server running on the ESP32, port 80, exposing REST endpoints the app can call
- Application layer — the React Native Android app, running on the user's phone, consuming those endpoints
The app never talks directly to the hardware pins. It makes HTTP requests to named endpoints on the device — /schedule, /dispense, /history, /status — and the firmware handles translating those into physical actions. This separation means the app logic and the hardware logic stay independent: changing the motor control code does not break the app, and adding a new screen to the app does not touch the firmware.
ESP32 Firmware
The firmware runs on the XIAO ESP32C6 using the Arduino framework. Its job is to manage the hardware and expose a REST API the app can call. I wrote it in Arduino C++ and flashed it using Arduino IDE 2 with the ESP32 board package and the following libraries:
- ArduinoJson — parsing and generating JSON for all API requests and responses
- NTPClient — syncing the device clock to
pool.ntp.orgwith IST offset - ESP32Servo — controlling the gate servo
- Adafruit SSD1306 + Adafruit GFX — driving the OLED display
REST API Endpoints
Every endpoint adds CORS headers so the app can call them from any origin. All request and response bodies are JSON.
| Endpoint | Method | Description |
|---|---|---|
/status |
GET | Device health, current disc slot, next dose time, WiFi signal, uptime |
/schedule |
GET | Returns the full list of dose slots with their times and enabled state |
/schedule |
POST | Replaces the full slot list — up to 8 slots, each with an id, time, and enabled flag |
/dispense |
POST | Manually triggers dispense for a given slot number |
/history |
GET | Returns the last 30 dispense events with taken/missed status and timestamps |
/settings |
POST | Toggle buzzer on/off, or push new WiFi credentials to the device |
/reset |
POST | Reboots the ESP32 |
Startup Sequence
When the device powers on, the firmware runs through a fixed startup sequence in setup():
- Configure all GPIO pins — buzzer, solenoid, Hall sensors, stepper motor
- Lock the solenoid and close the gate servo to a known state
- Initialize I2C and bring up the OLED display
- Load saved data from NVS (non-volatile storage) — dose schedule, history, buzzer preference
- Auto-detect the lid Hall sensor polarity by sampling 20 readings at boot
- Home the disc by stepping the stepper until the disc Hall sensor triggers
- Connect to WiFi — 30 attempts with 500ms between each
- Sync time via NTP
- Register all HTTP route handlers and start the server
- Double buzz to signal the device is ready
Dispense Logic
The main loop checks the current time against the saved schedule every 10 seconds. When the current time matches an enabled slot that has not been dispensed yet today, dispenseSlot() fires. The exact sequence depends on the slot index:
- Slot 0 — advances the stepper disc by one compartment using
rotateOneSlot() - Slot 1 — opens the servo gate, waits 5 seconds, then closes it
- Slot 2 and above — unlocks the solenoid, opens the gate, closes it, reopens briefly, then re-locks
Dispensed flags reset at midnight each day. Slots that were missed (the device was off at the scheduled time) are logged as missed in the history.
The React Native App
The app is a single-file React Native project — all the screens, navigation, API calls, and styles live in one MediBeeApp.tsx file. I chose React Native because it targets Android directly and I am familiar with JavaScript. The APK is built using Gradle and runs standalone on the phone with no dev server needed.
Shared State — AppCtx
At the root level, a React context called AppCtx holds three values shared across all screens:
- ip — the ESP32's IP address on the local network, persisted to AsyncStorage so it survives app restarts
- setIp — function to update the IP from the Settings screen
- online — a boolean set by a 15-second ping loop that polls
GET /statusfrom the root component
Every screen reads online from context and disables action buttons when the device is unreachable. This prevents the user from triggering requests that will silently time out.
API Layer — apiFetch
All network calls go through a single wrapper function, apiFetch, that adds a 5-second timeout using AbortController. If the request does not complete within 5 seconds the controller aborts it and the caller catches the error. This keeps the UI responsive even when the device is off or the IP is wrong.
Time — IST Without Locale Dependency
The device runs on Indian Standard Time (UTC+5:30). Rather than relying on the phone's locale setting, the app computes IST directly: Date.now() + 5.5 × 3600000. It then reads UTC hours and minutes from that shifted timestamp. This works correctly on any phone regardless of what timezone is set in Android settings.
App Screens
Home
The home screen shows a live IST clock updating every second, the current date, the next scheduled dispense time, and an online/offline badge for the device. Below that, each dose slot in the schedule is listed with a Dispense Now button that posts to /dispense to trigger a manual release. The buttons are disabled and dimmed when the device is offline.
Schedule
The Schedule screen loads the current slot list from the device on open. Each slot shows the dose time in 12-hour format and a toggle switch to enable or disable it. Tapping the time opens a custom time picker modal with up/down arrows for hours and minutes and AM/PM selector buttons. Slots can be added (up to 8) or removed. Nothing is sent to the device until the user taps Save to device, which posts the full updated slot array to POST /schedule.
History
The History screen pulls the last 30 dispense events from GET /history and displays them in reverse chronological order. Each entry shows the slot number, the scheduled time, a timestamp of when the event was recorded, and a taken/missed badge. Pull-to-refresh reloads the list from the device.
Settings
The Settings screen has three sections:
- Device connection — a text field for the ESP32's IP address. The field saves to AsyncStorage on blur so the app remembers the address between sessions. A Test connection button makes a direct fetch to
/statusand shows a green or red result with the failure reason if it does not connect. - Push WiFi to device — fields for SSID and password. Sending these posts to
POST /settingswith the credentials, which the firmware writes to NVS. The device uses them on the next reboot. This means you only need to hardcode credentials once at flash time; after that you can update them OTA from the app. - Hardware — a toggle for the buzzer that posts to
POST /settingsimmediately on change, and a Reset device button that confirms before posting toPOST /reset.
Building the APK
React Native apps need to be compiled into an APK before they can be installed on a phone. The build uses Gradle, which bundles all the JavaScript into the binary so the app runs standalone without a Metro dev server. The key steps were:
- Copy
MediBeeApp.tsxinto the React Native project asApp.tsx - Install dependencies —
@react-navigation/native,@react-navigation/bottom-tabs,react-native-screens,react-native-safe-area-context,@react-native-async-storage/async-storage - Set
JAVA_HOMEto Android Studio's bundled JRE atC:\Program Files\Android\Android Studio\jbr - Run
.\gradlew assembleReleasefrom theandroid/directory - Collect the output APK from
android/app/build/outputs/apk/release/app-release.apk
One important detail: assembleDebug produces an APK that tries to load JavaScript from a Metro dev server at startup. Installing it on a standalone phone with no server running produces a blank screen. The release build bundles the JS, so it works offline.
Gradle 9.x also breaks the React Native plugin, so the wrapper is pinned to Gradle 8.14 in gradle-wrapper.properties.
Result
The app connects to the MediBee dispenser over the local WiFi network, shows live status, lets the user configure the dose schedule and push it to the device, view dispense history, and trigger a manual dispense from any screen. The ESP32 firmware manages all the hardware independently — it runs the schedule and fires dispenses automatically without the phone being open — and the app gives the user visibility and control over that process.
Prompts Used
Both the app and the firmware were developed using Claude Code. Below is the prompt used to generate the initial React Native app. The firmware was built in the same session by extending the API requirements into Arduino C++.
App Prompt
Build a mobile app UI (React Native or Flutter) for a smart pill dispenser called MediBee. The app connects to an ESP32C6 via WiFi. Screens: Home Screen Current time (IST) Current slot number Next dispense time Device connection status (online/offline) Schedule Screen Shows 3 schedule slots: 08:00, 12:00, 20:00 Toggle each on/off Edit dispense time History Screen Log of past dispenses with timestamp Missed dose indicator Settings Screen WiFi credentials input Buzzer on/off toggle Design: Clean medical UI White/blue color scheme Large readable text for elderly users Backend: ESP32 hosts a simple HTTP REST API App sends GET/POST requests to ESP32 IP address
Code
MediBeeApp.tsx — React Native App
View full source — MediBeeApp.tsx
medibee_esp32.ino — ESP32 Firmware
Firmware code to be added — paste the .ino contents and I will embed it.