11. Networking and Communications
This week's assignment is to design, build, and connect wired or wireless node(s) with network or bus addresses and a local interface. I used this task as the base for the Smart Pill Dispenser which is a modular system built around a custom PCB for the XIAO ESP32-C6. It combines an RTC clock (DS3231), an OLED display, and a Wi-Fi web interface (HTTP), all communicating over the I2C bus and served through the microcontroller's built-in Wi-Fi Access Point.
- Design and mill the PCB: Create the schematic and PCB layout in KiCad, convert Gerber files to PNG, generate the tool paths on MODS Project and mill on the Roland SRM-20.
- Solder components: Here test continuity before final assembly and add components to the board like SMD resistors, NeoPixel, and pin headers.
- Connect the RTC and OLED display: Wire the DS3231 real-time clock module via I2C (SDA/SCL), run an I2C scanner to confirm detection, and verify time-reading functionality. Repeat the test code with the OLED display.
- Build an I2C hub board: A small PCB that splits one I2C connection into multiple outputs so the RTC and OLED display can share the same two signal wires.
- Write the base code: Create an Arduino sketch that reads the time from the RTC clock and displays it on the OLED screen.
- Set up Wi-Fi Access Point: Configure the XIAO ESP32-C6 to broadcast its own Wi-Fi network (like a mini router), and create the HTTP interface.
- Solve hardware and software bugs: Debug issues like double pull-up resistors, wrong I2C pins, incorrect libraries, refining the system at every step.
What is Networking and Communications?
In electronics and embedded systems, networking and communications refers to the ways microcontrollers and other devices exchange data with each other or with the outside world. Think of it like spoken language: each protocol is a different language with its own rules about who speaks first, how many wires are needed, how fast data travels, and how far it can reach.
This week we explored how our microcontrollers can "talk" to sensors or other boards (even to other devices like phones or laptops), either through physical wires or wireless connections through radio signals. Some of the existing protocols are I2C, SPI, UART, Wi-Fi, Bluetooth, ESP-NOW, HTTP, MQTT, and many more. Each has its own advantages and disadvantages depending on the use case. To learn more about them check the group assignment.
I2C: Inter-Integrated Circuit
I2C is a two-wire serial bus: SDA (Serial Data-transports the info) and SCL (Serial Clock-Synchronizes the data transmission), plus a shared ground. One device is the host (usually the microcontroller) and others are the targets with a unique 7-bit addresses.
The host or controllersends a START signal, then the address of the device it wants to talk to, then the data, and finally a STOP signal. Every device on the bus listens, but only the one with the matching address responds. This is why you can connect many devices to the same two wires.
Advantages
- Only 2 wires needed regardless of how many devices
- Many devices can share the same bus (each has a unique address)
- Simple wiring, great for sensors and displays
Disadvantages
- Slower than SPI (up to ~400 kHz standard)
- Short distances only (same PCB or a few cm)
- Address conflicts if two devices share the same address
- Pull-up resistors required on SDA and SCL in some cases
I2C bus: one host, multiple targets, only 2 signal wires.
Real-world examples
OLED displays, RTC clocks, temperature sensors (BME280), IMUs (MPU-6050), EEPROMs. This is the protocol I used this week to connect the RTC (DS3231) and the OLED display to my ESP32-C6 PCB.
SPI: Serial Peripheral Interface
SPI is a four-wire full-duplex protocol: MOSI (Master Out Slave In), MISO (Master In Slave Out), SCK (Clock), and CS/SS (Chip Select, one per device). Data flows in both directions simultaneously, making it significantly faster than I2C.
Unlike I2C, SPI has no address system. Instead, you pick which device to talk to by pulling its CS line LOW. This means every additional device needs its own dedicated CS wire from the master.
Advantages
- Very fast (tens of MHz possible)
- Full-duplex: send and receive at the same time
- No address collisions
Disadvantages
- 4 wires + 1 CS per device (many wires quickly)
- Short distances only
- More complex wiring with multiple targets
SPI: 4 wires, very fast, each slave needs its own CS pin.
Real-world examples
SD card readers, SPI displays (ILI9341), flash memory, ADC/DAC converters, and wireless modules (nRF24L01 uses SPI for register access).
UART: Universal Asynchronous Receiver-Transmitter
UART is the simplest serial communication: just two wires, TX (Transmit) and RX (Receive), crossed between the two devices (TX of one goes to RX of the other). There is no clock wire; both devices must agree on the same speed beforehand, called the baud rate (commonly 9600 or 115200 bps).
It is asynchronous, meaning each byte is wrapped in start and stop bits so the receiver knows where each byte begins and ends without needing a shared clock.
Advantages
- Only 2 wires (TX + RX)
- Works over longer distances than I2C or SPI
- Universal: used by almost every microcontroller
- Easy to debug via Serial Monitor in Arduino IDE
Disadvantages
- Point-to-point only (just 2 devices)
- Both sides must use the same baud rate
- No built-in error checking
- Slower than SPI
UART: 2 wires, asynchronous, requires same baud rate on both ends.
Real-world examples
Arduino Serial Monitor, GPS modules (NEO-6M), GSM modules (SIM800L), Bluetooth UART adapters (HC-05), and communication between two microcontrollers.
Wi-Fi: Wireless Local Area Network
Wi-Fi allows a microcontroller to join or create a wireless network and communicate using standard internet protocols (TCP/IP, HTTP, WebSockets). The ESP32-C6 has a built-in Wi-Fi radio, so no extra hardware is needed.
There are two common modes: Station (STA) mode, where the microcontroller connects to an existing router like your phone does, and Access Point (AP) mode, where the microcontroller creates its own Wi-Fi network that other devices can join. This week I used AP mode so the pill dispenser works anywhere without needing an internet router.
Advantages
- No wires at all between devices
- Can serve web pages and full UIs to any browser
- Long range (tens of meters)
- Standard protocols (HTTP, WebSockets)
Disadvantages
- High power consumption vs I2C or UART
- More complex setup (IP addresses, servers)
- Latency higher than wired protocols
- Requires compatible hardware (not all chips have Wi-Fi)
Real-world examples
IoT sensors that upload data to the cloud, smart home devices, web-controlled robots, and exactly what I built: a pill dispenser with a web interface served from the microcontroller itself.
STA mode: Microcontroller connects to an existing Wi-Fi network.
AP mode: Microcontroller creates its own Wi-Fi network.
Bluetooth / BLE: Bluetooth Low Energy
Bluetooth is a short-range wireless protocol designed for device-to-device communication. Classic Bluetooth is used for audio streaming and file transfer. BLE (Bluetooth Low Energy) is optimized for low-power sensors and wearables: it sends tiny packets of data occasionally rather than maintaining a constant stream.
The ESP32-C6 supports BLE natively. Devices advertise small packets (called advertisements) that nearby phones can detect without pairing, or they can establish a connection for bidirectional data exchange.
Advantages
- Extremely low power in BLE mode
- No router or Wi-Fi network required
- Direct connection to phones
- Widely supported on all modern smartphones
Disadvantages
- Short range (~10 m typical)
- Limited bandwidth (BLE sends small data packets)
- Pairing/bonding can be complex
- Not ideal for serving full web pages
BLE: short and fast communication with low power consumption.
Real-world examples
Smartwatches, fitness trackers, wireless headphones, BLE beacons in stores, medical devices, and many home automation sensors.
ESP-NOW: Espressif's Peer-to-Peer Wireless Protocol
ESP-NOW is a connectionless wireless protocol developed by Espressif for ESP8266 and ESP32 family chips. Unlike Wi-Fi, there is no router, pairing handshake, or IP stack involved. Devices communicate directly using MAC addresses, sending small data packets of up to 250 bytes almost instantly.
It operates on the 2.4 GHz band and can work simultaneously alongside Wi-Fi. One device acts as the sender and targets one or more receivers by their MAC address. The receiver does not need to be "listening": it simply registers a callback that fires whenever a packet arrives.
Advantages
- No router or Wi-Fi infrastructure needed
- Extremely fast and low-latency (sub-millisecond)
- Very low power consumption compared to full Wi-Fi
- Can broadcast to multiple receivers simultaneously
- Works at up to ~200 m in open air
Disadvantages
- Limited to 250 bytes per packet
- Only works between ESP8266 / ESP32 devices
- No built-in encryption by default
- Requires knowing the target device MAC address in advance
Real-world examples
Multi-node sensor networks, wireless remote controls between ESP boards, mesh lighting systems, robotics projects where multiple ESP32 controllers need to talk directly, and distributed data loggers that relay readings to a central ESP node.
ESPNOW: Direct communication between ESP devices.
ESPNOW: Different way of communicating.
HTTP: HyperText Transfer Protocol
HTTP is the application-layer protocol that powers the web. When a browser types a URL and presses Enter, it sends an HTTP request to a server, which responds with an HTTP response containing the page content. In embedded systems, microcontrollers like the ESP32 can act as both HTTP clients (fetching data from APIs) and HTTP servers (serving web pages to browsers).
HTTP works on top of TCP/IP over Wi-Fi. A client sends a request with a method (GET, POST, PUT, DELETE), a path (e.g. /set-alarm), optional headers, and an optional body. The server responds with a status code (200 OK, 404 Not Found, 303 Redirect, etc.) and the response content.
Advantages
- Universal: any browser or device can make HTTP requests
- Human-readable protocol, easy to debug
- Works over any Wi-Fi network
- REST APIs are built on top of HTTP
- No special client software required
Disadvantages
- Higher overhead than raw TCP or UDP
- Stateless: each request is independent (no memory)
- HTTP (not HTTPS) is unencrypted by default
- Polling required to get live data (vs WebSockets)
HTTP: HyperText Transfer Protocol for web communication.
Real-world examples
This week's pill dispenser uses HTTP: the ESP32-C6 runs a web server that handles GET requests to / (home page), /setAlarm, /tomada, /toggleLang, and /reset. Any phone browser connected to the access point can interact with the device through these routes.
MQTT: Message Queuing Telemetry Transport
MQTT is a lightweight publish-subscribe messaging protocol designed for constrained devices and low-bandwidth networks. Instead of a client talking directly to a server, all devices connect to a central broker (like Mosquitto or HiveMQ). Devices publish messages to a topic (a named channel), and any device that has subscribed to that topic receives the message automatically.
This decoupled model is ideal for IoT: a temperature sensor publishes to home/bedroom/temperature, and a dashboard and a thermostat both subscribed to that topic each receive the reading independently.
Advantages
- Extremely lightweight: minimal packet overhead
- Decoupled: publishers and subscribers don't need to know about each other
- One message can reach many subscribers at once
- Supports QoS levels (fire-and-forget, at-least-once, exactly-once)
- Works over Wi-Fi and cellular
Disadvantages
- Requires a broker server (added infrastructure)
- Broker becomes a single point of failure
- Not suitable for large data transfers
- More complex setup than plain HTTP for simple tasks
MQTT: Lightweight publish-subscribe messaging protocol.
Real-world examples
Home automation platforms (Home Assistant, Node-RED), industrial IoT monitoring systems, connected weather stations, smart building sensors, and any scenario where many distributed sensors need to report to a central dashboard or trigger actions on other devices.
Smart Pill Dispenser: My Week 11 Project
Designing the Modular ESP32-C6 PCB
I designed a modular PCB for the XIAO ESP32-C6 in KiCad. The idea was to have a compact board that exposes I2C connections (SDA/SCL), SPI (MOSI, MISO, SCK), a NeoPixel for visual feedback/debug signals, a button and pin headers to add componets like a potentiometer. Being modular means the XIAO plugs in and can be removed for other projects.
Why the ESP32-C6?
The XIAO ESP32-C6 has both Wi-Fi (2.4 GHz) and Bluetooth Low Energy built in, which makes it ideal for a connected device like the pill dispenser. It is small, has sufficient GPIO, and is supported by Arduino IDE through the ESP32 board package.
The PCB production process follows the same steps as Week 08 (Electronics Production):
1.Schematic in KiCad
I drew the schematic placing the XIAO ESP32-C6 footprint at the center, then added I2C header pins (SDA, SCL, VCC, GND) for connecting external devices like the RTC and OLED.
I also added a 4.3 kΩ pull-up resistor on each I2C line ( this caused problems later), a WS2812B NeoPixel with a 240 Ω resistor on the data line, a button with a 10 kΩ resistor for user input and different pin headers for general use like SPI or extra GPIOs. I made sure to connect the GND and 3.3V power lines correctly.
2. PCB Layout and Routing
After placing all footprints I routed the copper traces with a minimum clearance of 0.5 mm and a minimum track width of 0.7 mm. Routing without cutting the paths was a bit tricky but I managed to fit everything in a 57x39 mm board.
Here and in the schematic I used the rule checker to find any errors and make sure all connections were correct. Some common errors were related to clearance violations and incorrect power connections.
3. Gerbers to PNG
Once done, I exported the Gerber files. And used Gerber2PNG to convert them to PNG format as it is required by the Mods page.
I saved both the outline and the traces as separate PNG files, which I then loaded into the Mods project page to generate the tool paths for milling.
4. RML files generation on Mods
I uploaded the PNG files to Mods, selecting the SRM-20 mill configuration and assigned the correct settings for each layer, all in x,y, z cordinates in 0,0,0.
The settings I use are:
- Traces for the engraving pass: 6º #V-bit tool, 0.4 mm tool diameter, 0.1016 mm cut depth with 4 offsets. And a speed of 4 mm/s.
- Outline (board edge): 1/16 cutout tool, 1.6 mm tool diameter, 0.6096 mm cut depth and a 0.6096 mm max depth. And a speed of 4 mm/s.
- Drills for through-hole: 1/32 drill tool, 0.8 mm tool diameter, 0.254 mm cut depth, 1.7018 mm max depth. And a speed of 0.2 mm/s.
Step 5: Milling on the Roland SRM-20
The board was milled on the Roland SRM-20 Mini Mill at Fab Lab Puebla using the RML tool paths generated from Mods.
Some issues I encountered were related to the zeroing of the tool and the flatness of the PCB material, which caused some traces to be cut too deep and others not deep enough. I had to adjust the z-zero point and run the milling process twice to get clean traces without cutting through the board.
Step 6: Soldering All Components
Before soldering anything, I didn't test continuity which would have caught any defects early (no continuity on the Neopixel connections).
Soldering order: First I did the SMD resistors (1206), the pin headers, then the WS2812B NeoPixel and lastly the XIAO ESP32-C6 module. Its important tto cover the bottom of the board to prevent short-circuits. After soldering, I tested continuity with a multimeter to confirm all connections were correct.
Setting Up Arduino IDE for the XIAO ESP32-C6
Before writing any code, Arduino IDE needs to know about the ESP32-C6 chip. Here is the step-by-step setup:
1. Add the ESP32 Board URL
Open Arduino IDE, go to File > Preferences, and paste this URL in the "Additional Boards Manager URLs" field and click "OK" to save:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
2. Install the ESP32 Package
Go to Tools > Board > Boards Manager, search for "esp32", and install the package by Espressif Systems (version 3.x or newer recommended).
3. Select the Correct Board
Go below Tools and click on the dropdown menu were the boards are list and click on
4. Search and select the XIAO ESP32-C6
After clicking these interface will appear, search for the "XIAO ESP32-C6" board and select it in the port you want to use.
If the board is not detected, hold the BOOT button while plugging in the USB cable to enter bootloader mode.
5. Install Required Libraries
Go to Sketch > Include Library > Manage Libraries and install the following to ensure all communications and peripherals work:
- RTClib by Adafruit: For communicating with the DS3231 RTC via I2C.
- Adafruit SH110X: Depending on your OLED display, we used for the 1.3" OLED (SH1106 controller).
- Adafruit GFX Library: The core graphics library required by all Adafruit displays.
- Adafruit NeoPixel: Necessary for controlling the integrated WS2812B LED.
- WiFi & WebServer: These are part of the ESP32 Arduino Core. You don't need to install them separately, but you must have the ESP32 boards installed in the Board Manager.
Note: If you are using a different OLED, you might need the Adafruit SSD1306 library instead.
To install a library, simply search for its name in the Library Manager, click on it, and then click "Install". Make sure to install the latest stable version to avoid compatibility issues.Here is a image of how the manager library looks:
I2C Hub Board and OLED Display
Since both the RTC and the OLED display use I2C, they can share the same two wires. However, my PCB only has one I2C connector. The cleanest solution was to build a small I2C hub board: a tiny PCB that splits one I2C connection into multiple outputs, so both devices share the bus in parallel.
I designed the hub in KiCad and milled it on the Mini Mill using the same process as before (schematic, PCB layout, Gerbers, Mods, mill, solder).
Final result
The hub helped us connect both the DS3231 RTC and the SSD1306 OLED display to the same I2C bus (D0/D1) without any issues. It was a simple 4-pin repeater board that made the wiring clean and modular.
Step 1: Designing the I2C Hub
The hub is a simple 4-pin bus repeater: VCC, GND, SDA, and SCL come in from the main board, and the same four lines go out to multiple connectors. No active components needed, just a compact PCB that holds everything cleanly without soldering individual wires.
Step 2: Milling and Assembling the Hub
The hub was milled and soldered following the same steps as the main PCB. After assembly I connected:
- Main PCB (D0/D1 I2C) → Hub input
- Hub output 1 → DS3231 RTC module
- Hub output 2 → SSD1306 OLED display (128x64 pixels)
Step 3: Wrong Library, Then Hello Fab!
When I first tried to run an OLED test, nothing appeared on the screen. After investigating I realized I had installed the wrong OLED library: I had installed the generic SSD1306 library instead of Adafruit SSD1306 (which also requires Adafruit GFX). Once I switched to the correct one, the display responded immediately.
Correct Libraries for SSD1306 OLED
Install both of these via Library Manager:
- Adafruit SSD1306 (search exactly this name)
- Adafruit GFX Library (required dependency)
// Hello Fab on OLED: first test
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // not used, set to -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
Wire.begin(D0, D1);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("OLED not found!");
while (1);
}
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 20);
display.println("Hello Fab!");
display.display();
}
void loop() {}
Step 4: RTC Time on the OLED
Next I combined both devices: the RTC provides the current time, and the OLED displays it. The date format was changed from numeric (7/4) to abbreviated text (7 Abr / 7 Apr) to avoid the common confusion between day and month.
// Show RTC time on OLED with bilingual date format
#include <Wire.h>
#include <RTClib.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
RTC_DS3231 rtc;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
// Returns abbreviated month name in Spanish or English
String getMonthName(int m, bool spanish) {
const char* esMonths[] = {"Ene","Feb","Mar","Abr","May","Jun",
"Jul","Ago","Sep","Oct","Nov","Dic"};
const char* enMonths[] = {"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"};
if (m < 1 || m > 12) return "?";
return spanish ? esMonths[m-1] : enMonths[m-1];
}
void setup() {
Wire.begin(D0, D1);
rtc.begin();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
}
void loop() {
DateTime now = rtc.now();
display.clearDisplay();
// Time: large text
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 8);
char timeBuf[9];
sprintf(timeBuf, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
display.print(timeBuf);
// Date: smaller text
display.setTextSize(1);
display.setCursor(10, 40);
display.print(now.day());
display.print(" ");
display.print(getMonthName(now.month(), false)); // false = English
display.print(" ");
display.print(now.year());
display.display();
delay(1000);
}
Connecting the DS3231 RTC via I2C
The DS3231 is a real-time clock module. It keeps track of the current time (seconds, minutes, hours, day, month, year) even when the microcontroller is off, thanks to a small coin cell battery. It communicates over I2C, making it simple to connect with just two signal wires (SDA and SCL).
DS3231 I2C Addresses
The DS3231 module exposes two I2C addresses on the bus: 0x68 for the clock registers and 0x57 for the built-in EEPROM (AT24C32). My I2C scanner confirmed both when the connection was working correctly.
I2C Bus connection result
My PCB was designed with SDA on D4 and SCL on D5 of the ESP32-C6, and included 4.3 kΩ pull-up resistors on both lines. I connected the RTC module and ran an I2C scanner sketch, but it returned nothing. No devices found.
Step 1: First Wiring Attempt (and the Pull-up Problem)
My PCB was designed with SDA on D4 and SCL on D5 of the ESP32-C6, and included 4.3 kΩ pull-up resistors on both lines. I connected the RTC module and ran an I2C scanner sketch, but it returned nothing. No devices found.
After a lot of trial and error swapping cables and pins, I discovered by accident that the RTC responded correctly on pins D0 (SDA) and D1 (SCL). The built-in I2C peripheral of the ESP32-C6 was different from what I had assumed in my schematic.
Step 2: I2C Scanner Sketch
This is the scan sketch I used to find which addresses were responding. Every I2C device has a unique address from 0x00 to 0x7F. The scanner tries each one and reports which ones respond.
// I2C Scanner: run this first to confirm your device is detected
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin(D0, D1); // SDA = D0, SCL = D1
Serial.println("Scanning I2C bus...");
int found = 0;
for (byte addr = 8; addr < 127; addr++) {
Wire.beginTransmission(addr);
byte error = Wire.endTransmission();
if (error == 0) {
Serial.print("Device found at address 0x");
if (addr < 16) Serial.print("0");
Serial.println(addr, HEX);
found++;
}
}
if (found == 0) {
Serial.println("No I2C devices found. Check wiring and pull-ups.");
} else {
Serial.print("Total devices found: ");
Serial.println(found);
}
}
void loop() {}
Successful output:
Scanning I2C bus... Device found at address 0x57 Device found at address 0x68 Total devices found: 2
Step 3: Reading Time from the RTC
With the scanner confirming the RTC was detected, I loaded the RTClib library and ran a basic time-reading sketch. The first time you run it, the RTC needs to be set to the current time. DateTime(__DATE__, __TIME__) reads the compile time from Arduino IDE automatically.
#include <Wire.h>
#include <RTClib.h>
RTC_DS3231 rtc;
void setup() {
Serial.begin(115200);
Wire.begin(D0, D1);
if (!rtc.begin()) {
Serial.println("RTC not found! Check wiring.");
while (1);
}
// Set RTC to the time this sketch was compiled
// Remove this line after the first upload or time will reset on every boot
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
Serial.println("RTC ready.");
}
void loop() {
DateTime now = rtc.now();
Serial.print(now.year()); Serial.print('/');
Serial.print(now.month()); Serial.print('/');
Serial.print(now.day()); Serial.print(" ");
Serial.print(now.hour()); Serial.print(':');
Serial.print(now.minute()); Serial.print(':');
Serial.println(now.second());
delay(1000);
}
Wi-Fi Access Point and Web Interface
The final layer of the project is the web interface for the pill dispenser. The XIAO ESP32-C6 creates its own Wi-Fi network (Access Point mode) so the user can connect from any phone or laptop without needing an internet router. The built-in web server then serves an HTML/CSS interface at a fixed IP address (192.168.4.1).
How Access Point Mode Works
In AP mode the ESP32-C6 behaves like a mini router. It broadcasts a Wi-Fi network with a name (SSID) and optional password. When you connect to it and open a browser to 192.168.4.1, the microcontroller receives the HTTP request and sends back the web page. No internet connection required.
OLED Welcome Screen (Connection Window)
When the device powers on, the OLED shows a 15-second welcome window displaying the network SSID and the IP address. This gives the user time to connect before the display switches to clock mode. After 15 seconds it transitions automatically to show the current time.
Web Interface: Iterative Design
I built the interface from simple to complex, iterating continuously. The final interface includes:
- A language toggle (Spanish / English) that updates all UI text in real time
- A Quick Set Up section to set alarms by time only (date auto-handled)
- A Manual Set Up section with full date and time control
- A circular log showing the last 3 alarms set and last 3 doses taken
- A current time display reading live from the RTC
Initial Web Interface Layout
The first version of the web interface was basic but functional. It included simple time input fields and a status display. This early prototype helped me understand the HTML structure and what elements were essential before adding complexity like bilingual support and logs.
Final Web Interface in Action
The final version includes the language toggle button, Quick Set Up for fast alarm scheduling, alarm and dose logs, and live time display from the RTC. The interface automatically refreshes to show the current status and supports both Spanish and English without page reloads.
Access Point Setup Code
#include <WiFi.h>
#include <WebServer.h>
const char* ssid = "PillDispenser";
const char* password = "farma2026";
WebServer server(80);
void setup() {
Serial.begin(115200);
// Start Access Point
WiFi.softAP(ssid, password);
Serial.print("AP IP address: ");
Serial.println(WiFi.softAPIP()); // prints 192.168.4.1
// Define routes
server.on("/", handleRoot);
server.on("/set-alarm", HTTP_POST, handleSetAlarm);
server.on("/dose-taken", HTTP_POST, handleDoseTaken);
server.on("/time", handleTime);
server.begin();
Serial.println("Web server started.");
}
void loop() {
server.handleClient();
}
Alarm Logic: The "Tomorrow" Bug Fix
The Quick Set Up lets the user enter an alarm time (e.g., 8:00 AM). But if it is already 10:00 PM, 8:00 AM has already passed today. The first version triggered the alarm immediately in that case because it compared hour and minute values directly without checking the date.
The Fix: Compare UNIX timestamps instead. If the computed alarm time is in the past, add exactly 24 hours (one day) automatically.
void handleSetAlarm() {
int alarmHour = server.arg("hour").toInt();
int alarmMinute = server.arg("minute").toInt();
DateTime now = rtc.now();
// Build alarm DateTime for today at the requested H:M
DateTime alarmTime(now.year(), now.month(), now.day(),
alarmHour, alarmMinute, 0);
// If that time is already past, schedule for tomorrow
if (alarmTime.unixtime() <= now.unixtime()) {
alarmTime = alarmTime + TimeSpan(1, 0, 0, 0); // add 1 day
}
// Store alarmTime and set up alarm...
server.send(200, "application/json",
"{\"status\":\"ok\",\"scheduled\":\"" + alarmTime.timestamp() + "\"}");
}
Bilingual Interface and Log System
The web page stores all text in two dictionaries (a JavaScript object) and swaps them when the user clicks the language toggle. This means the entire interface changes to Spanish or English without reloading the page.
The circular log keeps arrays of the last 3 alarm entries and last 3 dose confirmations, displayed in the interface so the user can check their history at a glance.
Same interface in Spanish (left) and English (right).
Full System in Action
Everything running together: the XIAO ESP32-C6 creates the Wi-Fi network, the OLED shows the clock and notifications, the RTC maintains accurate time, and the web interface lets the user set alarms and log doses from their phone.
Problems Encountered
Hardware and software debugging was a big part of this week. Here are the main issues I ran into and how I solved each one:
NeoPixel With No Continuity
I added a WS2812B NeoPixel to my PCB schematic, but when I uploaded any NeoPixel test sketch, the LED never lit up. I suspected a cold solder joint, so I desoldered everything and checked all connections. After checking and re-soldering multiple times I still could not find the cause. I eventually found that there was no continuity on the trace leading to the NeoPixel's data pin, most likely a milling artifact (a very thin gap in the copper). Because the rest of the project was working, I decided to skip the NeoPixel for now and instead use a standard green and red LED for debug feedback. Green = operation OK, Red = error or fail. This was actually more reliable than the NeoPixel for debugging because it does not require any library and works even when the Serial Monitor is unavailable.
Double Pull-up Resistors on I2C
As described in the RTC section, my PCB added 4.3 kΩ pull-up resistors on SDA and SCL. The DS3231 module has its own internal pull-ups. Running both sets in parallel lowered the effective resistance significantly, interfering with I2C signaling. The devices were not detected at all. Once I identified this, I learned that you should only use pull-up resistors on the bus when they are not already provided by the sensor module.
Wrong I2C Pins (D4/D5 vs D0/D1)
My schematic mapped SDA to D4 and SCL to D5, following a diagram I found online. In practice the XIAO ESP32-C6 exposes its hardware I2C on D0 (SDA) and D1 (SCL). Even though the ESP32 can use Wire.begin() with any two pins in software mode, the hardware I2C peripheral on D0/D1 was much more stable. I discovered the correct pins through trial and error, which cost significant debugging time.
Wrong OLED Library
After wiring the OLED and running a display test, nothing appeared. I eventually traced this to having installed a generic SSD1306 library instead of the Adafruit SSD1306 + Adafruit GFX pair. The function names and initialization sequence are different between libraries. Switching to the Adafruit version resolved the issue immediately.
Alarm Triggering Immediately (The Date-Bug)
When using Quick Set Up to schedule a time that had already passed in the current day (e.g., setting 8:00 AM at 10:00 PM), the alarm fired right away. The fix was to compare full UNIX timestamps instead of just hour and minute values, and add 24 hours automatically when the computed alarm time was already in the past. Described in detail in the Web Server tab.