Productive Timer Raspberry Pi Pico

par rapido8

Fichiers imprimables (3)

Description

this is for picobuilders chalenge
With this productivity timer, procrastination is no longer a problem.
Here are the features:

.Timer
.Stopwatch
.Game
.Zen Breath
.Metronome
.Settings
.Info
.Data Graph

Materials I Used
.3D‑printed case
.3D‑printed potentiometer knob (from Idee 3D (Alex Torres) on Printables
.3D‑printed mounting plate
.Raspberry Pi Pico (2020)
.EC11 rotary encoder
.0.96" I2C OLED screen (128 × 96 px)
.Wires

Pin Connections
OLED Screen (I2C)
.Connect GND of the OLED to any GND pin on the Pico (e.g., Pin 38).

.Connect VCC of the OLED to the 3V3 OUT pin on the Pico (Pin 36).

.Connect SDA of the OLED to GPIO 4 on the Pico (Pin 6).

.Connect SCL of the OLED to GPIO 5 on the Pico (Pin 7).

EC11 Rotary Encoder
.Connect GND of the encoder to any GND pin on the Pico (e.g., Pin 18).

.The + / VCC pin can technically be left unconnected (the code uses internal pull‑ups), but I recommend connecting it anyway.

.Connect CLK (A) to GPIO 15 on the Pico (Pin 20).

.Connect DT (B) to GPIO 14 on the Pico (Pin 19).

.Connect SW (Button) to GPIO 13 on the Pico (Pin 17).

Quick Tips for Your Build
.No extra resistors needed:
The script uses INPUT_PULLUP, so the Pico handles the signal lines internally. No external resistors required.

Keep signals clean:
.If the menu jumps or behaves strangely when you touch the dial, it’s picking up noise.
Keep your wires short (under 15 cm) and make sure all GND connections are solid.
Also, I recommend putting weight in a plastic bag in the case to make it heavier.

this is the code for arduino ide

include include include define SCREEN_WIDTH 128define SCREEN_HEIGHT 64define OLED_RESET -1define SCREEN_ADDRESS 0x3C

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

// Pins Encodeur (Ajuste selon ton câblage)
const int ENC_CLK = 15;
const int ENC_DT = 14;
const int ENC_SW = 13;

// Variables Système
int hours = 12, mins = 0;
int lang = 1; // 0 = FR, 1 = EN
unsigned long lastTick = 0;
unsigned long lastActivity = 0;
const unsigned long SLEEP_TIMEOUT = 30000;

// Navigation et Interface
int menuIndex = 0;
const int TOTAL_APPS = 8;
const char appNamesFR[] = {"TIME MASTER", "ARCADE BOX", "ZEN BREATH", "METRONOME", "DATA GRAPH", "CODE VAULT", "REGLAGES", "INFOS"};
const char
appNamesEN[] = {"TIME MASTER", "ARCADE BOX", "ZEN BREATH", "METRONOME", "DATA GRAPH", "CODE VAULT", "SETTINGS", "INFO"};

int lastClk;
int rotationDelta = 0;

// Gestion intelligente du bouton (Clic court vs Clic Long)
int btnState = 0; // 0: Rien, 1: Clic court, 2: Clic long (Quitter)
unsigned long btnPressTime = 0;
bool btnHeld = false;

void readHardware() {
// Lecture Rotation
int clk = digitalRead(ENC_CLK);
if (clk != lastClk && clk == LOW) {
if (digitalRead(ENC_DT) != clk) rotationDelta++;
else rotationDelta--;
lastActivity = millis();
}
lastClk = clk;

// Lecture Bouton (Anti-rebond et détection Clic Long)
int sw = digitalRead(ENC_SW);
if (sw == LOW) {
if (!btnHeld) {
btnPressTime = millis();
btnHeld = true;
} else if (millis() - btnPressTime > 600) {
btnState = 2; // Déclenche le clic long
btnHeld = false;
while(digitalRead(ENC_SW) == LOW); // Attend le relâchement physique
lastActivity = millis();
}
} else {
if (btnHeld) {
if (millis() - btnPressTime > 20) btnState = 1; // Clic court
btnHeld = false;
lastActivity = millis();
}
}
}

int getRotation() {
int val = rotationDelta;
rotationDelta = 0;
return val;
}

int getButton() {
int b = btnState;
btnState = 0;
return b;
}

void updateClock() {
// Utilisation de 'while' pour rattraper toutes les minutes manquées
// si le système était bloqué dans une application.
while (millis() - lastTick >= 60000) {
lastTick += 60000; // Conserve la précision exacte des secondes
mins++;
if (mins >= 60) { mins = 0; hours = (hours + 1) % 24; }
}
}

void appTimeMaster() {
int mode = 0; // 0: Chrono, 1: Timer
bool running = false;
long secs = 0;
int setMins = 5;
unsigned long lastTime = millis();
display.setTextSize(1);

while(true) {
readHardware();
int btn = getButton();
if (btn == 2) return;

int r = getRotation();if (!running) {  if (r != 0) {    if (mode == 0 && r > 0) { mode = 1; setMins = 5; secs = 300; }    else if (mode == 1) {      setMins += r;      if (setMins < 1) { mode = 0; secs = 0; }      else secs = setMins * 60;    }  }  if (btn == 1) { running = true; lastTime = millis(); }} else {  if (btn == 1) running = false; // Pause}if (running && millis() - lastTime >= 1000) {  lastTime += 1000;  if (mode == 0) secs++;  else { secs--; if (secs <= 0) running = false; }}display.clearDisplay();display.drawRoundRect(0,0,128,64,4,WHITE);display.setCursor(8, 8); if (lang == 0) display.print(mode == 0 ? "CHRONOMETRE" : "TIMER POMODORO");else display.print(mode == 0 ? "STOPWATCH" : "POMODORO TIMER");display.setTextSize(2);display.setCursor(18, 30);int h = secs / 3600, m = (secs % 3600) / 60, s = secs % 60;if (h>0) { display.print(h); display.print(":"); }if (m<10) display.print("0"); display.print(m); display.print(":");if (s<10) display.print("0"); display.print(s);display.setTextSize(1);display.setCursor(12, 52);if(running) display.print(lang == 0 ? "CLIC POUR PAUSE" : "CLICK TO PAUSE");else display.print(mode == 0 ? (lang == 0 ? "TOURNEZ -> TIMER" : "TURN -> TIMER") : (lang == 0 ? "<- CHRONO | CLIC=GO" : "<- WATCH | CLICK=GO"));display.display();

}
}

void playJumper() {
int py = 40, vy = 0;
int obsX = 128, obsW = 8, obsH = 10, score = 0;
bool isJumping = false;
unsigned long lastFrame = millis();

while(true) {
readHardware(); int btn = getButton(); if (btn == 2) return;
if (btn == 1 && py == 40) { vy = -6; isJumping = true; } // Saut

if (millis() - lastFrame >= 20) {   lastFrame = millis();  py += vy;  if (py < 40) vy += 1; // Gravité  else { py = 40; vy = 0; isJumping = false; }  obsX -= (3 + score/5); // Accélération  if (obsX < -10) { obsX = 128; obsH = random(8, 18); score++; }  // Collision (Joueur est 8x8 en X=10)  if (obsX < 18 && obsX + obsW > 10 && py + 8 > 48 - obsH) {    display.clearDisplay(); display.setCursor(30, 25); display.print(lang == 0 ? "FIN DE PARTIE" : "GAME OVER");     display.setCursor(40, 40); display.print("SCORE: "); display.print(score);    display.display(); delay(2000); return;  }  display.clearDisplay();  display.drawLine(0, 48, 128, 48, WHITE); // Sol  display.fillRect(10, py, 8, 8, WHITE); // Joueur  display.fillRect(obsX, 48 - obsH, obsW, obsH, WHITE); // Obstacle  display.setCursor(60, 2); display.print(score);  display.display();}

}
}

void playSnake() {
int sx[20], sy[20], len = 3, dir = 1; // 0:H, 1:D, 2:B, 3:G
int fx = random(2, 20) 4, fy = random(2, 10) 4;
sx[0]=64; sy[0]=32; for(int i=1;i<20;i++){sx[i]=0;sy[i]=0;}
unsigned long lastMv = 0;
while(true) {
readHardware(); if (getButton() == 2) return;
int r = getRotation();
if (r > 0) dir = (dir + 1) % 4; if (r < 0) dir = (dir + 3) % 4;

if (millis() - lastMv > 120) {  lastMv = millis();  for(int i=len-1; i>0; i--) { sx[i]=sx[i-1]; sy[i]=sy[i-1]; }  if (dir==0) sy[0]-=4; else if (dir==1) sx[0]+=4; else if (dir==2) sy[0]+=4; else if (dir==3) sx[0]-=4;  if (abs(sx[0]-fx)<4 && abs(sy[0]-fy)<4) {    if (len<20) len++; fx = random(1, 30)*4; fy = random(1, 14)*4;  }  if (sx[0]<0||sx[0]>124||sy[0]<0||sy[0]>60) {    display.clearDisplay(); display.setCursor(45,30); display.print("CRASH!"); display.display(); delay(1500); return;  }}display.clearDisplay();display.drawRect(fx, fy, 4, 4, WHITE); // Pommefor(int i=0; i<len; i++) display.fillRect(sx[i], sy[i], 4, 4, WHITE);display.display();

}
}

void playCatcher() {
int px = 54; // Panier
int ax = random(10, 118), ay = -5, score = 0, speed = 2;
unsigned long lastFrame = millis();

while(true) {
readHardware(); if (getButton() == 2) return;
px = constrain(px + (getRotation() * 8), 0, 108); // Largeur max = 128 - 20

if (millis() - lastFrame >= 20) {  lastFrame = millis();  ay += speed;  if (ay >= 58) {    // Vérifie si attrapé    if (ax + 4 > px && ax < px + 20) {      score++; ax = random(10, 118); ay = -5;      speed = 2 + (score / 10); // Accélère    } else {      // Raté      display.clearDisplay(); display.setCursor(30, 25); display.print(lang == 0 ? "FIN DE PARTIE" : "GAME OVER");       display.setCursor(40, 40); display.print("SCORE: "); display.print(score);      display.display(); delay(2000); return;    }  }  display.clearDisplay();  display.fillRect(px, 58, 20, 4, WHITE); // Panier  display.fillRect(ax, ay, 4, 4, WHITE);  // Pomme  display.setCursor(60, 2); display.print(score);  display.display();}

}
}

void appArcade() {
int gameSel = 0;
while(true) {
readHardware(); int btn = getButton(); if (btn == 2) return;
int r = getRotation();
if (r != 0) gameSel = (gameSel + r + 3) % 3;

display.clearDisplay();display.setCursor(35, 5); display.print("ARCADE BOX");display.drawLine(30, 15, 98, 15, WHITE);display.setCursor(20, 25); display.print(gameSel == 0 ? "> 1. SNAKE"   : "  1. SNAKE");display.setCursor(20, 35); display.print(gameSel == 1 ? "> 2. JUMPER"  : "  2. JUMPER");display.setCursor(20, 45); display.print(gameSel == 2 ? "> 3. CATCHER" : "  3. CATCHER");display.display();if (btn == 1) {  if (gameSel == 0) playSnake();  else if (gameSel == 1) playJumper();  else if (gameSel == 2) playCatcher();}

}
}

void appZenBreath() {
unsigned long startCycle = millis();
while(true) {
readHardware(); if (getButton() == 2) return;

long t = (millis() - startCycle) % 16000; // Cycle de 16 secondesfloat radius = 5; String msg = "";if (t < 4000) { // Inspire  radius = 5 + (20.0 * t / 4000.0); msg = lang == 0 ? "INSPIREZ" : "INHALE";} else if (t < 8000) { // Bloque  radius = 25; msg = lang == 0 ? "BLOQUEZ" : "HOLD";} else if (t < 12000) { // Expire  radius = 25 - (20.0 * (t - 8000) / 4000.0); msg = lang == 0 ? "EXPIREZ" : "EXHALE";} else { // Bloque  radius = 5; msg = "PAUSE";}display.clearDisplay();display.fillCircle(64, 28, (int)radius, WHITE);int cursorX = (msg.length() * 6) / 2;display.setCursor(64 - cursorX, 52); display.print(msg);display.display();

}
}

void appMetronome() {
int bpm = 120; bool playing = false;
unsigned long lastBeat = 0; bool beatTick = false;
while(true) {
readHardware(); int btn = getButton(); if (btn == 2) return;
if (btn == 1) playing = !playing;

int r = getRotation();if (r != 0) bpm = constrain(bpm + r * 5, 40, 240); long interval = 60000 / bpm;if (playing && millis() - lastBeat >= interval) {  lastBeat = millis(); beatTick = true;}display.clearDisplay();display.drawRoundRect(0,0,128,64,4,WHITE);display.setCursor(10, 10); display.print("METRONOME");display.setTextSize(3);display.setCursor(40, 28); display.print(bpm);display.setTextSize(1);if (beatTick) {  display.fillCircle(20, 40, 8, WHITE);  if (millis() - lastBeat > 50) beatTick = false;}display.setCursor(10, 52); if (lang == 0) display.print(playing ? "CLIC=PAUSE | BPM" : "CLIC=PLAY  | BPM");else display.print(playing ? "CLICK=PAUSE| BPM" : "CLICK=PLAY | BPM");display.display();

}
}

void appDataGraph() {
int vals[128]; for(int i=0;i<128;i++) vals[i] = 32;
while(true) {
readHardware(); if (getButton() == 2) return;
display.clearDisplay();
for(int i=0; i<127; i++) {
vals[i] = vals[i+1];
display.drawLine(i, vals[i], i+1, vals[i+1], WHITE);
}
vals[127] = constrain(vals[126] + random(-4, 5), 15, 60);
display.fillRect(0,0,128,12,BLACK);
display.setCursor(5, 2); display.print(lang == 0 ? "SYSTEM TEMP GRAPHE" : "SYSTEM TEMP GRAPH");
display.display();
delay(20);
}
}

void appVault() {
int code[4] = {0,0,0,0}, sel = 0;
while(true) {
readHardware(); int btn = getButton(); if (btn == 2) return;
code[sel] = (code[sel] + getRotation() + 10) % 10;

display.clearDisplay();display.setCursor(35, 10); display.print("CODE PIN");for(int i=0; i<4; i++) {  display.setCursor(40 + (i*15), 35);  if(sel == i) { display.setTextColor(BLACK, WHITE); display.print(code[i]); }   else { display.setTextColor(WHITE); display.print("*"); }}display.setTextColor(WHITE);if(btn == 1) {   sel++;   if(sel > 3) {    display.clearDisplay(); display.setCursor(30, 30);     if (code[0]==1 && code[1]==2 && code[2]==3 && code[3]==4) display.print(lang == 0 ? "ACCES OK" : "ACCESS OK");    else display.print(lang == 0 ? "REFUSE" : "DENIED");    display.display(); delay(1500); return;  }}display.display();

}
}

void appSettings() {
int sel = 0;
while(true) {
readHardware(); int btn = getButton(); if (btn == 2) return;
if (btn == 1) { sel++; if (sel > 2) return; }
int r = getRotation();

// Ajout d'une sécurité : si on change l'heure manuellement, on réinitialise les secondesif (sel == 0 && r != 0) { hours = (hours + r + 24) % 24; lastTick = millis(); } else if (sel == 1 && r != 0) { mins = (mins + r + 60) % 60; lastTick = millis(); }else if (sel == 2 && r != 0) lang = (lang + 1) % 2; // Bascule FR/ENdisplay.clearDisplay();display.setCursor(30, 5); display.print(lang == 0 ? "REGLAGES" : "SETTINGS");display.setTextSize(2); display.setCursor(35, 20);if (sel == 0 && (millis()/250)%2) display.setTextColor(BLACK, WHITE); else display.setTextColor(WHITE);if (hours < 10) display.print("0"); display.print(hours);display.setTextColor(WHITE); display.print(":");if (sel == 1 && (millis()/250)%2) display.setTextColor(BLACK, WHITE); else display.setTextColor(WHITE);if (mins < 10) display.print("0"); display.print(mins);display.setTextColor(WHITE); display.setTextSize(1);display.setCursor(40, 45);if (sel == 2 && (millis()/250)%2) display.setTextColor(BLACK, WHITE); else display.setTextColor(WHITE);display.print(lang == 0 ? "FRANCAIS" : "ENGLISH");display.setTextColor(WHITE);display.display();

}
}

void appInfo() {
while(true) {
readHardware(); if (getButton() == 2) return;
display.clearDisplay();
display.drawRoundRect(0, 0, 128, 64, 4, WHITE);
display.setCursor(10, 10); display.print("SYSTEM INFO");
display.drawLine(10, 20, 110, 20, WHITE);

display.setCursor(10, 28); display.print(lang == 0 ? "carte pico" : "pico board");display.setCursor(10, 38); display.print("version:5.1");display.setCursor(10, 48); display.print(lang == 0 ? "etat semi fonctinel" : "semi-functional");display.display();

}
}

void drawMenu() {
display.clearDisplay();

display.fillRoundRect(-10, 2, 18, 60, 6, WHITE);
int indicatorY = 8 + (menuIndex * (48 / (TOTAL_APPS - 1)));
display.fillCircle(4, indicatorY, 2, BLACK);

display.setCursor(15, 5); display.print("PICO OS");
display.setCursor(95, 5);
if(hours < 10) display.print("0"); display.print(hours);
display.print(":");
if(mins < 10) display.print("0"); display.print(mins);
display.drawLine(15, 15, 120, 15, WHITE);

display.fillRect(15, 35, 113, 14, WHITE);
display.setTextColor(BLACK);
display.setCursor(20, 38);
display.print(lang == 0 ? appNamesFR[menuIndex] : appNamesEN[menuIndex]);
display.setTextColor(WHITE);

display.setCursor(15, 55); display.print(lang == 0 ? "Maintenir=Quitter" : "Hold = Quit");
display.display();
}

void setup() {
pinMode(ENC_CLK, INPUT_PULLUP);
pinMode(ENC_DT, INPUT_PULLUP);
pinMode(ENC_SW, INPUT_PULLUP);
Wire.begin();
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay(); display.setTextColor(WHITE);
lastClk = digitalRead(ENC_CLK);
lastActivity = millis();
}

void loop() {
readHardware(); updateClock();
display.setTextSize(1); // Sécurité

if (millis() - lastActivity > SLEEP_TIMEOUT) {
display.clearDisplay();
display.setTextSize(3);
display.setCursor(18, 20);
if(hours<10)display.print("0");display.print(hours);display.print(":");
if(mins<10)display.print("0");display.print(mins);
display.setTextSize(1);
display.setCursor(10, 52); display.print(lang == 0 ? "CLIC LONG=QUITTER" : "LONG CLICK TO QUIT");
display.display();
} else {
drawMenu();
int r = getRotation();
if (r != 0) menuIndex = (menuIndex + r + TOTAL_APPS) % TOTAL_APPS;

if (getButton() == 1) { // Clic court pour ouvrir  switch(menuIndex) {    case 0: appTimeMaster(); break;    case 1: appArcade(); break;    case 2: appZenBreath(); break;    case 3: appMetronome(); break;    case 4: appDataGraph(); break;    case 5: appVault(); break;    case 6: appSettings(); break;    case 7: appInfo(); break;  }  display.setTextSize(1); // Réinitialise toujours en sortant !}

}
}

Tags