Productive Timer Raspberry Pi Pico





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.
this is the code for arduino ide
include <Wire.h>
include <Adafruit_GFX.h>
include <Adafruit_SSD1306.h>
define SCREEN_WIDTH 128
define SCREEN_HEIGHT 64
define OLED_RESET -1
define 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); // Pomme
for(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 secondes
float 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 secondes
if (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/EN
display.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 !
}
}
}






