Arduino HID Ultrasonic Volume Control (Media Controller)



Hello, welcome my friends! As we know, we use keyboards for a variety of tasks, but there are some keys that we use more frequently than others, particularly those for media control and adjusting brightness. However, there are times when we’re deeply focused on important work and don’t want to keep reaching for the keyboard to adjust the volume or skip a track. That’s where a custom, hands-free solution comes in handy!

I decided to create a media control system using the Arduino Pro Micro. This compact microcontroller is perfect for the job because it has built-in HID (Human Interface Device) capabilities, meaning it can easily function as a keyboard, mouse, or even a media controller without needing any extra software. Just plug it into your computer, and it’s ready to go!

To make this project even more intuitive and user-friendly, I integrated an ultrasonic sensor to detect hand gestures. By simply moving your hand closer or further away from the sensor, you can adjust the volume up or down, making it incredibly easy to control your media without even touching the keyboard. The sensor has a range of up to 60 centimetres, which allows for smooth and responsive control.

But I didn’t stop there! I also added touch-sensitive TP223 sensors for additional controls like skipping to the next track, going back to the previous one, and even muting or pausing the music with just a tap. These capacitive sensors are highly sensitive and work seamlessly with the Arduino Pro Micro.

To add a visual element, I incorporated a 16-bit Neopixel ring that provides beautiful light animations whenever you interact with the controls. For instance, when you skip to the next track, the ring lights up with a colourful rotating animation, making the experience even more engaging.

Finally, I included a 4-digit 7-segment TM1637 display to show the current volume level in real-time. This ensures you always know exactly how loud your music is without having to guess.

Overall, this project combines practicality with a touch of creativity, transforming a regular media control setup into an interactive and hands-free experience. Whether you’re deep into work or just lounging around, this Arduino-based media controller makes managing your audio a breeze!

Component Needed



Arduino Pro Micro: This is suitable for HID applications and its key features.

Ultrasonic Sensor: Describe how ultrasonic sensors works, their role in the project, and why you chose this specific model.

TP223 Touch Sensor: Explain how capacitive touch sensors function and their benefits in creating touch-based controls.

Neopixel Ring: Discuss the visual feedback provided by the Neopixel ring and how it enhances the user.

Other Components: Mention any resistors, wires, breadboard, or additional tools you used.

 

Circuit Diagram:

Here is a Circuit Diagram Provided with detailed instructions on how to connect each component, including wiring diagrams. You can test it on your breadboard or directly solder it to plane PCB board.


Offer Advice to carefully solder and avoid common mistakes, and ensuring stable connections.

 

Coding and Project:

Here is the code you can save or Copy from here:-

#include <HID-Project.h>                    // Include HID_Project library
#include <HID-Settings.h>
#include <NewPing.h>
#include <Adafruit_NeoPixel.h>              // Include Adafruit Neopixel library


// Define HID Consumer Control Codes for Brightness
#define CONSUMER_BRIGHTNESS_UP   0x006F
#define CONSUMER_BRIGHTNESS_DOWN 0x0070

#define TRIGGER_PIN 10
#define ECHO_PIN 9
#define MAX_DISTANCE 60 // Maximum distance we want to measure (in centimeters)

// Touch sensor pins
#define PREVIOUS_TOUCH_PIN 7
#define NEXT_TOUCH_PIN 14
#define PLAY_PAUSE_TOUCH_PIN 15
#define MUTE_TOUCH_PIN 16

// Define brightness control touch sensor pins
#define BRIGHTNESS_UP_PIN 8
#define BRIGHTNESS_DOWN_PIN 6

// Neopixel settings
#define NEOPIXEL_PIN 5
#define NUM_PIXELS 16
Adafruit_NeoPixel ring = Adafruit_NeoPixel(NUM_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

// Define the 8 corner LEDs indices
const int cornerLEDs[8] = {0, 2, 4, 6, 8, 10, 12, 14};

// Variables for continuous hue animation
unsigned long lastHueUpdate = 0;
const unsigned long hueUpdateInterval = 100; // Time between hue updates (in ms)
int hueContinuous = 0;                       // Current hue value
const uint8_t continuousBrightness = 100;     // Brightness level (0-255)


NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE);

int val = 0;
int previousval = -1;
int currentVolume = 0;
int smoothVal = 0;

// Variables for brightness control
int currentBrightness = 50;    // Current brightness level (0-100%)
int targetBrightness = 50;     // Target brightness level based on touch sensors
unsigned long lastBrightnessUpdate = 0;
const unsigned long brightnessCooldown = 100; // Cooldown time in ms between brightness adjustments
unsigned long lastDebounceTime_brightnessUp = 0;
unsigned long lastDebounceTime_brightnessDown = 0;
const unsigned long debounceDelay = 200; // 200ms debounce delay

void setup() {
  pinMode(PREVIOUS_TOUCH_PIN, INPUT);       // Set touch sensor pins as input
  pinMode(NEXT_TOUCH_PIN, INPUT);
  pinMode(PLAY_PAUSE_TOUCH_PIN, INPUT);
  pinMode(MUTE_TOUCH_PIN, INPUT);
  pinMode(BRIGHTNESS_UP_PIN, INPUT);
  pinMode(BRIGHTNESS_DOWN_PIN, INPUT);

  Consumer.begin();                         // Initialize computer connection
  ring.begin();                             // Initialize Neopixel ring
  ring.show();                              // Clear any previous data
  delay(1000);                              // Wait for computer to connect

  // Set initial volume to 0
  for (int a = 0; a < 50; a++) {
    Consumer.write(MEDIA_VOLUME_DOWN);
    delay(2);
  }
}

void loop() {

  unsigned long currentMillis = millis();
  unsigned long currentTime = millis();
  int distance = sonar.ping_cm();           // Read distance from ultrasonic sensor

  // Simple smoothing filter for ultrasonic sensor
  if (distance > 0 && distance <= MAX_DISTANCE) {
    smoothVal = (smoothVal * 3 + distance) / 4; // Average with previous values for smoothing
    val = map(smoothVal, 0, MAX_DISTANCE, 0, 100);  // Map smoothed distance to 0-100 range

    if (val != previousval) {               // Check if volume level has changed
      if (val > previousval) {
        // Increase volume slowly
        while (currentVolume < val) {
          Consumer.write(MEDIA_VOLUME_UP);
          currentVolume++;
          delay(50); // Slower delay for smoother volume control
        }
      } else {
        // Decrease volume slowly
        while (currentVolume > val) {
          Consumer.write(MEDIA_VOLUME_DOWN);
          currentVolume--;
          delay(50); // Slower delay for smoother volume control
        }
      }
      previousval = val;                    // Update previous value
    }
  }

  // Adjust brightness smoothly towards targetBrightness
  if (currentBrightness < targetBrightness && (currentMillis - lastBrightnessUpdate > brightnessCooldown)) {
    Consumer.write(CONSUMER_BRIGHTNESS_UP);
    currentBrightness++;
    lastBrightnessUpdate = currentMillis;
  }
  if (currentBrightness > targetBrightness && (currentMillis - lastBrightnessUpdate > brightnessCooldown)) {
    Consumer.write(CONSUMER_BRIGHTNESS_DOWN);
    currentBrightness--;
    lastBrightnessUpdate = currentMillis;
  }

  // Handle brightness touch sensors with debouncing
  handleBrightnessControls(currentMillis);

  delay(10); // Short delay to prevent overwhelming the loop

  // Handle Previous Media touch sensor activation (counterclockwise animation)
  if (digitalRead(PREVIOUS_TOUCH_PIN) == HIGH) {
    Consumer.write(MEDIA_PREV);
    animateRing(false);                     // Trigger ring animation for previous (counterclockwise)
    delay(500); // Debounce delay
  }

  // Handle Next Media touch sensor activation (clockwise animation)
  if (digitalRead(NEXT_TOUCH_PIN) == HIGH) {
    Consumer.write(MEDIA_NEXT);
    animateRing(true);                      // Trigger ring animation for next (clockwise)
    delay(500); // Debounce delay
  }

  // Handle Play/Pause touch sensor activation
  if (digitalRead(PLAY_PAUSE_TOUCH_PIN) == HIGH) {
    Consumer.write(MEDIA_PLAY_PAUSE);
    playPauseNeopixelEffect();
    delay(500); // Debounce delay
  }

  // Handle Mute touch sensor activation
  if (digitalRead(MUTE_TOUCH_PIN) == HIGH) {
    Consumer.write(MEDIA_VOLUME_MUTE);
    muteNeopixelEffect();                  // Trigger violet effect for mute
    delay(500); // Debounce delay
  }
  startUp();
  delay(100);                               // Adjust this delay for responsiveness
}

// Function to animate the Neopixel ring with hue colors
void animateRing(bool clockwise) {
  uint16_t startHue = 0;

  for (int i = 0; i < NUM_PIXELS; i++) {
    startHue += 65536 / NUM_PIXELS;         // Increment hue for each pixel (full rotation)
    int pixelIndex = clockwise ? i : (NUM_PIXELS - 1 - i);
    ring.setPixelColor(pixelIndex, ring.ColorHSV(startHue, 255, 255));
    ring.show();
    delay(50);                             // Delay between lighting each pixel
  }

  // Clear the ring after animation
  delay(300);
  ring.clear();
  ring.show();
}

// Function for Play/Pause Neopixel effect (Green for Play, Red for Pause)
void playPauseNeopixelEffect() {
  // Green for Play and Red for Pause (alternate for effect)
  for (int i = 0; i < 2; i++) {
    ring.fill(ring.Color(0, 255, 0));  // Green for Play
    ring.show();
    delay(200);
    ring.fill(ring.Color(255, 0, 0));  // Red for Pause
    ring.show();
    delay(200);
  }
  ring.clear();
  ring.show();
}

// Function for Mute Neopixel effect (Violet color)
void muteNeopixelEffect() {
  ring.fill(ring.Color(148, 0, 211));   // Violet color for mute
  ring.show();
  delay(500);
  ring.clear();
  ring.show();
}


// Function to handle brightness touch controls
void handleBrightnessControls(unsigned long currentMillis) {
  // Brightness Up Button
  if (digitalRead(BRIGHTNESS_UP_PIN) == HIGH && (currentMillis - lastDebounceTime_brightnessUp > debounceDelay)) {
    if (targetBrightness < 100) {
      targetBrightness += 1; // Increase brightness by 1%
      targetBrightness = constrain(targetBrightness, 0, 100);
    }
    lastDebounceTime_brightnessUp = currentMillis;
  }

  // Brightness Down Button
  if (digitalRead(BRIGHTNESS_DOWN_PIN) == HIGH && (currentMillis - lastDebounceTime_brightnessDown > debounceDelay)) {
    if (targetBrightness > 0) {
      targetBrightness -= 1; // Decrease brightness by 1%
      targetBrightness = constrain(targetBrightness, 0, 100);
    }
    lastDebounceTime_brightnessDown = currentMillis;
  }
}
/*
  // Function to set initial volume at startup
  void setInitialVolume(int initialVolume) {
  targetVolume = initialVolume;
  currentVolume = initialVolume; // Assume starting volume is already at initialVolume
  }
*/
// Function to set initial brightness at startup
void setInitialBrightness(int initialBrightness) {
  targetBrightness = initialBrightness;
  currentBrightness = initialBrightness; // Assume starting brightness is already at initialBrightness
}
void startUp() {
  unsigned long currentMillis = millis();

  // Check if it's time to update the hue
  if (currentMillis - lastHueUpdate >= hueUpdateInterval) {
    // Increment the hue value
    hueContinuous += 256; // Adjust this value to control hue change speed
    if (hueContinuous >= 65536) { // Hue range for ColorHSV is 0-65535
      hueContinuous = 0; // Reset hue after a full rotation
    }

    // Update each corner LED with a different hue to create a rotating effect
    for (int i = 0; i < 8; i++) {
      // Calculate hue offset for each LED
      int hueOffset = (hueContinuous + (i * 65536 / 8)) % 65536;

      // Convert HSV to RGB and set the pixel color
      uint32_t color = ring.ColorHSV(hueOffset, 255, continuousBrightness);
      ring.setPixelColor(cornerLEDs[i], color);
    }

    // Display the updated colors on the NeoPixel ring
    ring.show();

    // Update the last hue update time
    lastHueUpdate = currentMillis;

    // Optional: Debugging information
    // Serial.print("HueContinuous: ");
    // Serial.println(hueContinuous);
  }
}

 

Testing and Debugging

Testing your Arduino Pro Micro-based HID media control project is a crucial step to ensure everything works as intended. Begin by verifying each component individually. Start with the ultrasonic sensor by checking if it accurately detects the distance of your hand. You can use a simple serial monitor output to display the distance readings, ensuring they match your expectations within the 0-60 cm range. If the sensor readings fluctuate wildly, consider adding a slight delay or using a moving average filter to stabilize the output.

Next, test the TP223 touch sensors by confirming that each sensor triggers the intended media control action (e.g., Play/Pause, Next, Previous, Mute) without delay or false triggers. If a sensor is unresponsive, double-check the wiring and ensure the sensitivity is correctly adjusted.

Once the sensors are working, move on to testing the HID functionality. Ensure that the Arduino correctly sends media control commands to your computer. Use a simple test code to increase and decrease the volume, and check if it responds smoothly and accurately. If the volume control is too sensitive or not responsive enough, you may need to adjust the mapping of sensor readings to volume levels or introduce a small delay between commands to avoid overwhelming the system.

After the basic functions are working, test the Neopixel ring to ensure it provides the correct visual feedback. If the colours are off or the animations are not smooth, review your code for any timing issues or incorrect settings in the Neopixel library.

Finally, run the entire system together and observe its performance. Look for any delays, incorrect outputs, or unexpected behaviour. Debugging may involve tweaking sensor sensitivity, adjusting code logic, or improving power supply stability to ensure a smooth and reliable user experience.

 

Conclusion



Creating your own HID media control system with Arduino Pro Micro is more than just a technical project—it's a step towards a more personalized, efficient workspace. This project not only solves a common problem but also gives you the satisfaction of crafting a tool tailored to your needs. The flexibility to control your media with simple hand gestures or a tap adds a layer of convenience that’s hard to match with standard devices. Plus, the vibrant Neopixel feedback adds a bit of flair, making your setup as visually engaging as it is functional. Dive into this project, and transform how you interact with your media!

 

 


Comments