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

Group Assignment

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:

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:

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():

  1. Configure all GPIO pins — buzzer, solenoid, Hall sensors, stepper motor
  2. Lock the solenoid and close the gate servo to a known state
  3. Initialize I2C and bring up the OLED display
  4. Load saved data from NVS (non-volatile storage) — dose schedule, history, buzzer preference
  5. Auto-detect the lid Hall sensor polarity by sampling 20 readings at boot
  6. Home the disc by stepping the stepper until the disc Hall sensor triggers
  7. Connect to WiFi — 30 attempts with 500ms between each
  8. Sync time via NTP
  9. Register all HTTP route handlers and start the server
  10. 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:

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:

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:

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:

  1. Copy MediBeeApp.tsx into the React Native project as App.tsx
  2. Install dependencies — @react-navigation/native, @react-navigation/bottom-tabs, react-native-screens, react-native-safe-area-context, @react-native-async-storage/async-storage
  3. Set JAVA_HOME to Android Studio's bundled JRE at C:\Program Files\Android\Android Studio\jbr
  4. Run .\gradlew assembleRelease from the android/ directory
  5. 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.