Stationery Unit D2

by Arnov Sharma in Circuits > Gadgets

93 Views, 0 Favorites, 0 Comments

Stationery Unit D2

I Built a Smart R2-D2 Desk Companion!
R2D2 Desk Bot
33A.gif
35.gif
36.gif
38.gif
39.gif
121123.jpg

Greetings everyone, and welcome back!

Here's something from beyond Stationery Unit D2, an R2-D2-inspired, interactive desk organizer and pen holder.

This isn't just a 3D-printed, R2-D2-looking pen holder; it’s an interactive, web-connected desk terminal powered by an ESP32-P4 dev board.

What really brings this droid to life is its behavior. I set up a background task in the code so the device generates life-like actions all on its own. Every 20 seconds, it plays a unique sequence of astromech beeps and chirps. It uses math to pick random frequencies, so it sounds like the droid is actually thinking or muttering to itself, making it feel truly alive.

I also wanted to make it easy to use, so I added two physical buttons on top. One button (GPIO 48) triggers fun "Easter Egg" songs I coded in, like the Star Wars theme and a few others, while the second button (GPIO 47) lets me instantly toggle the screen between a Radar UI and my favorite GIFs.

The web interface is where the real fun happens. Through the app, we can play GIFs on the ESP32-P4's display, trigger a cinematic Star Wars-style quote display, or even use the text-to-astromech converter to make the droid speak any message.

The design of this project was heavily inspired by R2-D2. Instead of creating a standard cylindrical R2-D2, I went with a flatter design that incorporates key visual elements from the original droid. The body was 3D-printed and then assembled.

This article covers the complete build process, from design and wiring to setting up the code and final assembly.

Let's get started.

Supplies

These were the materials used in this project—

  1. Custom PCBs (Provided by PCBWAY)
  2. Wavshare ESP32 P4 Dev Board with Touch Display
  3. Lithium Cell for Power source 3.7V 500mAh
  4. 3D printed parts
  5. 4x4 Push Buttons
  6. Connecting Wires
  7. Speaker
  8. M2 screws

DESIGN

R2.791.png
R2.792.png

The entire model was designed in Fusion 360. While this project is heavily inspired by R2-D2, I didn't follow the original design entirely; I completely ditched the traditional cylindrical body in favor of a flat, modern aesthetic that still closely resembles the iconic droid.

For the internals, I incorporated a Waveshare ESP32-P4 development board, which features a 4.7-inch display positioned slightly off-center. My idea is to print a blue screen on the display with a red circle that resembles R2D2.

To capture that authentic droid aesthetic, I modeled greeble parts, which I’ll be printing in blue, and the body will be printed in white.

On the back, I added a custom lid section that serves as a pen holder, keeping the device practical while looking the part.

Downloads

ENCLOSURE

Screenshot 2026-06-11 140642.jpg
Screenshot 2026-06-11 140717.jpg
Screenshot 2026-06-11 140819.jpg

The enclosure of our design comprises two main parts: the front enclosure, which is designed to resemble R2-D2, and the back enclosure, which serves as a lid for the model while also incorporating the pen holder section.

Both the front and back enclosures are secured together using M2 screws.

SWITCH SECTION

Screenshot 2026-06-11 140908.jpg
Screenshot 2026-06-11 140949.jpg

For adding controls, I incorporated two buttons into the design. The buttons are positioned on the top face of the model.

To mount them, I designed two retaining ribs that allow the switch PCB to slide into position securely. Before installing the switch PCB, two identical switch actuators are placed in their respective positions.

Once the switch PCB is inserted, the actuators are locked in place. Pressing these actuators activates the switches, registering the button press.

POWER SOURCE PLACEMENT

Screenshot 2026-06-11 141050.jpg

For the power source, I used a 3.7 V 500 mAh lithium-ion cell (size 14500), which is smaller than the more commonly used 18650 cell.

I positioned the battery on the opposite side of the display and added two ribs to keep it securely locked in place.

ESP32 P4 DISPLAY PLACEMENT

Screenshot 2026-06-11 141129.jpg

The ESP32-P4 Dev Board is placed slightly off-center. To keep it securely in position, I added several ribs that ensure the display does not move from its intended location.

GREEBLES PARTS

Screenshot 2026-06-11 141204.jpg
Screenshot 2026-06-11 141411.jpg
Screenshot 2026-06-11 141556.jpg

To make this device look and feel like a droid, I designed several greeble parts that would be printed in Blue PLA and then placed in position.

These include a part that sits between the screen and the head section, as well as a long part that sits next to the display.

Two additional parts are mounted on the top section slightly above the display, along with a circular grille part, all of which are printed in Blue PLA.

3D PRINTED PARTS

IMG_E8892.JPG

After finalizing the model, all parts were exported as mesh files and then 3D printed on my Anycubic Kobra S1 printer using two filament colors.

Blue Hyper PLA was used for the majority of the parts, including the back enclosure, greeble parts, and switch actuator.

White Hyper PLA was used for the front enclosure. All parts were printed with a 0.4 mm nozzle, 0.2 mm layer height, 25% infill, and no supports were required for the blue parts.

Only the front enclosure required supports, as it was printed upside down, so the inner parts of the model needed support.

PCB DESIGN

Screenshot 2025-10-18 115921.jpg
Screenshot 2025-10-18 120000.jpg

This is the Switch PCB we used in this project, and it's a simple one. Here, we have added two 4×4mm push buttons on a 20×20mm board. The placement of each button and the mounting holes are all positioned according to the 3D model.

A CON5 connector is also added. The first pin is connected to GND, and the remaining four terminals are connected to each button pin. Through this connector, we will later pair the UNIHIKER M10 with our button board.

BTW, we are reusing this PCB from a previous project, which you can check out at the link below.

https://www.instructables.com/HitPad/

PCBWAY SERVICE

Image50.jpg
IMG_9477.JPG
Screenshot 2026-07-08 154943.jpg

The Switchboard PCB was provided by PCBWay. I designed the PCB in my PCB design software and sent the Gerber files to PCBWay for fabrication. The PCBs arrived within a week, and the overall quality was amazing.

Over the past 12 years, PCBWay has built a strong reputation for providing reliable PCB manufacturing and assembly services, becoming a go-to choice for engineers and makers worldwide.

This month, PCBWay is celebrating its 12th Anniversary with a special campaign. As part of the celebration, the campaign highlights the journey of makers on the platform, including how long they’ve been part of the PCBWay community and the different PCB colors they’ve explored over the years. PCBWay offers a wide variety of PCB colors beyond the classic green, including yellow, blue, red, and many more, giving makers even more ways to make their projects stand out.

The anniversary campaign also features special activities, rewards, and offers, so it’s worth checking out if you’re planning your next PCB project.

PCBWay also offers CNC machining and 3D printing services, making it a convenient platform for bringing all kinds of hardware projects to life.

SWITCH PCB ASSEMBLY

11.gif
12.gif
13.gif
  1. Button board assembly was pretty straightforward. We start by placing all switches in their position.
  2. Next, we flipped the board over and then used a soldering iron to solder all the leads of the push buttons.

The switch PCB is now assembled.

HARDWARE: WAVSHARE ESP32 P4 WIFI6 Touch LCD

04-Trim.gif
05-Trim.gif

Here's the star of our project: the Waveshare ESP32-P4 Development Board. It is built around the ESP32-P4 microcontroller, featuring a dual-core 400 MHz RISC-V processor for high-performance applications, along with a dedicated low-power RISC-V core for efficient background tasks.

The board comes equipped with a 4.3-inch IPS capacitive touch display with a resolution of 480 × 800 pixels, providing a responsive and vibrant user interface. It supports a rich set of human-machine interaction peripherals, including a MIPI-CSI camera interface with an integrated Image Signal Processor (ISP) for image capture and processing applications. The board also features USB 2.0 OTG High-Speed (HS) support, enabling fast data transfer and versatile USB connectivity options.

For hardware expansion, it includes an onboard 40-pin GPIO header that is compatible with selected Raspberry Pi HAT expansion boards, making it easy to integrate additional sensors, modules, and peripherals.

An additional highlight of this development board is the onboard ESP32-C6-MINI module. Since the ESP32-P4 itself does not include native Wi-Fi or Bluetooth connectivity, the ESP32-C6 serves as a dedicated wireless coprocessor, providing Wi-Fi 6 (802.11ax) and Bluetooth Low Energy (BLE 5.x) capabilities. This combination allows developers to leverage the processing power of the ESP32-P4 while maintaining modern wireless connectivity for IoT and connected-device applications.

You can check out more details about this board from Wavshare's WIKI PAGE.

https://docs.waveshare.com/ESP32-P4-WIFI6-Touch-LCD-4.3

ESP32 P4 WIFI6 Touch LCD SPEAKER ASSEMBLY

06.gif
08.gif
09.gif

In the Waveshare ESP32-P4 Wi-Fi 6 Touch LCD Kit, an onboard compatible speaker is included.

This speaker serves as a crucial component of our build, as it will be used to output the various astromech sound effects generated by the system.

  1. To install the speaker, we first removed the protective layer from the 3M double-sided adhesive tape attached to its back.
  2. The speaker was then carefully positioned in the center of the board, directly above the ESP32-P4 metal shielding can, ensuring that it did not interfere with any connectors or components.
  3. Once aligned correctly, the speaker was pressed firmly into place, securing it to the board and completing the installation.

GREEBLES PART ASSEMBLY

17.gif
18.gif
19.gif
20.gif
21.gif
22.gif
  1. We start the greebles assembly process by placing the Grill part in its position first. It is pressed firmly into place and gets locked in position due to the zero surface clearance.
  2. Next, we apply super glue to the mounting positions of the other greeble parts, then position them one by one.

All parts require some super glue for mounting.

ESP32 P4 & SWITCH BOARD ASSEMBLY

10.gif
11.gif
  1. Next, we connect our switchboard to the ESP32-P4 Board.
  2. We connect the GND pin of the switchboard to the GND pin of the ESP32-P4, GPIO48 to Button 1, and GPIO47 to Button 2.
  3. For these connections, single-core silver-copper wire is used.

SWITCH SECTION ASSEMBLY

12.gif
13.gif
14.gif

The 3D-printed switch actuators were positioned in place from inside the main enclosure.

Next, the switch PCB is slid into position slightly below the switch actuators. It is pressure-fitted in place and remains securely fixed.

ESP32 P4 ASSEMBLY

23.gif
24.gif

The ESP32-P4 board is placed into position from the inside of the main enclosure. To secure it in place, hot glue is applied around all four corners of the display, firmly attaching it to the 3D-printed enclosure.

I've been watching James's channel a lot recently, and I really like how he uses hot glue in many of his builds instead of overengineering screw bosses, brackets, and retainers just to hold a part in place. For non-load-bearing components, it's a simple, fast, and surprisingly effective solution.

POWER SOURCE ASSEMBLY

26.gif
27.gif
28.gif

For the power source, I am using a standard 3.7 V, 2200 mAh 18650 lithium-ion cell. A JST connector has been added to the battery, making it compatible with the ESP32-P4 board's battery connector.

A small amount of hot glue is applied to the battery mounting area, and the cell is placed on top of it. Once the glue hardens, the battery is securely held in position.

Finally, using a pair of tweezers, the JST connector from the battery is plugged into the ESP32-P4 board's battery connector, completing the internal power setup.

FINAL ASSEMBLY

30.gif
31.gif

Finally, the back lid is placed in position, and four M2 screws are used to secure both enclosure parts together, completing the assembly process.

CODE

The code for this project was done in collaboration with Aahan Sharma; he took care of the whole code part.

Full code can be found here on his GitHub page: https://github.com/AahanDoesGit/r2d2

While trying to build this project, I struggled to get the ESPIDF VS Code extension working properly on my MacBook. The paths and Python environments can sometimes get tangled. I found that setting up ESP-IDF purely via the macOS Terminal is much cleaner

Install Dependencies

First, install the required build tools using Homebrew:

brew install cmake ninja dfu-util python3

Download ESP-IDF (v5.4)

Create a directory for your Espressif tools and clone the specific ESP-IDF release branch. We used 5.4.x because it includes critical support for the new ESP32-P4 chip:

mkdir -p ~/esp
cd ~/esp
git clone -b release/v5.4 --recursive https://github.com/espressif/esp-idf.git

Install the Toolchain

Navigate into the newly cloned directory and run the install script. This downloads the correct compilers (like the RISC-V GCC toolchain for the P4) and creates an isolated Python virtual environment:

cd ~/esp/esp-idf
./install.sh all

The Golden Rule (Exporting the Environment)

Every time you open a new Terminal window to work on your project, you must "source" the ESP-IDF environment. If you don't do this, you will get a zsh: command not found: idf.py error!

source ~/esp/esp-idf/export.sh

Building and Flashing

With your environment active, navigate to your project folder (e.g., cd ~/esp/r2d2-system). You can now build, flash, and monitor the serial output with a single command:

idf.py -p /dev/cu.usbmodemXXXXX build flash monitor

Booting Up (main.c)

The entry point of our application is app_main. Its job is to initialize the hardware peripherals and spin up our individual subsystems

void app_main(void)
{
// 1. Initialize the SD Card (Mounts /sdcard)
r2d2_sdcard_init();

// 2. Initialize Procedural Audio Synthesizer
r2d2_audio_init();

// 3. Initialize the Display and LVGL GUI
r2d2_display_init();

// 4. Connect to Wi-Fi
r2d2_wifi_init();
// Once Wi-Fi connects, it automatically starts the Web Server!
}

We mount the SD card first because the display needs to load fonts and GIFs, and the audio needs to allocate buffers

The Network: ( r2d2_wifi.c)

Connecting to Wi-Fi on an ESP32 involves the Event Loop. Don't just connect; we register handlers that listen for events like WIFI_EVENT_STA_START or IP_EVENT_STA_GOT_IP.

static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
// We successfully got an IP address!
// Start SNTP to sync the Galactic Standard Time (GST)
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "pool.ntp.org");
sntp_init();

// Start the Web Dashboard
r2d2_webserver_start();
}
}

Eliminating Wi-Fi Latency

By default, ESP-IDF puts the Wi-Fi modem into Power Save Mode, which is 300-500ms delayed.

ESP_ERROR_CHECK(esp_wifi_start());
// Disable Wi-Fi power save to eliminate HTTP latency
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));

The Voice of R2-D2: ( r2d2_audio.c )

This generates a tone mathematically.

To play a sound, we generate a Sine Wave into a PCM buffer and send it to the ES8311 I2S Audio Codec

static void play_astromech_beep(int start_freq, int end_freq, int duration_ms) {
int num_samples = (SAMPLE_RATE * duration_ms) / 1000;
int16_t *sample_buffer = malloc(num_samples * sizeof(int16_t));
for (int i = 0; i < num_samples; i++) {
double t = (double)i / SAMPLE_RATE;
double progress = (double)i / num_samples;
// Slide the frequency from start to end (Pitch Bending)
double current_freq = start_freq + (end_freq - start_freq) * progress;
// Generate the sine wave sample
sample_buffer[i] = (int16_t)(10000 * sin(2 * M_PI * current_freq * t));
}

// Send the raw audio to the speaker chip
esp_codec_dev_write(speaker_dev, sample_buffer, num_samples * sizeof(int16_t));
free(sample_buffer);
}

Translating Text to Beeps

When you type "Hello" into the dashboard, we take the ASCII value of each character to seed our procedural math

for (int i = 0; text[i] != '\0'; i++) {
int seed = (int)text[i]; // ASCII value
// Deterministically generate a pitch based on the letter!
int start_freq = 400 + (seed * 19) % 1100;
int end_freq = start_freq + ((seed * 37) % 500 - 250);
int duration = 90 + (seed * 11) % 180;
play_astromech_beep(start_freq, end_freq, duration);
}

The Physical Screen: ( r2d2_display.c)

We created a retro-futuristic RADAR UI using standard LVGL components

// Create a spinning arc to simulate a radar sweep
radar_arc = lv_arc_create(screen_bg);
lv_arc_set_bg_angles(radar_arc, 0, 360);
lv_obj_set_style_arc_color(radar_arc, lv_color_hex(0x00FF00), LV_PART_INDICATOR);

// Use an LVGL animation to spin it indefinitely
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, radar_arc);
lv_anim_set_values(&a, 0, 360);
lv_anim_set_time(&a, 2000);
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
// ...

Hardware mutex locks

Because FreeRTOS runs the UI on Core 0, but our Web Server also runs on Core 0 or 1 might try to change the screen text. We wrap every LVGL call with a Mutex Lock provided by the BSP (Board Support Package)

bsp_display_lock(0); // Pause rendering
lv_label_set_text(quote_label, "NEW TEXT FROM DASHBOARD");
bsp_display_unlock(); // Resume rendering

The Star Wars Frontend

Before we look at the C code that serves the website, we will explore how we built the actual Star Wars-themed frontend interface, the entire frontend HTML, CSS, and JavaScript, into the ESP32 firmware as a massive C string.

The Cinematic Layout & Animations

We didn't just want a boring dashboard; we wanted an immersive Star Wars experience. To achieve this without heavy image files, we relied heavily on Pure CSS

/* Deep Space Background */
body {
background-color: #0b0f19;
color: #ffe81f; /* Classic Star Wars Yellow */
font-family: 'Orbitron', sans-serif;
}

/* Twinkling Stars */
.stars {
position: absolute;
width: 100%; height: 100%;
background-image:
radial-gradient(2px 2px at 20px 30px, #ffffff, rgba(0,0,0,0)),
radial-gradient(2px 2px at 40px 70px, #ffffff, rgba(0,0,0,0));
background-size: 200px 200px;
animation: twinkle 4s infinite;
}

@keyframes twinkle {
0%, 100% { opacity: 0.8; }
50% { opacity: 0.3; }
}

We also added CSS objects for an X-Wing and TIE Fighter flying across the screen, using @keyframes to animate their left and top properties infinitely.

Dynamic JavaScript & Buttons

The frontend uses the modern JavaScript fetch API to communicate with the ESP32 without requiring page reloads.

The Transmit to Droid Button

When you type a phrase and click the transmit button, JavaScript grabs the value and sends an HTTP POST request to the ESP32

document.getElementById('speak-btn').addEventListener('click', () => {
const input = document.getElementById('speak-input');
const text = input.value.trim();
if (!text) return;

// Send the text to the ESP32 backend
fetch('/api/speak', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: text
}).then(res => {
if(res.ok) {
console.log("Transmission successful");
input.value = ''; // Clear the input field
}
});
});

Polling System Status

To keep the dashboard updated with the droid's current state (e.g., the system time, current mode

const updateStatus = () => {
fetch('/api/status')
.then(res => res.json())
.then(data => {
// Update the UI with the JSON response
document.getElementById('current-screen-mode').innerText =
data.mode === 'DISPLAY' ? 'DISPLAY MODE' : 'AUDIO / IDLE';
document.getElementById('galactic-clock').innerText = 'GST: ' + data.time;
});
};
// Run this every 5 seconds
setInterval(updateStatus, 5000);

This is how the web browser stays perfectly synced with physical hardware

The Backend ( r2d2_webserver.c)

We wrote a massive raw string in C containing our HTML, CSS, and JavaScript.

static const char* dashboard_html =
"<!DOCTYPE html>"
"<html>"
"<head><style>body { background: #0b0f19; color: #ffe81f; }</style></head>"
// ... hundreds of lines of UI ...
"</html>";

static esp_err_t get_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "text/html");
httpd_resp_sendstr(req, dashboard_html);
return ESP_OK;
}

The API Endpoints (The Backend)

The Web UI relies on JavaScript fetch() calls to talk to the ESP32. We set up an HTTP GET handler at /api/status.

static esp_err_t status_get_handler(httpd_req_t *req)
{
// 1. Get current mode
r2d2_mode_t mode = r2d2_get_mode();
// 2. Get current quote on screen
char quote_buf[128];
r2d2_display_get_quote(quote_buf, sizeof(quote_buf));

// 3. Format as a JSON string
char json_resp[512];
snprintf(json_resp, sizeof(json_resp),
"{\"mode\":\"%s\", \"quote\":\"%s\"}",
mode == MODE_DISPLAY ? "DISPLAY" : "AUDIO_IDLE", quote_buf);

// 4. Send back to the browser!
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, json_resp);
return ESP_OK;
}

Handling the "Transmit to Droid" Button

When you type text and click Transmit, the browser sends an HTTP POST to /api/speak.

static esp_err_t speak_post_handler(httpd_req_t *req)
{
char buf[128] = {0};
// Receive the text payload from the browser
httpd_req_recv(req, buf, sizeof(buf) - 1);

// Send it to the screen
r2d2_display_set_quote(buf);

// Send it to the audio synthesizer (this spins off a background task!)
r2d2_speak_text(buf);

httpd_resp_sendstr(req, "OK");
return ESP_OK;
}

Because r2d2_speak_text spins up a new FreeRTOS task, the speak_post_handler returns OK immediately!

Build and Flash Instructions

Clone the repository:

git clone https://github.com/AahanDoesGit/r2d2.git
cd r2d2-system

Load the ESP-IDF environment (assuming standard installation path):

source ~/esp/esp-idf/export.sh

Build the firmware:

idf.py build

Flash the firmware and open the serial monitor:

Replace /dev/cu.usbmodem5B5E0698681 with your board's specific port

idf.py -p /dev/cu.usbmodem5B5E0698681 flash monitor


RESULT

I Built a Smart R2-D2 Desk Companion!
R2D2 Desk Bot
32.gif
33B.gif
34.gif
36.gif
38.gif
39.gif

And here is the final result: a fully functional, talking R2-D2-themed desk bot that doubles as a handy pen holder.

You can store your pens and stuff on the back while the droid keeps you company at your desk. The front screen displays a striking blue background with a red circle that symbolizes R2-D2’s eye, which pulses and animates whenever the droid speaks. It’s the perfect blend of utility and personality, a true desk companion that feels like it’s come to life right in front of you.

Every 20 seconds, the device plays a 5-second sequence of random astromech beeps and chirps. Instead of playing static files, it uses math to pick random frequencies and then bends the pitch up and down in real time. Because of this randomness, the sounds feel just as organic and unpredictable as a real droid would make.

For manual control, I’ve mapped two buttons that do the following functions:

Button 1 triggers our Easter Egg songs. I coded these six special tracks purely using R2-D2’s beep frequencies. The library includes AC/DC’s Back in Black (R2-D2 Edition), Happy Birthday, Rick Astley’s Never Gonna Give You Up (Rickroll!), the Star Wars Main Theme, the Super Mario Bros. Theme, and my personal favorite, the Pokémon Diamond/Pearl Lake Theme.

Button 2 acts as a physical toggle for the screen mode. Even if you don't have the web dashboard open, you can press this button to switch the display back and forth between the Radar UI and GIF play modes.

For now, this project has been completed. Special thanks for reaching this far, and I’ll be back with a new project very soon.