He likes your company… just not too close, and definitely not too loud.

We all know someone who enjoys company but needs their space. Dobby the IntroBOT is a playful desk companion built around that feeling. Inspired by introverts, who value personal space and quiet surroundings, Dobby watches how people approach it and listens to the room, then shows how it feels through an animated face and sound. Approach calmly and it stays happy. Come closer slowly or make some noise and it becomes uneasy. Rush in or shout, and it gets worried and sounds an alarm.

Beyond a desk toy, Dobby explores a bigger design idea: turning everyday architectural elements into responsive, human-centred interfaces that make people more aware of each other’s comfort.

COMPONENTS (BOM)

HOW IT WORKS

An ultrasonic sensor measures distance many times per second. By comparing each reading with the previous one and the time between them, Dobby works out how fast someone is approaching. It only counts movement within 4 m and only when the distance is shrinking. A sound sensor listens for 50 ms at a time and measures the gap between the loudest and quietest values, which gives a stable volume level. Both values are smoothed with a moving average of the last five readings, so one noisy reading can’t change Dobby’s mood.

CIRCUIT DIAGRAM

The Arduino then decides how Dobby feels. A slow approach (10–40 cm/s) or moderate noise makes it Sad, with a low 200 Hz tone. A fast approach (over 40 cm/s) or loud noise makes it Worried, with a sharp 800 Hz tone. Otherwise it stays Happy and quiet. The face is drawn on the OLED from simple lines and circles, and it blinks, glances around and gently bobs, so Dobby always feels alive.

LOGIC FLOW CHART

MAIN CODE

//Ultrasound Library

#include “Ultrasonic.h”          // CM: using the library again

//Face Library

#include <Wire.h>

#include “Faces.h”

Ultrasonic ultrasonic(6);        // CM: sensor on D6

#define BUZZER_PIN 5

#define SOUND_PIN A2

#define MAX_RANGE 400     // detection range in cm

#define WINDOW_SIZE 5    // average size

#define FAST_SPEED 40    //

#define SLOW_SPEED 25     //

long prevDist = 0;              // previous distance

unsigned long prevTime = 0;     // time of previous distance

long speedSamples[WINDOW_SIZE] = { 0 };  // moving average buffer

long speedSmoothed = 0;

long soundSamples[WINDOW_SIZE] = { 0 };  // moving average buffer

long soundSmoothed = 0;

long nSamples = 0;

Emotion mood = HAPPY;

long readSoundLevel() {

  unsigned long start = millis();

  int highest = 0;

  int lowest = 1023;

  while (millis() – start < 50) {

    int value = analogRead(SOUND_PIN);

    if (value > highest) highest = value;

    if (value < lowest)  lowest = value;

  }

  return highest – lowest;

}

void setup() {

  Serial.begin(9600);

  pinMode(BUZZER_PIN, OUTPUT);

  pinMode(SOUND_PIN, INPUT);

  Wire.begin();

  faceBegin(0x3D);     // start the face screen

}

void loop() {

  long currdist = ultrasonic.MeasureInCentimeters();   // CM: measuring distance in cm

  unsigned long currtime = millis();

  long speed = 0;

  bool inRange = (currdist > 0 && currdist <= MAX_RANGE && prevDist > 0 && prevDist <= MAX_RANGE);   //checking if object is out of range

  if (inRange) {

    long diff = prevDist – currdist;          // distance moved

    unsigned long timeframe = currtime – prevTime; //  time passed between movement

    if (diff > 0 && timeframe > 0) {

      speed = diff * 1000 / (long)timeframe; // Ccm/s

    }

  }

 long sound = readSoundLevel();                

  soundSamples[nSamples % WINDOW_SIZE] = sound;

  soundSmoothed = 0;

  //Smoothening the speed

  speedSamples[nSamples % WINDOW_SIZE] = speed;

  nSamples += 1;

  speedSmoothed = 0;

  for (int i = 0; i < WINDOW_SIZE; i++) {

    speedSmoothed += speedSamples[i];

    soundSmoothed += soundSamples[i];

  }

  speedSmoothed = speedSmoothed / WINDOW_SIZE;

  soundSmoothed = soundSmoothed / WINDOW_SIZE;

  soundSmoothed = map(soundSmoothed, 150, 600, 0, 100);

  // Overwriting as “previous” for next loop

  prevDist = currdist;

  prevTime = currtime;

  // Printing

  Serial.print(“distance:”);

  Serial.print(currdist);

  Serial.print(” speed:”);

  Serial.println(speedSmoothed);

  Serial.print(” sound:”);

  Serial.println(soundSmoothed);

  // Reaction

  if ((speedSmoothed > 10 && speedSmoothed <= FAST_SPEED) || (soundSmoothed > 35 && soundSmoothed < 75)) { // slow approach

    tone(BUZZER_PIN, 200);

    mood = SAD;

  } else if (speedSmoothed > FAST_SPEED || soundSmoothed > 60) { // fast approach

    tone(BUZZER_PIN, 800);

    mood = WORRIED;

  } else {

    noTone(BUZZER_PIN);    // away

    mood = HAPPY;

  }

  drawFace(mood);   // showing the mood

  delay(10);

}

FACIAL EXPRESSION CODE

#include “Faces.h”

#include <Wire.h>

#include <Adafruit_GFX.h>

#include <Adafruit_SSD1306.h>

#include <math.h>

#define FACE_WIDTH  128

#define FACE_HEIGHT 64

// The display object lives here, hidden from the main sketch

static Adafruit_SSD1306 face(FACE_WIDTH, FACE_HEIGHT, &Wire, -1);

// Eye positions shared by every emotion

static const int LEFT_X  = 39;

static const int RIGHT_X = 89;

static void drawEye(int x, int y, int pupilX, int pupilY,

                    int eyeWidth, int eyeHeight) {

  face.fillRoundRect(x – eyeWidth / 2, y – eyeHeight / 2,

                     eyeWidth, eyeHeight, eyeHeight / 2,

                     SSD1306_WHITE);

  face.fillCircle(pupilX, pupilY, 4, SSD1306_BLACK);

  face.fillCircle(pupilX – 1, pupilY – 1, 1, SSD1306_WHITE);

}

// One function per emotion

// lookX = pupil wobble, eyeY = eye bob, eyeH = blink height

static void drawHappy(int lookX, int eyeY, int eyeH) {

  drawEye(LEFT_X,  eyeY, LEFT_X  + lookX, eyeY, 24, eyeH);

  drawEye(RIGHT_X, eyeY, RIGHT_X + lookX, eyeY, 24, eyeH);

  // Eyebrows

  face.drawLine(27, 13, 46, 10, SSD1306_WHITE);

  face.drawLine(80, 10, 99, 13, SSD1306_WHITE);

  // Smile

  face.drawLine(43, 42, 50, 49, SSD1306_WHITE);

  face.drawLine(50, 49, 78, 49, SSD1306_WHITE);

  face.drawLine(78, 49, 85, 42, SSD1306_WHITE);

  // Cheeks

  face.drawPixel(25, 42, SSD1306_WHITE);

  face.drawPixel(103, 42, SSD1306_WHITE);

}

static void drawSad(int lookX, int eyeY, int eyeH) {

  drawEye(LEFT_X,  eyeY + 2, LEFT_X  + lookX, eyeY + 2, 24, eyeH);

  drawEye(RIGHT_X, eyeY + 2, RIGHT_X + lookX, eyeY + 2, 24, eyeH);

  // Eyebrows

  face.drawLine(27, 10, 47, 16, SSD1306_WHITE);

  face.drawLine(81, 16, 101, 10, SSD1306_WHITE);

  // Frown

  face.drawLine(44, 51, 52, 44, SSD1306_WHITE);

  face.drawLine(52, 44, 76, 44, SSD1306_WHITE);

  face.drawLine(76, 44, 84, 51, SSD1306_WHITE);

  // Tears

  face.drawLine(29, 35, 27, 42, SSD1306_WHITE);

  face.drawLine(99, 35, 101, 42, SSD1306_WHITE);

}

static void drawWorried(int lookX, int eyeY) {

  // Wide open eyes (no blink)

  face.drawCircle(LEFT_X,  eyeY, 13, SSD1306_WHITE);

  face.drawCircle(RIGHT_X, eyeY, 13, SSD1306_WHITE);

  face.fillCircle(LEFT_X  + lookX, eyeY, 4, SSD1306_WHITE);

  face.fillCircle(RIGHT_X + lookX, eyeY, 4, SSD1306_WHITE);

  // Eyebrows

  face.drawLine(26, 10, 46, 15, SSD1306_WHITE);

  face.drawLine(81, 14, 101, 7, SSD1306_WHITE);

  // Nervous mouth

  face.drawRoundRect(53, 42, 22, 13, 5, SSD1306_WHITE);

  face.drawLine(57, 47, 71, 47, SSD1306_WHITE);

  // Sweat marks

  face.drawLine(15, 25, 20, 30, SSD1306_WHITE);

  face.drawLine(108, 30, 113, 25, SSD1306_WHITE);

}

// Public functions (declared in Faces.h)

bool faceBegin(uint8_t address) {

  if (!face.begin(SSD1306_SWITCHCAPVCC, address)) {

    return false;

  }

  face.clearDisplay();

  face.display();

  return true;

}

void drawFace(Emotion emotion) {

  face.clearDisplay();

  // Shared animation: pupils drift, eyes bob, regular blink

  unsigned long t = millis();

  int lookX = (int)(sin(t / 250.0) * 4);

  int eyeY  = 24 + (int)(sin(t / 350.0) * 2);

  bool blinking = (t % 3200 < 130);

  int eyeH = blinking ? 3 : 18;

  switch (emotion) {

    case HAPPY:   drawHappy(lookX, eyeY, eyeH); break;

    case SAD:     drawSad(lookX, eyeY, eyeH);   break;

    case WORRIED: drawWorried(lookX, eyeY);     break;

  }

  face.display();

}

FACE DEFINITION

// Faces.h — animated robot face on an SSD1306 OLED

#ifndef FACES_H

#define FACES_H

#include <Arduino.h>

// The emotions the robot can show.

// To add a new one (e.g. SCARED), add it here AND

// write a drawXxx() function in Faces.cpp.

enum Emotion {

  HAPPY,

  SAD,

  WORRIED,

  ANGRY,

  CURIOUS

};

// Start the face screen. Returns false if the OLED isn’t found.

bool faceBegin(uint8_t address = 0x3C);

// Draw one animation frame of the given emotion.

// Call this repeatedly in loop() so the eyes move and blink.

void drawFace(Emotion emotion);

#endif

PROTOTYPE

DEMONSTRATION VIDEO

NEXT STEPS

Right now, Dobby can only show how it feels. The next step is to let it act on that feeling: with wheels and DC motors, the robot will back away when someone approaches too fast or makes too much noise, keeping its personal space instead of only reacting on its face.

In the long run, the same behaviour could move into smart furniture: desks, chairs or booths that sense crowding and noise and quietly signal when someone needs space, turning a small robot into a responsive part of the built environment.