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();
}
}