JBR-001: A Desktop Companion Robot Powered by Arduino UNO Q
JBR-001 is an open-source desktop companion robot powered by the Arduino® UNO™ Q and built to make robotics, computer vision, edge AI, and physical AI fun to explore.
Introduction
JBR-001 is an open-source, 3D-printable desktop companion robot powered by the Arduino® UNO™ Q.
We designed JBR-001 as a small platform for experimenting with robotics, physical interaction, edge AI, and computer vision. It combines a camera, distance sensing, sound, an animated display, and three servo motors in a compact robot that you can print, assemble, program, and extend yourself.

The robot can greet you by moving its arms and head, play sounds, animate its display, sense nearby objects using a distance sensor, and use its camera for computer vision applications running on the Arduino UNO Q.
JBR-001 - an open-source desktop companion robot powered by the Arduino® UNO™ Q
How It Works
At the center of JBR-001 is the Arduino UNO Q. It controls the robot's display, sensors, buzzer, and servo motors, while also providing the computing capabilities needed for more advanced applications such as computer vision.
JBR-001 uses three servo motors to create movement. One controls the head, while the other two control the left and right arms.
A distance sensor located at the back of the head allows JBR-001 to detect nearby objects behind it. The buzzer provides simple audio feedback, while the integrated display gives JBR-001 its animated face and heartbeat.
The camera adds another level of interaction, allowing the robot to go beyond simple distance sensing and respond to what it sees.
The three servo motors use an external power supply rather than drawing their power directly from the Arduino UNO Q. The external power supply and the UNO Q share a common ground, allowing the control signals from the board to reliably control the servos.
The camera and Arduino UNO Q are connected through a USB-C hub.

Main connections
- Head servo - pin 9
- Left arm servo - pin 10
- Right arm servo - pin 11
- Three servos - external power supply
- External power supply ground - Arduino UNO Q ground
- Modulino Distance - Modulino Buzzer
- Modulino Buzzer - Arduino UNO Q
- Arduino UNO Q - USB-C hub
- Camera - USB-C hub
Important: Do not power all three servo motors directly from the Arduino UNO Q. Use a suitable external power supply for the servos and connect the grounds of the external supply and UNO Q together.
Assembling the JBR-001
One of the goals behind JBR-001 was to make the robot something you could build yourself. The body is made from 3D-printable parts, with the electronics and servos installed directly inside the printed structure.
You will need:
- Arduino UNO Q
- Camera
- Modulino Distance
- Modulino Buzzer
- Three servo motors
- External power supply for the servos
- USB-C hub
- JBR-001 3D-printed parts
- Cables, screws, and mounting hardware
- All 3D-printable STL files for JBR-001 can be found here.
Start by printing the JBR-001 components and assembling the main body. Install the servo motors as you assemble the head and arms so that their shafts align correctly with the moving parts.
The head servo provides horizontal movement, while one servo inside each side of the body controls an arm.
Once the mechanical parts are assembled, install the Arduino UNO Q and route the servo, sensor, camera, and power cables through the body. Keeping the wiring organized at this stage makes closing the robot considerably easier.
The camera is installed in the head, giving it a forward-facing view of the environment.
The distance sensor is positioned so that JBR-001 can detect someone approaching from the back.
For a complete walkthrough of the mechanical assembly, watch the assembly video below, where Goran and Nenad explain it step by step.
JBR-001 - assembly instructions
Bringing the Head Display to Life
The display is one of the simplest ways to give JBR-001 some personality.
When the robot is idle, it displays a heart that continuously pulses between two sizes. This creates a subtle heartbeat animation and gives the robot a visual indication that it is running even when nothing else is happening.
The animation is created using two small bitmap representations of the heart: a larger heart and a smaller one.
// Small heart - flipped to match display orientation
uint8_t heartSmall[8][13] = {
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,1,0,0,0,0,0},
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,1,1,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,0,1,1,1,1,0,1,1,1,1,0,0},
{0,0,0,1,1,0,0,0,1,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0}
};
// Large heart - flipped to match display orientation
uint8_t heartLarge[8][13] = {
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,1,1,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,1,1,1,1,1,1,1,1,1,1,1,0},
{1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1},
{0,1,1,1,1,1,0,1,1,1,1,1,0},
{0,0,1,1,1,0,0,0,1,1,1,0,0}
};
Rather than stopping the entire program with long delays, the heartbeat can be updated based on elapsed time. This is important because JBR-001 still needs to monitor its distance sensor and respond to other events while the animation is running.
// Heartbeat animation
unsigned long heartbeatTimer = 0;
int heartbeatStep = 0;
void heartbeat() {
unsigned long now = millis();
switch (heartbeatStep) {
case 0:
matrix.renderBitmap(heartLarge, 8, 13);
heartbeatTimer = now;
heartbeatStep = 1;
break;
case 1:
if (now - heartbeatTimer >= 120) {
matrix.renderBitmap(heartSmall, 8, 13);
heartbeatTimer = now;
heartbeatStep = 2;
}
break;
case 2:
if (now - heartbeatTimer >= 100) {
matrix.renderBitmap(heartLarge, 8, 13);
heartbeatTimer = now;
heartbeatStep = 3;
}
break;
case 3:
if (now - heartbeatTimer >= 160) {
matrix.renderBitmap(heartSmall, 8, 13);
heartbeatTimer = now;
heartbeatStep = 4;
}
break;
case 4:
if (now - heartbeatTimer >= 700) {
heartbeatStep = 0;
}
break;
}
}
The display can easily be adapted for other expressions and states. For example, different graphics could indicate that JBR-001 has detected an object, recognized something with its camera, or is waiting for an interaction.
Adding Sound with the Buzzer
JBR-001 uses a buzzer to provide simple audio feedback. For example, when the robot detects someone nearby, it can play a short sequence of notes as a friendly greeting.
A simple greeting can be created with just four tones:
// Play JBR-001's friendly greeting
void playHello() {
buzzer.tone(523, 120); // C5
delay(150);
buzzer.tone(659, 120); // E5
delay(150);
buzzer.tone(784, 180); // G5
delay(210);
buzzer.tone(1047, 250); // C6
delay(270);
}
The ascending sequence gives JBR-001 a short and recognizable "hello" sound without requiring a speaker or audio files.
Sound can also be used to communicate different robot states. Different tone sequences could indicate successful detection, warnings, startup, or other events in your own applications.
Sensing Nearby Objects
JBR-001 uses a distance sensor located at the back of its head to sense nearby objects and measure how far away they are. The sensor continuously measures the distance behind the robot and makes this information available to your application.
Reading the distance sensor is straightforward. You can define a distance threshold and use it to trigger actions such as moving the head or arms, playing a sound, or changing the display animation.
This gives you another simple input for creating interactive behaviors and combining physical sensing with JBR-001's other capabilities.
// React when someone approaches
if (distanceSensor.available()) {
float distance = distanceSensor.get();
if (distance RESET_DISTANCE) {
objectDetected = false;
}
}
Controlling the Servos
Movement is provided by three servo motors. The head servo is connected to pin 9, while the two arm servos are connected to pins 10and 11.
Each servo can be controlled by specifying its target angle:
headServo.write(90);
From there, we can create more natural movement by moving between several positions rather than immediately jumping between large angles.
For example, JBR-001 can turn its head slightly toward a visitor while raising and lowering its arms as part of the greeting animation.
The servo motors require more current than should be supplied directly by the Arduino UNO Q, especially when several motors move at the same time. For this reason, all three servos are powered from an external power source.
The ground of the external servo power supply must also be connected to the ground of the Arduino UNO Q. Without this common electrical reference, the control signals sent from the UNO Q to the servos may not work reliably.
When creating your own animations, keep the mechanical limits of the robot in mind. Servo movement should remain within the range supported by the printed joints rather than automatically using the servo's entire theoretical 0° to 180° range.
Small movements often work better for JBR-001. A slight head turn or short arm movement can make the robot expressive without making the animation feel overly mechanical.
Adding Computer Vision
The camera is where JBR-001 starts becoming much more than a sensor-controlled desktop robot.
Connected to the Arduino UNO Q through the USB-C hub, the camera can provide images for computer vision applications running on the robot.

This opens up many possibilities. JBR-001 could recognize objects placed in front of it, react differently depending on what it sees, detect specific items, or combine visual information with its distance sensor to create more sophisticated interactions.
For example, instead of simply knowing that something is standing in front of the robot, computer vision can help JBR-001 understand what it is looking at.
That is also where synthetic data becomes particularly useful. We can use images for specific objects and scenarios, train a computer vision model, and deploy it to JBR-001 without first having to manually capture and label large numbers of images.
How Computer Vision Works on the JBR-001
The basic computer vision workflow on JBR-001 is straightforward. The camera captures images of the environment in front of the robot. These images are processed on the Arduino UNO Q using a trained computer vision model. The model identifies what it sees, and JBR-001 can use the result to decide what to do next.
The complete workflow looks like this:

Once an object has been detected, the result can be connected to any of JBR-001's physical outputs. The robot can move its head or arms, play a sound, change its display, or combine several actions into a single reaction.
This separation also makes JBR-001 easy to experiment with. The computer vision model determines what the robot can recognize, while your application determines how the robot reacts.
What Are We Going to Detect?
For our example, we trained JBR-001 to recognize different modules from the Arduino® Modulino™ family.
When a Modulino is placed in front of JBR-001's camera, the computer vision model analyses the image and identifies which Modulino it sees. The detection result can then be used by the application to trigger movement, sound, display animations, or other behaviors.
For this project, we used an Arduino Modulino dataset, which is publicly available on VisionDatasets.com

The dataset contains synthetic images of different Modulinos across a variety of positions, orientations, backgrounds, lighting conditions, and other visual variations designed to help the model recognize the objects when they are presented to the real camera.
The same workflow isn't limited to Modulinos. By changing the dataset and training a new model, JBR-001 can be taught to recognize completely different objects.
Importing the Dataset into Edge Impulse
For training and deploying our computer vision model, we use Edge Impulse.
Vision Datasets integrates directly with Edge Impulse, allowing the Arduino Modulino dataset to be transferred directly into an Edge Impulse project without manually downloading, organizing, and uploading the images.
Open the Arduino Modulino dataset on VisionDatasets.com and select Upload to Edge Impulse.

Provide the API key for your Edge Impulse project and select the number of images, image resolution, and Modulino classes you want to include.
Once the upload is complete, the images appear under Data Acquisitionin Edge Impulse, already labeled and ready to use.
You can find a complete walkthrough of the integration in the official Edge Impulse documentation.
Training the Model with Edge Impulse
With the Arduino Modulino dataset available in Edge Impulse, we can create the computer vision pipeline.
Start by opening Data Acquisitionand reviewing the imported images. Verify that the different Modulino classes are represented correctly and that the training and testing data look as expected.
Next, create an impulse and configure the image processing and learning blocks for the model. Edge Impulse then handles the training workflow using the images imported from Vision Datasets. During training, the model learns the visual characteristics that distinguish the different Modulinos from one another.
The goal isn't simply to recognize the synthetic images used during training. We want the model to generalize to images captured by the real camera installed in JBR-001.
This is where variation in the dataset becomes important. Different object positions, rotations, backgrounds, lighting conditions, and other variations expose the model to a wider range of appearances during training.
Once training is complete, use Edge Impulse's model testing tools to evaluate its performance on images that weren't used during training. The most important test, however, is putting a real Modulino in front of JBR-001 and seeing whether the model recognizes it.
Once we're satisfied with the model, we can deploy it to the robot.
Running the Model on the Arduino UNO Q
The next step is to run our trained Edge Impulse model on the Arduino UNO Q.
The camera installed in JBR-001's head provides the image input. The Arduino UNO Q runs inference using the trained model and returns the prediction to our application.

The process can run continuously, allowing JBR-001 to observe what is placed in front of it and react whenever it recognizes one of the Modulinos included in the model.
Connecting Computer Vision to JBR-001
Detecting an object is useful, but the project becomes much more interesting when the detection produces a physical reaction.
JBR-001 already gives us several ways to respond to what the camera sees:
- move the head
- move the left or right arm
- play a sound
- change the display animation
We can map the predictions returned by the computer vision model to these behaviors.
For example, when JBR-001 recognizes a Modulino, it could turn its head, raise its arms, play a short sound, and change the animation on its display. Different Modulinos could also trigger different reactions.
The model provides information about what exists in the physical environment, while the Arduino application translates that information into movement, sound, and visual feedback.
Build Your Own Computer Vision Application
The Arduino Modulino example is only a starting point. The same workflow can be used to teach JBR-001 to recognize completely different objects. You could experiment with recognizing tools, electronic components, everyday objects, or objects specific to your own project.
VisionDatasets.com provides ready-to-use computer vision datasets that can be imported directly into Edge Impulse, or you can create and use your own training data.
Once you've trained a different model, the physical JBR-001 platform doesn't need to change. You simply decide what the robot should recognize and what it should do when a detection occurs.
You can also combine multiple detections with different movements, sounds, and display animations to give JBR-001 completely new behaviours.
Conclusion
You now have the foundation of a working JBR-001.
We started with a set of 3D-printed parts and an Arduino UNO Q and turned them into a desktop robot that can display animations, detect distance of objects, make sounds, and move its head and arms.
JBR-001 is open source so that you can modify it, experiment with it, and make it your own. You can create new display animations, design different sounds and movements, add sensors, modify the 3D-printed parts, or completely change how the robot behaves.
And with the camera and computing capabilities of the Arduino UNO Q, you can also start building interactions based on what the robot sees.
Code
JBR-001 Source Code
GitHub repository with JBR-001 source code
Latest commit to the master branch on 25-09-2026
JBR-001 Source Code
cpp
head and arms movement, buzzer, distance, display
#include
#include
#include "Arduino_LED_Matrix.h"
// JBR-001 hardware
Servo headServo;
Servo leftArmServo;
Servo rightArmServo;
ModulinoBuzzer buzzer;
ModulinoDistance distanceSensor;
ArduinoLEDMatrix matrix;
// Servo pins
const int HEAD_SERVO_PIN = 9;
const int LEFT_ARM_SERVO_PIN = 10;
const int RIGHT_ARM_SERVO_PIN = 11;
// Servo resting positions
const int HEAD_CENTER = 90;
const int LEFT_ARM_CENTER = 90;
const int RIGHT_ARM_CENTER = 90;
// Distance detection
const float DETECTION_DISTANCE = 200.0;
const float RESET_DISTANCE = 250.0;
bool objectDetected = false;
// Small heart
// flipped to match display orientation
uint8_t heartSmall[8][13] = {
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,1,0,0,0,0,0},
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,1,1,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,0,1,1,1,1,0,1,1,1,1,0,0},
{0,0,0,1,1,0,0,0,1,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0}
};
// Large heart
// flipped to match display orientation
uint8_t heartLarge[8][13] = {
{0,0,0,0,1,1,1,1,1,0,0,0,0},
{0,0,0,1,1,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,1,1,0,0},
{0,1,1,1,1,1,1,1,1,1,1,1,0},
{1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1},
{0,1,1,1,1,1,0,1,1,1,1,1,0},
{0,0,1,1,1,0,0,0,1,1,1,0,0}
};
// Heartbeat animation
unsigned long heartbeatTimer = 0;
int heartbeatStep = 0;
void heartbeat() {
unsigned long now = millis();
switch (heartbeatStep) {
case 0:
matrix.renderBitmap(heartLarge, 8, 13);
heartbeatTimer = now;
heartbeatStep = 1;
break;
case 1:
if (now - heartbeatTimer >= 120) {
matrix.renderBitmap(heartSmall, 8, 13);
heartbeatTimer = now;
heartbeatStep = 2;
}
break;
case 2:
if (now - heartbeatTimer >= 100) {
matrix.renderBitmap(heartLarge, 8, 13);
heartbeatTimer = now;
heartbeatStep = 3;
}
break;
case 3:
if (now - heartbeatTimer >= 160) {
matrix.renderBitmap(heartSmall, 8, 13);
heartbeatTimer = now;
heartbeatStep = 4;
}
break;
case 4:
if (now - heartbeatTimer >= 700) {
heartbeatStep = 0;
}
break;
}
}
// Play JBR-001's friendly greeting
void playHello() {
buzzer.tone(523, 120); // C5
delay(150);
buzzer.tone(659, 120); // E5
delay(150);
buzzer.tone(784, 180); // G5
delay(210);
buzzer.tone(1047, 250); // C6
delay(270);
}
// Perform JBR-001's startup movement
void makeMovement() {
// Look left, right, then forward
headServo.write(70);
delay(400);
headServo.write(110);
delay(400);
headServo.write(HEAD_CENTER);
delay(400);
// Move both arms
leftArmServo.write(70);
rightArmServo.write(110);
delay(500);
leftArmServo.write(110);
rightArmServo.write(70);
delay(500);
// Return to resting position
leftArmServo.write(LEFT_ARM_CENTER);
rightArmServo.write(RIGHT_ARM_CENTER);
delay(500);
}
void setup() {
Modulino.begin();
buzzer.begin();
distanceSensor.begin();
matrix.begin();
// Show the heart while JBR-001 starts
matrix.renderBitmap(heartSmall, 8, 13);
// Move each servo to its resting position
headServo.attach(HEAD_SERVO_PIN);
headServo.write(HEAD_CENTER);
delay(400);
leftArmServo.attach(LEFT_ARM_SERVO_PIN);
leftArmServo.write(LEFT_ARM_CENTER);
delay(400);
rightArmServo.attach(RIGHT_ARM_SERVO_PIN);
rightArmServo.write(RIGHT_ARM_CENTER);
delay(400);
delay(500);
// Say hello and come to life
playHello();
makeMovement();
}
void loop() {
// Keep the heart beating
heartbeat();
// React when someone approaches
if (distanceSensor.available()) {
float distance = distanceSensor.get();
if (distance RESET_DISTANCE) {
objectDetected = false;
}
}
}
Downloadable files
JBR-001-schematics
JBR-001 wiring schematics
JBR-001-schematics.png

Documentation
JBR-001 Assembly Instructions
Assembly instructions PDF file.
JBR-001 Assembly Instructions.pdf
JBR-001 documentation
Assembly instructions




