Ashish Joy
Published © CC BY-NC-SA

TouchNav - Spotify Control HID Touchpad

SAMD21-based USB HID controller for Spotify—play, pause, skip & adjust volume with touch and rotary input.

IntermediateFull instructions provided4 hours28
TouchNav - Spotify Control HID Touchpad

Things used in this project

Hardware components

Microchip SAMD21E17A MCU
×1
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×1
WS2812B LED
×5
SMD LED 1206
×2
3.3V 1A Voltage Regulator LDO
×1
BSS138 N Channel Mosfet
×1
ProshPlay Type C - Breakout Boar
×1
SMD Resistor 49.9 ohm, 1206
×1
SMD Resistor 10K ohm, 1206
×4
SMD Resistor 10 ohm, 1206
×1
SMD Capacitor 10uF, 1206
×2
SMD Resistor 0 ohm, 1206
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

PCB KiCad Schematic

Code

Spotify HID Controller

Arduino
#include <HID-Project.h>
#include <HID-Settings.h>

#include "Adafruit_FreeTouch.h"
#include "Adafruit_NeoPixel.h"

Adafruit_FreeTouch t0 = Adafruit_FreeTouch(2, OVERSAMPLE_64, RESISTOR_0, FREQ_MODE_NONE);
Adafruit_FreeTouch t1 = Adafruit_FreeTouch(3, OVERSAMPLE_64, RESISTOR_0, FREQ_MODE_NONE);
Adafruit_FreeTouch t2 = Adafruit_FreeTouch(4, OVERSAMPLE_64, RESISTOR_0, FREQ_MODE_NONE);
Adafruit_FreeTouch t3 = Adafruit_FreeTouch(5, OVERSAMPLE_64, RESISTOR_0, FREQ_MODE_NONE);
Adafruit_FreeTouch t4 = Adafruit_FreeTouch(6, OVERSAMPLE_64, RESISTOR_0, FREQ_MODE_NONE);

// Rotary Encoder Inputs
#define CLK 9
#define DT 8
#define SW 10
#define PIN 11 

#define NUMPIXELS 5 
#define IDLE_TIME 3000  // Time in milliseconds to wait before idle effect


int lastDirection = 0;  // 1 = Right, -1 = Left
int isPlaying = 1;

int counter = 0;
int currentStateCLK;
int lastStateCLK;
unsigned long lastButtonPress = 0;

int baseline[5] = {1e6, 1e6, 1e6, 1e6, 1e6};

Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

unsigned long lastActivityTime = 0;
bool isIdle = true;

void setup() {
	pinMode(CLK, INPUT);
	pinMode(DT, INPUT);
	pinMode(SW, INPUT_PULLUP);

	Consumer.begin();
	Keyboard.begin();
	Consumer.write(MEDIA_STOP);
	SerialUSB.begin(9600);

	pixels.begin();

	// Initialize FreeTouch sensors
	t0.begin(); t1.begin(); t2.begin(); t3.begin(); t4.begin();

	// Calibrate baseline
	for (int i = 0; i < 100; i++) {
		baseline[0] = min(baseline[0], t0.measure());
		baseline[1] = min(baseline[1], t1.measure());
		baseline[2] = min(baseline[2], t2.measure());
		baseline[3] = min(baseline[3], t3.measure());
		baseline[4] = min(baseline[4], t4.measure());
		delay(10);
	}

	lastStateCLK = digitalRead(CLK);
}

void showFlow(int direction) {
	isIdle = false;
	lastActivityTime = millis();

	for (int i = 0; i < NUMPIXELS; i++) {
		pixels.clear();

		for (int j = 0; j <= i; j++) {
			int index = (direction == 1) ? j : (NUMPIXELS - 1 - j);

			if (direction == 1) {  
				pixels.setPixelColor(index, pixels.Color(0, 255 - (j * 50), 0));  // Green Flow (Right)
			} else {  
				pixels.setPixelColor(index, pixels.Color(255 - (j * 50), 0, 0));  // Red Flow (Left)
			}
		}
		
		pixels.show();
		delay(50);
	}
}

void openSpotify() {
	Keyboard.press(KEY_LEFT_GUI); // Press Windows key (for Windows) or Command key (for Mac)
	Keyboard.press('r');  // Press 'R' to open Run dialog (Windows only)
	delay(200);
	Keyboard.releaseAll();

	delay(200);
	Keyboard.print("spotify");  // Type "spotify"
	delay(200);
	Keyboard.press(KEY_RETURN);  // Press Enter
	Keyboard.releaseAll();

	
	
}

void showIdleEffect() {
	static int brightness = 0;
	static int fadeDirection = 1;

	brightness += fadeDirection * 5;
	if (brightness >= 150 || brightness <= 0) {
		fadeDirection *= -1;
	}

	for (int i = 0; i < NUMPIXELS; i++) {
		if (isPlaying) {
			pixels.setPixelColor(i, pixels.Color(0, brightness, 0));  // Green when playing
		} else {
			pixels.setPixelColor(i, pixels.Color(300, 0, 0));  // Purple when paused
		}
	}

	pixels.show();
	delay(50);
}

void loop() {
	pixels.clear();

	currentStateCLK = digitalRead(CLK);

	if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {
		if (digitalRead(DT) != currentStateCLK) {
			counter++;
			Consumer.write(MEDIA_VOLUME_UP);
			SerialUSB.println("Volume UP");
			lastDirection = 1;
			showFlow(lastDirection);
		} else {
			counter--;
			Consumer.write(MEDIA_VOLUME_DOWN);
			SerialUSB.println("Volume DOWN");
			lastDirection = -1;
			showFlow(lastDirection);
		}
	}

	lastStateCLK = currentStateCLK;

	int btnState = digitalRead(SW);

	if (btnState == LOW) {
		if (millis() - lastButtonPress > 200) {
			SerialUSB.println("Play/Pause");
			Consumer.write(MEDIA_PLAY_PAUSE);
		}
		lastButtonPress = millis();
	}

	int touchValues[5] = {
		t0.measure() - baseline[0],
		t1.measure() - baseline[1],
		t2.measure() - baseline[2],
		t3.measure() - baseline[3],
		t4.measure() - baseline[4]
	};

	if (touchValues[0] > 9) {
		Consumer.write(MEDIA_REWIND);
		SerialUSB.println("Rewind");
		delay(1000);
	}

	if (touchValues[1] > 9) {
		Consumer.write(MEDIA_PREVIOUS);
		SerialUSB.println("Previous");
		delay(1000);
	}

static unsigned long touchStartTime = 0;
static bool touchHeld = false;
static bool isSpotifyOpen = false;  // Track Spotify state

if (touchValues[2] > 9) {
	if (!touchHeld) {
		touchStartTime = millis();  // Start timer
		touchHeld = true;
	}

	if (millis() - touchStartTime > 2000) {  // If held for more than 2 seconds
		if (isSpotifyOpen) {
			Keyboard.press(KEY_LEFT_GUI);
		Keyboard.press('r');  // Open Run
		delay(200);
		Keyboard.releaseAll();

		delay(200);
		Keyboard.print("taskkill /IM spotify.exe /F");
		delay(200);
		Keyboard.press(KEY_RETURN);
		Keyboard.releaseAll();
		} else {
			SerialUSB.println("Opening Spotify");
			openSpotify();
		}
		
		isSpotifyOpen = !isSpotifyOpen;  // Toggle state
		touchHeld = false;  // Reset flag to prevent repeated triggers
	}
} else if (touchHeld) {
	// If released before 2 seconds, Play/Pause
	if (millis() - touchStartTime <= 2000) {
		isPlaying = !isPlaying;
		Consumer.write(MEDIA_PLAY_PAUSE);
		SerialUSB.println("Play/Pause");
	}
	touchHeld = false;  // Reset flag
}

	if (touchValues[3] > 9) {
		Consumer.write(MEDIA_NEXT);
		SerialUSB.println("Next");
		delay(1000);
	}

	if (touchValues[4] > 9) {
		Consumer.write(MEDIA_FAST_FORWARD);
		SerialUSB.println("Fast Forward");
		delay(500);
		if (touchValues[4] > 9) {
			Consumer.write(MEDIA_FAST_FORWARD);
		}
	}

	delay(5);

	if (millis() - lastActivityTime > IDLE_TIME) {
		isIdle = true;
		lastDirection = 0;  // **Fix: Reset direction when going idle**
	}

	if (isIdle) {
		showIdleEffect();
	}
}

Credits

Ashish Joy
1 project • 3 followers
Mechatronics engineer | Exploring the intersection of design, code & machines

Comments