Embedded Coding

Pet Rock

Balboa features six distinct personality modes Tranquil, Contemplative, Resigned, Sleeping, Steadfast, and Existing, with unique animations for each state.

The project includes inspirational quotes such as "Rock bottom is solid ground," "Being is enough," "I remain," and many more, perfectly capturing the zen-like wisdom of a digital pet rock.

Board Assembly

Materials:

- QPAD21
- Microcontroller
- LED
- Voltage regulator
- Capacator
- Resistor diode 1K (1001 the last digit signifies how many more zeros there are)
- Micro USB (found)
- Two different headers
- Screen
- Two pieces inbetween the header and microcontroller labeled R4 and R5 the pieces are numbered 4991


Match the dot on the microcontroller to the corner with the dot on the board for correct orientaton. For the LED the green is on the left.

Use lots of flux! Since I didn't have access to solder paste, flux was necessary for clean coverage and reliable connections. Use rubbing alcohol and a clean toothbrush (only when the device is unplugged and powered off) to remove excess flux.

Ensure all pins are completely covered with solder, but avoid connecting adjacent pins. Through-hole components must be completely filled with solder for reliable connections. For the micro usb component, I found it successful to place it in the slots then solder, rather than trying to tack it down first. Don't forget the row of pins in front of it.

To remove excess solder, use a desoldering copper braid and place the braid on the excess solder. Apply a heated iron tip to the braid. Lift the copper up with the iron, otherwise it'll get stuck to the board.

I followed these directions for initial setup of the microcontroller: Arduino SAM Tutorial

Verify all necessary libraries are installed before compiling.


Thank you Anthony, Jake, Alan and Quentin for you help

Pet Rock Code

// Pin mapping: PA02-PA07 = touch pads 0-5, PA15 = LED, PA16/PA17 = I2C (SDA/SCL)

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_FreeTouch.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
#define LED_PIN 15  // PA15 - LED

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Capacitive touch objects for QPAD21  
// PA02=2, PA03=3, PA04=4, PA05=5, PA06=6, PA07=7 (touch pads 0-5)
Adafruit_FreeTouch qt_0 = Adafruit_FreeTouch(2, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);  // PA02
Adafruit_FreeTouch qt_1 = Adafruit_FreeTouch(3, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);  // PA03
Adafruit_FreeTouch qt_2 = Adafruit_FreeTouch(4, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);  // PA04
Adafruit_FreeTouch qt_3 = Adafruit_FreeTouch(5, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);  // PA05
Adafruit_FreeTouch qt_4 = Adafruit_FreeTouch(6, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);  // PA06
Adafruit_FreeTouch qt_5 = Adafruit_FreeTouch(7, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);  // PA07

Adafruit_FreeTouch* touchPads[] = {&qt_0, &qt_1, &qt_2, &qt_3, &qt_4, &qt_5};

// Balboa Moods 
enum RockState {
  TRANQUIL,
  CONTEMPLATIVE, 
  RESIGNED,
  SLEEPING,
  STEADFAST,
  EXISTING
};

RockState currentState = TRANQUIL;
RockState previousState = EXISTING; // Track previous state to avoid repeats
String rockName = "Balboa";
unsigned long lastInteraction = 0;
unsigned long lastStateChange = 0;
unsigned long lastBlink = 0;
bool rockEyes = false;     // For "blinking"

// Quotes from Balboa
String balboaQuotes[] = {
  "I am unmoved",
  "Patience, young human",
  "Time means nothing to me",
  "Your problems are fleeting", 
  "I remain",
  "Stillness is strength",
  "Change is an illusion",
  "I endure all",
  "I am one with the void",
  "Wisdom comes from within",
  "I have no needs",
  "Rock bottom is solid ground",
  "Being is enough"
};

const int totalQuotes = 13; // Correct count
int lastQuoteIndex = -1;   // Track last quote to avoid repeats

const int touchThreshold = 500;

void setup() {
  Serial.begin(9600);
  pinMode(LED_PIN, OUTPUT);
  
  // Initialize all 6 touch pads
  if (!qt_0.begin()) Serial.println("Failed to begin qt_0");
  if (!qt_1.begin()) Serial.println("Failed to begin qt_1"); 
  if (!qt_2.begin()) Serial.println("Failed to begin qt_2");
  if (!qt_3.begin()) Serial.println("Failed to begin qt_3");
  if (!qt_4.begin()) Serial.println("Failed to begin qt_4");
  if (!qt_5.begin()) Serial.println("Failed to begin qt_5");
  
  // Initialize OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }
  
  display.clearDisplay();
  showWelcome();
  delay(3000);
  
  lastInteraction = millis();
  lastStateChange = millis();
  displayRock();
}

void loop() {
  // Check for touch interactions
  checkTouchInteractions();
  
  // State changes (every 30-90 seconds)
  if(millis() - lastStateChange > random(30000, 90000)) {
    changeRockState();
  }
  
  // Rock "blinking" animation
  if(millis() - lastBlink > random(3000, 12000)) {
    rockEyes = !rockEyes;
    lastBlink = millis();
    displayRock();
  }
  
  // Update display every 2 seconds
  static unsigned long lastUpdate = 0;
  if(millis() - lastUpdate > 2000) {
    displayRock();
    lastUpdate = millis();
  }
  
  // LED breathing effect 
  breathingLED();
  
  delay(50);
}

void checkTouchInteractions() {
  for(int i = 0; i < 6; i++) { // Now checking all 6 touch pads
    if(touchPads[i]->measure() > touchThreshold) {
      handleTouch(i);
      lastInteraction = millis();
      delay(300); // Debounce
    }
  }
}

void handleTouch(int pad) {
  // Get a different quote than the last one
  int quoteIndex;
  
  // Prevent same quote twice in a row
  do {
    quoteIndex = random(0, totalQuotes);
  } while(quoteIndex == lastQuoteIndex && totalQuotes > 1);
  
  lastQuoteIndex = quoteIndex;
  String quote = balboaQuotes[quoteIndex];
  
  // Better text fitting
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  
  // Center "Balboa says:"
  display.setCursor(30, 5);
  display.println("Balboa says:");
  
  // Word wrap the quote
  displayWrappedText(quote, 5, 25, 118);
  
  display.display();
  delay(3000);
  
  Serial.println("Touch pad " + String(pad) + " - Quote " + String(quoteIndex) + ": " + quote);
}

void changeRockState() {
  RockState newState;
  
  // Prevent same state twice in a row
  do {
    newState = (RockState)random(0, 6); // 0-5 for the 6 states
  } while(newState == currentState);
  
  previousState = currentState;
  currentState = newState;
  lastStateChange = millis();
  
  // Show dramatic state change
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(15, 15);
  display.println("Balboa is now:");
  display.setTextSize(1);
  display.setCursor(10, 35);
  display.println(getStateString());
  display.display();
  delay(2000);
}

String getStateString() {
  switch(currentState) {
    case TRANQUIL: return "TRANQUIL";
    case CONTEMPLATIVE: return "CONTEMPLATIVE";
    case RESIGNED: return "RESIGNED";
    case SLEEPING: return "SLEEPING";
    case STEADFAST: return "STEADFAST";
    case EXISTING: return "EXISTING";
    default: return "CONFUSED";
  }
}

void displayRock() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  
  // Name (centered)
  display.setTextSize(1);
  int nameWidth = rockName.length() * 6;
  display.setCursor((128 - nameWidth) / 2, 5);
  display.println(rockName);
  
  // Draw Balboa differently based on state
  int rockX = 55, rockY = 20;
  drawBalboaByState(rockX, rockY);
  
  // State indicator (centered)
  String stateText = getStateString();
  int stateWidth = stateText.length() * 6;
  display.setCursor((128 - stateWidth) / 2, 45);
  display.println(stateText);
  
  // Simple instruction
  display.setTextSize(1);
  display.setCursor(15, 55);
  display.println("Touch for wisdom");
  
  display.display();
}

void drawBalboaByState(int x, int y) {
  switch(currentState) {
    case TRANQUIL:
      // Smooth, peaceful oval
      display.fillRoundRect(x, y, 18, 12, 6, SSD1306_WHITE);
      if(rockEyes) {
        display.fillCircle(x + 4, y + 4, 1, SSD1306_BLACK);
        display.fillCircle(x + 14, y + 4, 1, SSD1306_BLACK);
      }
      break;
      
    case CONTEMPLATIVE:
      // Slightly taller, thinking rock
      display.fillRoundRect(x, y-1, 18, 14, 4, SSD1306_WHITE);
      // Always show thoughtful eyes
      display.fillCircle(x + 4, y + 3, 1, SSD1306_BLACK);
      display.fillCircle(x + 14, y + 3, 1, SSD1306_BLACK);
      // Add "thought lines"
      display.drawPixel(x + 9, y - 3, SSD1306_WHITE);
      display.drawPixel(x + 7, y - 4, SSD1306_WHITE);
      display.drawPixel(x + 11, y - 4, SSD1306_WHITE);
      break;
      
    case RESIGNED:
      // Flatter, settled look
      display.fillRoundRect(x, y + 2, 18, 10, 3, SSD1306_WHITE);
      if(rockEyes) {
        // Droopy eyes
        display.drawPixel(x + 4, y + 5, SSD1306_BLACK);
        display.drawPixel(x + 14, y + 5, SSD1306_BLACK);
        display.drawPixel(x + 4, y + 6, SSD1306_BLACK);
        display.drawPixel(x + 14, y + 6, SSD1306_BLACK);
      }
      break;
      
    case SLEEPING:
      // Smaller, cozy rock
      display.fillRoundRect(x + 2, y + 1, 14, 10, 5, SSD1306_WHITE);
      // Proper "Z" shapes for sleeping
      // Small Z
      display.drawLine(x + 19, y - 1, x + 22, y - 1, SSD1306_WHITE); // top line
      display.drawLine(x + 22, y - 1, x + 19, y + 1, SSD1306_WHITE); // diagonal
      display.drawLine(x + 19, y + 1, x + 22, y + 1, SSD1306_WHITE); // bottom line
      // Medium Z
      display.drawLine(x + 24, y - 4, x + 28, y - 4, SSD1306_WHITE); // top line
      display.drawLine(x + 28, y - 4, x + 24, y - 1, SSD1306_WHITE); // diagonal
      display.drawLine(x + 24, y - 1, x + 28, y - 1, SSD1306_WHITE); // bottom line
      break;
      
    case STEADFAST:
      // Angular, solid rectangle
      display.fillRect(x, y, 18, 12, SSD1306_WHITE);
      if(rockEyes) {
        // Determined square eyes
        display.fillRect(x + 3, y + 3, 2, 2, SSD1306_BLACK);
        display.fillRect(x + 13, y + 3, 2, 2, SSD1306_BLACK);
      }
      break;
      
    case EXISTING:
      // Basic perfect circle - pure existence
      display.fillCircle(x + 9, y + 6, 7, SSD1306_WHITE);
      if(rockEyes) {
        display.fillCircle(x + 6, y + 4, 1, SSD1306_BLACK);
        display.fillCircle(x + 12, y + 4, 1, SSD1306_BLACK);
      }
      break;
  }
}

void showWelcome() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  
  display.setTextSize(2);
  display.setCursor(10, 5);
  display.println("PET ROCK");
  
  display.setTextSize(1);
  display.setCursor(25, 25);
  display.println("SIMULATOR");
  
  display.setCursor(5, 40);
  display.println("Meet your new");
  display.setCursor(5, 50);
  display.println("friend: " + rockName + "!");
  
  display.display();
}

void displayWrappedText(String text, int x, int startY, int maxWidth) {
  int currentY = startY;
  int currentX = x;
  String currentLine = "";
  
  for(int i = 0; i < text.length(); i++) {
    char c = text.charAt(i);
    
    if(c == ' ') {
      // Check if adding this word would exceed line width
      int nextSpace = text.indexOf(' ', i + 1);
      if(nextSpace == -1) nextSpace = text.length();
      String nextWord = text.substring(i + 1, nextSpace);
      
      if((currentLine.length() + 1 + nextWord.length()) * 6 > maxWidth) {
        // Print current line and start new one
        display.setCursor(x, currentY);
        display.println(currentLine);
        currentY += 10;
        currentLine = "";
      } else {
        currentLine += c;
      }
    } else {
      currentLine += c;
    }
  }
  
  // Print final line
  if(currentLine.length() > 0) {
    display.setCursor(x, currentY);
    display.println(currentLine);
  }
}

void breathingLED() {
  // LED "breathes" to show rock's life force
  static unsigned long ledTimer = 0;
  static int brightness = 0;
  static int direction = 1;
  
  if(millis() - ledTimer > 50) {
    brightness += direction * 5;
    if(brightness >= 255 || brightness <= 0) {
      direction *= -1;
    }
    brightness = constrain(brightness, 0, 255);
    
    analogWrite(LED_PIN, brightness);
    ledTimer = millis();
  }
}