About Me Weekly Assignments Final Project
Kevin J Jijo
Final Project

Sweep — Automated Track Selection for Vinyl Records

Presentation Slide

Final Presentation Slide

Presentation Video

About the Project

Week 1 — Initial Sketch

Early sketch of the concept before the design was refined.

Week 1 Sketch Week 1 Sketch

View Week 1 documentation →

Week 2 — Possible Design

Early design direction explored during Week 2.

Week 2 Possible Design

View Week 2 documentation →

Project Documentation

System Integration
User flow, mechanism and PCB integration, overall system architecture.
Open documentation →
Applications and Implications
Inspiration, sources, what's planned, what's done.
Open documentation →
Project Development
Design progression, electronics integration, prototyping and iterative development.
Open documentation →

Features

Feature 1 Feature 1

PCB

3D Rendered PCB

Web App Screenshot

Groove Detection Web App Screenshot

Web app: fabsweep.web.app

Final Design Render

Final Fusion 360 Render

Final Build

Final Assembled Vinyl Player

Firmware

/* * Vinyl Record Selector — ALL-IN-ONE * Seeed XIAO ESP32-C6 * * Wiring: * Encoder A → D1 (GPIO1) * Encoder B → D2 (GPIO2) * Encoder SW → D3 (GPIO21) * OLED SDA → D4 (GPIO22) * OLED SCL → D5 (GPIO23) * Solenoid → D6 (GPIO16) * Servo Move → D7 (GPIO17) * Servo Lift → D8 (GPIO19) * * Libraries needed: * - Adafruit SSD1306 * - Adafruit GFX Library * - ESP32Servo * * ENCODER: CCW = increment track * * SONG TABLE: * Song 1 → 82° Song 2 → 79° Song 3 → 76° * Song 4 → 72° Song 5 → 68° Song 6 → 65° */ #include #include #include #include // ============================================================================= // PINS // ============================================================================= #define ENC_A 1 // D1 #define ENC_B 2 // D2 #define ENC_SW 21 // D3 // SDA = 22 (D4), SCL = 23 (D5) — handled by Wire.begin() #define SOLENOID_PIN 16 // D6 #define SERVO_MOVE 17 // D7 #define SERVO_LIFT 19 // D8 // ============================================================================= // DISPLAY // ============================================================================= #define SCREEN_W 128 #define SCREEN_H 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RESET); // ============================================================================= // SERVOS // ============================================================================= Servo servoLift; Servo servoMove; // ============================================================================= // MOTION LIMITS // ============================================================================= #define LIFT_UP 135.0f #define LIFT_DOWN 100.0f #define MOVE_MIN 50 #define MOVE_MAX 120 #define MOVE_MIN_DEGREE 57 // arm at vinyl record start #define MOVE_MAX_DEGREE 82 // arm at vinyl record end #define PARK 100.0f #define SEARCH_MIN 52 #define SERVO_MIN_US 500 #define SERVO_MAX_US 2500 #define STEP_DELAY 20 float liftPos = LIFT_DOWN; float movePos = PARK; // ============================================================================= // SONG TABLE — 6 songs, angle in degrees // ============================================================================= #define NUM_TRACKS 6 const char* trackNames[NUM_TRACKS] = { "Side A-1", "Side A-2", "Side A-3", "Side B-1", "Side B-2", "Side B-3", }; const float trackAngles[NUM_TRACKS] = { 82.0f, 79.0f, 76.0f, 72.0f, 68.0f, 65.0f }; // ============================================================================= // VINYL DISPLAY GEOMETRY // ============================================================================= #define VX 36 #define VY 30 #define LABEL_R 8 #define OUTER_R 28 #define RING_GAP 3 // ============================================================================= // UI STATE // ============================================================================= enum UIState { UI_IDLE, UI_CONFIRM, UI_PLAYING }; UIState uiState = UI_IDLE; uint32_t confirmMs = 0; #define CONFIRM_TIMEOUT 3000 // ms before confirm auto-cancels int currentTrack = 0; int playingTrack = -1; // ============================================================================= // SPIN ANIMATION // ============================================================================= float spinAngle = 0.0f; float spinSpeed = 0.8f; bool isSpinning = false; uint32_t spinStartMs = 0; #define SPIN_DURATION_MS 1200 #define SPIN_MAX_SPEED 8.0f // ============================================================================= // ENCODER — polled, rising edge only (matches working test sketch) // CCW = increment, CW = decrement // Sensitivity: only act every ENC_STEP pulses to reduce over-sensitivity // ============================================================================= int lastEncA = HIGH; int encPulses = 0; // accumulated pulses before registering a step #define ENC_STEP 2 // raise this (3,4…) if still too sensitive uint32_t lastBtnMs = 0; bool lastBtnState = HIGH; #define BTN_DEBOUNCE 200 // ============================================================================= // SERVO HELPERS // ============================================================================= void writeServoFloat(Servo &s, float angle) { angle = constrain(angle, 0.0f, 180.0f); int us = map((int)(angle * 100), 0, 18000, SERVO_MIN_US, SERVO_MAX_US); s.writeMicroseconds(us); } void moveLift(bool up) { float target = up ? LIFT_UP : LIFT_DOWN; while (abs(liftPos - target) > 0.1f) { liftPos += (liftPos < target) ? 0.2f : -0.2f; servoLift.write((int)liftPos); delay(STEP_DELAY); } liftPos = target; servoLift.write((int)liftPos); } void moveArmRaw(float target) { target = constrain(target, (float)MOVE_MIN, (float)MOVE_MAX); while (abs(movePos - target) > 0.1f) { movePos += (movePos < target) ? 0.2f : -0.2f; writeServoFloat(servoMove, movePos); delay(STEP_DELAY); } movePos = target; writeServoFloat(servoMove, movePos); } // ============================================================================= // MOTION SEQUENCES // ============================================================================= void restSequence() { Serial.println("REST"); digitalWrite(SOLENOID_PIN, HIGH); moveArmRaw(PARK); digitalWrite(SOLENOID_PIN, LOW); Serial.println("REST DONE"); } void searchSequence() { Serial.println("SEARCH START"); digitalWrite(SOLENOID_PIN, HIGH); moveArmRaw(PARK); digitalWrite(SOLENOID_PIN, LOW); moveLift(true); moveArmRaw(SEARCH_MIN); moveArmRaw(PARK); digitalWrite(SOLENOID_PIN, LOW); moveLift(false); Serial.println("SEARCH DONE"); } // Lift → move to groove angle → drop. Needle stays on record. void seekToAngle(float target) { target = constrain(target, (float)MOVE_MIN_DEGREE, (float)MOVE_MAX_DEGREE); Serial.print("SEEK → "); Serial.println(target); moveLift(true); while (abs(movePos - target) > 0.1f) { movePos += (movePos < target) ? 0.2f : -0.2f; writeServoFloat(servoMove, movePos); delay(STEP_DELAY); } movePos = target; writeServoFloat(servoMove, movePos); delay(150); moveLift(false); Serial.println("SEEK DONE"); } // Full play: search → settle → seek void playTrack(int trackIndex) { float angle = trackAngles[trackIndex]; Serial.printf("PLAY track %d at %.0f deg\n", trackIndex + 1, angle); searchSequence(); delay(300); seekToAngle(angle); Serial.println("PLAY COMPLETE"); } // ============================================================================= // OLED DRAW HELPERS // ============================================================================= void drawThickCircle(int cx, int cy, int r, int thickness, uint16_t colour) { for (int t = 0; t < thickness; t++) display.drawCircle(cx, cy, r - t, colour); } void drawGrooveTick(int cx, int cy, float angleDeg, int r1, int r2) { float rad = angleDeg * PI / 180.0f; display.drawLine( cx + (int)(r1 * cos(rad)), cy + (int)(r1 * sin(rad)), cx + (int)(r2 * cos(rad)), cy + (int)(r2 * sin(rad)), SSD1306_WHITE ); } void drawVinyl() { // Solid disc for (int r = OUTER_R; r >= 0; r--) display.drawCircle(VX, VY, r, SSD1306_WHITE); // Erase inner then re-draw faint grooves for (int r = LABEL_R + 1; r <= OUTER_R - 1; r++) display.drawCircle(VX, VY, r, SSD1306_BLACK); for (int r = LABEL_R + 2; r < OUTER_R - 1; r += 2) display.drawCircle(VX, VY, r, SSD1306_WHITE); // Track rings — highlighted ring = current selection for (int t = 0; t < NUM_TRACKS; t++) { int r = OUTER_R - t * RING_GAP; if (r <= LABEL_R) break; if (t == currentTrack) { drawThickCircle(VX, VY, r, 2, SSD1306_WHITE); drawThickCircle(VX, VY, r - 3, 2, SSD1306_WHITE); } else { display.drawCircle(VX, VY, r, SSD1306_WHITE); } } // Centre label for (int r = LABEL_R; r >= 0; r--) display.drawCircle(VX, VY, r, (r == LABEL_R) ? SSD1306_WHITE : SSD1306_BLACK); display.fillCircle(VX, VY, 1, SSD1306_WHITE); // Spin tick drawGrooveTick(VX, VY, spinAngle, LABEL_R + 1, LABEL_R + 3); } void drawTrackInfo() { display.drawFastVLine(74, 0, SCREEN_H, SSD1306_WHITE); // Track number display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(78, 2); display.print("TRACK"); display.setTextSize(2); display.setCursor(80, 13); char buf[4]; snprintf(buf, sizeof(buf), "%02d", currentTrack + 1); display.print(buf); // Track name (small) display.setTextSize(1); display.setCursor(78, 36); // Strip leading "XX " numbering if present, clip to 7 chars const char* name = trackNames[currentTrack]; char clip[8]; strncpy(clip, name, 7); clip[7] = '\0'; display.print(clip); // Status line (bottom) display.setCursor(76, 52); switch (uiState) { case UI_IDLE: display.print("SEL=PLAY"); break; case UI_CONFIRM: // Blink to draw attention if ((millis() / 350) % 2 == 0) display.print("> PLAY? <"); break; case UI_PLAYING: if ((millis() / 500) % 2 == 0) display.print("PLAYING "); else display.print(" "); break; } } // ============================================================================= // SETUP // ============================================================================= void setup() { Serial.begin(115200); delay(500); // I2C for OLED Wire.begin(22, 23); if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("OLED not found — check wiring"); while (true) delay(100); } display.setRotation(2); // 180° flip display.clearDisplay(); display.display(); // Solenoid pinMode(SOLENOID_PIN, OUTPUT); digitalWrite(SOLENOID_PIN, LOW); // Servos servoLift.attach(SERVO_LIFT, SERVO_MIN_US, SERVO_MAX_US); servoMove.attach(SERVO_MOVE); servoLift.write((int)liftPos); writeServoFloat(servoMove, movePos); // Encoder — polled, no interrupt needed pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); pinMode(ENC_SW, INPUT_PULLUP); lastEncA = digitalRead(ENC_A); Serial.println("=== VINYL SELECTOR READY ==="); for (int i = 0; i < NUM_TRACKS; i++) Serial.printf(" Track %d: %s %.0f deg\n", i+1, trackNames[i], trackAngles[i]); } // ============================================================================= // LOOP // ============================================================================= void loop() { uint32_t now = millis(); // ── Encoder (polled, rising edge only) ──────────────────────────────────── int currentA = digitalRead(ENC_A); if (currentA != lastEncA) { if (currentA == HIGH) { // rising edge only if (digitalRead(ENC_B) != currentA) encPulses++; // CW pulse else encPulses--; // CCW pulse } lastEncA = currentA; } // Only register a track step every ENC_STEP pulses (reduces sensitivity) int delta = 0; if (encPulses >= ENC_STEP) { delta = +1; encPulses = 0; } // CW = next if (encPulses <= -ENC_STEP) { delta = -1; encPulses = 0; } // CCW = prev if (delta != 0) { currentTrack = (currentTrack + delta + NUM_TRACKS) % NUM_TRACKS; if (uiState == UI_CONFIRM) uiState = UI_IDLE; Serial.printf("Track %d: %s\n", currentTrack+1, trackNames[currentTrack]); } // ── Confirm timeout ──────────────────────────────────────────────────────── if (uiState == UI_CONFIRM && (now - confirmMs) > CONFIRM_TIMEOUT) { uiState = UI_IDLE; Serial.println("Confirm timed out"); } // ── Button ───────────────────────────────────────────────────────────────── if (digitalRead(ENC_SW) == LOW && (now - lastBtnMs) > BTN_DEBOUNCE) { lastBtnMs = now; if (uiState == UI_IDLE) { uiState = UI_CONFIRM; confirmMs = now; Serial.printf("Confirm: play track %d?\n", currentTrack+1); } else if (uiState == UI_CONFIRM) { // Confirmed — run the full play sequence uiState = UI_PLAYING; playingTrack = currentTrack; isSpinning = true; spinStartMs = now; // Redraw "PLAYING" before the blocking motion starts display.clearDisplay(); drawVinyl(); drawTrackInfo(); display.display(); playTrack(playingTrack); // blocking — servos move here uiState = UI_IDLE; // return to idle when done } else if (uiState == UI_PLAYING) { // Shouldn't normally be reachable (playTrack is blocking) // but safety fallback uiState = UI_IDLE; } } // ── Spin animation ───────────────────────────────────────────────────────── if (isSpinning) { uint32_t elapsed = now - spinStartMs; if (elapsed < SPIN_DURATION_MS) { float t = (float)elapsed / SPIN_DURATION_MS; float e = (t < 0.5f) ? 2.0f*t*t : 1.0f - 2.0f*(1.0f-t)*(1.0f-t); spinSpeed = SPIN_MAX_SPEED * (1.0f - e) + 0.8f; } else { isSpinning = false; spinSpeed = 0.8f; } } spinAngle += spinSpeed; if (spinAngle >= 360.0f) spinAngle -= 360.0f; // ── Render (skipped while motion is blocking) ────────────────────────────── if (uiState != UI_PLAYING) { display.clearDisplay(); drawVinyl(); drawTrackInfo(); display.display(); } delay(30); }

Download Firmware (.ino) →

Bill of Materials (BOM)

Component Supplier Qty Unit Cost (₹) Total (₹)
Seeed Studio XIAO ESP32-C6 Fab Lab Stock 1 555.00 555.00
MP1584 Buck Converter Module Fab Lab Stock 1 41.00 41.00
100µF Electrolytic Capacitor (SMD) Fab Lab Stock 1 0.00 0.00
MOSFET N-Channel (AO3400A Equivalent) Fab Lab Stock 1 32.00 32.00
SS34 Schottky Diode Fab Lab Stock 1 2.00 2.00
JST-XH 2 Pin Female Connector Fab Lab Stock 2 0.00 0.00
JST-XH 2 Pin Male Connector Fab Lab Stock 2 0.00 0.00
1kΩ Resistor Fab Lab Stock 1 0.00 0.00
10kΩ 1206 Resistor Fab Lab Stock 3 0.50 1.50
100nF Ceramic Capacitor Fab Lab Stock 2 0.00 0.00
2×1 Male Pin Header Fab Lab Stock Several 0.00 0.00
Plywood Sheet Fab Lab Stock 1 0.00 0.00
Transparent Acrylic Sheet Fab Lab Stock 1 0.00 0.00
PLA Filament Fab Lab Stock Approx. 120 g 0.00 0.00

Total Project Cost

Total Estimated Cost ₹631.50

Mechanical Materials


Design Files