All
SOLID MODELSingle Board Computers (SBCs)18-May-2026

Productive Timer Raspberry Pi Pico

rapido8
rapido8
3 Files
stl Format

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 !

}

}

}

Downloads

Pot4_1.STL
60.5 KB
productive_timer_body.stl
150.8 KB
platev4.stl
21.4 KB