GENESIS Air Mouse (Wired)

GENESIS Air Mouse (Wired)

Introduction

An air mouse is a simple way to explore motion-based control without extra hardware like cameras or infrared tracking. This project uses GENESIS to show how motion data from an accelerometer can be turned into real-time cursor movement, with haptic feedback and physical inputs for navigation. It is a compact example of combining sensing, feedback, and user interaction in one build.

 

What You'll Need

Assemble

Plug the modules into the genesis as shown bellow

  • LRA Motor -> Port 4
  • Accelerometer -> Port 3
  • Two Pad -> Port 8
  • Rotary Encoder -> Port 7

At the bottom of this page, you will find the code needed for this project. Copy and paste it into Arduino IDE and upload it to the GENESIS.

Understanding the Code and Usage

Core Components:

  • Motion Detection: The MPU6050 accelerometer reads gyroscope data to detect rotational movement in 3D space
  • Calibration: On startup, the device takes 1000 samples while stationary to establish baseline values and eliminate drift
  • USB HID Protocol: The ESP32-S3's native USB capabilities allow it to appear as a mouse without requiring drivers

How Movement Works: The code converts angular velocity from the gyroscope into cursor movement. When you rotate the device:

  • Pitch (forward/backward tilt) controls vertical cursor movement
  • Yaw (left/right rotation) controls horizontal cursor movement
  • Roll movement is ignored to prevent accidental cursor jumps

Sensitivity Control: Open the Serial Monitor in Arduino IDE (115200 baud rate) to adjust sensitivity in real-time:

  • Type s0.3 for high precision (slower movement)
  • Type s1.0 for balanced sensitivity
  • Type s2.0 for faster movement
  • Type help to see all commands

Input Mapping:

  • Left pad = Left mouse click
  • Right pad = Right mouse click
  • Rotary encoder = Scroll wheel (rotate to scroll up/down)
  • All inputs provide haptic feedback through the LRA motor

Usage Tips:

  • Hold the device comfortably and make smooth, deliberate movements
  • Small wrist rotations work better than large arm movements
  • The device works best when held horizontally
  • If the cursor drifts, place the device flat and restart to recalibrate

After uploading the code make sure you reset the board. And leave it flat and still for a few seconds until you hear a vibration. This means the accelerometer has successfully calibrated itself.

Now you can pick up your mouse and browse away!

NOTE: Because the code enables HID functionality, once its running, it is no longer in programming mode. If you wish to upload code again, press and hold the user button, then press reset button and release both. Now you can upload code again.

Back to blog
#include "USB.h"
#include "USBHIDMouse.h"
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <RotaryEncoder.h>
#include "Haptic_Driver.h"

// Pin definitions
#define LEFT_BUTTON_PIN 43
#define RIGHT_BUTTON_PIN 40
#define ENCODER_PIN1 9
#define ENCODER_PIN2 39
#define SDA_PIN 47
#define SCL_PIN 48

// Initialize objects
USBHIDMouse Mouse;
Adafruit_MPU6050 mpu;
RotaryEncoder encoder(ENCODER_PIN1, ENCODER_PIN2, RotaryEncoder::LatchMode::TWO03);
Haptic_Driver hapDrive;

// Mouse state variables
bool leftButtonPressed = false;
bool rightButtonPressed = false;
bool leftButtonPrevState = false;
bool rightButtonPrevState = false;

// Encoder variables
int lastEncoderPos = 0;

// MPU6050 variables
sensors_event_t accel, gyro, temp;
float gyroX, gyroY;
float sensitivity = 0.3; // Start with lower sensitivity - adjust this value!

// Calibration variables
float gyroXOffset = 0;
float gyroYOffset = 0;

// Timing variables
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 10; // 100Hz update rate

void setup() {
    Serial.begin(115200);
    
    // Initialize USB HID Mouse
    Mouse.begin();
    USB.begin();
    
    Serial.println("ESP32-S3 USB HID Air Mouse Starting...");
    
    // Initialize I2C
    Wire.begin(SDA_PIN, SCL_PIN);
    
    // Initialize MPU6050
    if (!initializeMPU6050()) {
        Serial.println("MPU6050 initialization failed!");
        while(1);
    }
    
    // Initialize Haptic Driver
    initializeHaptic();
    
    // Initialize pins
    pinMode(LEFT_BUTTON_PIN, INPUT_PULLUP);
    pinMode(RIGHT_BUTTON_PIN, INPUT_PULLUP);
    
    // Calibrate gyroscope
    calibrateGyro();
    
    Serial.println("USB HID Mouse ready!");
    triggerHaptic(127, 200);
}

void loop() {
    if (millis() - lastUpdate >= updateInterval) {
        lastUpdate = millis();
        
        updateMouseMovement();
        updateButtons();
        updateScroll();
        
        // Check for sensitivity adjustment commands via Serial
        checkSerialCommands();
    }
    
    delay(1);
}

bool initializeMPU6050() {
    if (!mpu.begin()) {
        return false;
    }
    
    Serial.println("MPU6050 Found!");
    mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
    mpu.setGyroRange(MPU6050_RANGE_500_DEG);
    mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
    Serial.println("MPU6050 configured");
    
    delay(100);
    return true;
}

void initializeHaptic() {
    hapDrive.begin();
    hapDrive.defaultMotor();
    hapDrive.enableFreqTrack(true);
    hapDrive.setOperationMode(DRO_MODE);
    hapDrive.clearIrq(hapDrive.getIrqEvent());
    Serial.println("Haptic driver initialized!");
}

void calibrateGyro() {
    Serial.println("Calibrating gyroscope... Keep device still!");
    
    float sumX = 0, sumY = 0;
    const int samples = 1000;
    
    for (int i = 0; i < samples; i++) {
        mpu.getEvent(&accel, &gyro, &temp);
        sumX += gyro.gyro.x;
        sumY += gyro.gyro.y;
        delay(2);
    }
    
    gyroXOffset = sumX / samples;
    gyroYOffset = sumY / samples;
    
    Serial.println("Calibration complete!");
    Serial.printf("Offsets - X: %.4f, Y: %.4f\n", gyroXOffset, gyroYOffset);
}

void updateMouseMovement() {
    mpu.getEvent(&accel, &gyro, &temp);
    
    gyroX = gyro.gyro.x - gyroXOffset;
    gyroY = gyro.gyro.y - gyroYOffset;
    
    // Apply deadzone
    if (abs(gyroX) < 0.05) gyroX = 0;
    if (abs(gyroY) < 0.05) gyroY = 0;
    
    // Convert to mouse movement
    int mouseX = (int)(-gyroY * 180.0 / PI * sensitivity);
    int mouseY = (int)(gyroX * 180.0 / PI * sensitivity);
    
    // Limit movement
    mouseX = constrain(mouseX, -127, 127);
    mouseY = constrain(mouseY, -127, 127);
    
    // Send mouse movement
    if (mouseX != 0 || mouseY != 0) {
        Mouse.move(mouseX, mouseY);
    }
}

void updateButtons() {
    bool leftCurrent = !digitalRead(LEFT_BUTTON_PIN);
    bool rightCurrent = !digitalRead(RIGHT_BUTTON_PIN);
    
    // Handle left button
    if (leftCurrent != leftButtonPrevState) {
        leftButtonPrevState = leftCurrent;
        
        if (leftCurrent) {
            Mouse.press(MOUSE_LEFT);
            triggerHaptic(127, 50);
            Serial.println("Left click");
        } else {
            Mouse.release(MOUSE_LEFT);
        }
    }
    
    // Handle right button  
    if (rightCurrent != rightButtonPrevState) {
        rightButtonPrevState = rightCurrent;
        
        if (rightCurrent) {
            Mouse.press(MOUSE_RIGHT);
            triggerHaptic(127, 50);
            Serial.println("Right click");
        } else {
            Mouse.release(MOUSE_RIGHT);
        }
    }
}

void updateScroll() {
    encoder.tick();
    
    int newPos = encoder.getPosition();
    int scrollDelta = newPos - lastEncoderPos;
    
    if (scrollDelta != 0) {
        lastEncoderPos = newPos;
        
        // Limit scroll speed
        scrollDelta = constrain(scrollDelta, -3, 3);
        
        // Send scroll
        Mouse.move(0, 0, scrollDelta);
        triggerHaptic(127, 30);
        
        Serial.printf("Scroll: %d\n", scrollDelta);
    }
}

void triggerHaptic(int intensity, int duration) {
    hapDrive.setVibrate(intensity);
    delay(duration);
    hapDrive.setVibrate(0);
}

void setSensitivity(float newSensitivity) {
    sensitivity = constrain(newSensitivity, 0.1, 10.0);
    Serial.printf("Sensitivity set to: %.1f\n", sensitivity);
}

void checkSerialCommands() {
    if (Serial.available()) {
        String command = Serial.readStringUntil('\n');
        command.trim();
        
        if (command.startsWith("s")) {
            // Extract number after 's' - e.g., "s2.5" sets sensitivity to 2.5
            float newSens = command.substring(1).toFloat();
            if (newSens > 0) {
                setSensitivity(newSens);
            }
        }
        else if (command == "help" || command == "?") {
            Serial.println("\n=== Air Mouse Commands ===");
            Serial.println("s<number> - Set sensitivity (e.g., s1.5, s3.0)");
            Serial.println("Current sensitivity: " + String(sensitivity));
            Serial.println("Range: 0.1 to 10.0");
            Serial.println("Lower = less sensitive, Higher = more sensitive");
        }
    }
}