【ルネサス技術コラム】Arduino Uno R4 WiFiのオンボードマトリクスでポモドーロタイマーを作ってみた

作業を始める時にありがちなのが、

  • 気づいたら集中が切れていた

  • 休憩を入れようと思っていたのに、だらだら作業し続けてしまう

  • 逆に、少し休むつもりが長く休みすぎる

  • PC上のタイマーは作業ウィンドウなどに隠れて見逃しやすく、スマホはつい他の通知も見てしまう

といった問題です。

こうした「集中と休憩の切り替え」を助ける定番の方法がポモドーロ・テクニックです。ただ、PCやスマホのタイマーに頼ると、どうしても通知や別アプリに気を取られやすくなります。

そこで今回は、「エレクトロニクスを簡単に」をテーマにRenesas RA4M1搭載のArduino Uno R4 WiFiのオンボード12×8 LED matrix を使って、作業時間と休憩時間の切り替えを視覚的に知らせるポモドーロタイマーを作ってみました。
Arduino Uno R4 WiFiは、メインMCUにRenesas RA4M1、無線機能にESP32-S3を搭載しており、Arduino Cloudとも互換性があります。

今回はそのオンボード LED Matrixを活用し、集中時間と休憩時間を視覚的に管理できるシンプルなポモドーロタイマーを実装してみます。
外付けディスプレイなしで、ボード単体で進捗が見えるのがポイントです。

ポモドーロタイマーとは、作業と休憩のサイクルを繰り返す時間管理法です。
短時間に集中することで、作業効率の向上や集中力の維持を目指します。
1980年代にイタリア人のフランチェスコ・シリロ氏が考案しました。
「ポモドーロ (Pomodoro)」はイタリア語で「トマト」を意味し、シリロ氏が使っていたトマト型のキッチンタイマーが名前の由来です。
現在では勉強や仕事の生産性向上のために世界中で活用されています。

目次

はじめに: どんな困りごとに対するソリューションなのか

ポモドーロタイマーは単なる「時間計測」ではなく、集中の区切りを作るための補助装置として役立ちます。

特に次のような困りごとに対して効果があります。

  • 作業時間の上限を決めて、集中しやすくしたい

  • 休憩を後回しにせず、意識的に切り替えたい

  • PC画面を見続けなくても進捗を把握したい

  • 机の上に置いて、今が作業フェーズか休憩フェーズか一目で分かるようにしたい

  • 個人作業だけでなく、チームの“集中時間”を共有したい

今回はその解決策として、Arduino Uno R4 WiFiのオンボードLED matrixを使って、時間の進行をバー表示し、フェーズの切り替えを直感的に可視化する仕組みを作ります。

つまり今回のソリューションは、

「集中時間を続けやすくしたい」
「休憩の切り替えを忘れないようにしたい」
「余計な通知のない専用タイマーがほしい」

という困りごとに対するものです。

今回のソリューション概要

今回作るのは、Arduino Uno R4 WiFiのオンボード12×8 LED matrix上で進捗を表示するポモドーロタイマーです。

本記事では以下を実現します。

  1. ポモドーロタイマーの動作実装

  2. LED Matrixによる状態可視化

  3. ボタン操作による制御

基本仕様は次の通りです。

  • 作業フェーズ: 25分

  • 休憩フェーズ: 5分

  • ボタン1: 開始/一時停止

  • ボタン2短押し: 次のフェーズへスキップ

  • ボタン2長押し: 最初の作業フェーズにリセット

表示のイメージは次のようになります。

  • 作業中: 左から伸びる進捗バー

  • 休憩中: 右から伸びる進捗バー

  • 一時停止中: ポーズアイコン

  • フェーズ切替時: チェックマーク点滅

また、シリアルモニタには

  • 現在のフェーズ

  • 残り時間

  • 実行状態

を文字でも出力するため、デバッグや確認もしやすくしています。

なぜArduino Uno R4 WiFiが向いているのか

今回のテーマに Arduino Uno R4 WiFi が向いている理由は、何よりオンボード12×8 LED matrix を標準搭載していることです。

公式ドキュメントによると、Arduino Uno R4 WiFiは Renesas RA4M1 (Arm Cortex-M4 / 48MHz / 32kB SRAM / 256kB Flash) を搭載し、さらにESP32-S3 によって Wi-Fi / Bluetooth に対応しています。また、5V動作で扱いやすいのも特徴です。
Arduino Docs

今回はWi-Fi機能そのものを使うわけではありませんが、Uno R4 WiFiの大きな魅力であるオンボードLED matrix を活かして、

  • 外部LCDなし

  • 配線を増やしすぎない

  • 机上でそのまま使える

という、シンプルで見栄えの良いPoC にしやすいのが利点です。
今回のPoCではLED Matrixのみを使用しますが、将来的にはWi-Fi経由でSlack通知やクラウド連携へ発展できます。

ローカル表示の基本構成

今回の構成では、Arduino Uno R4 WiFi本体に加えて、2つのタクトスイッチを接続するだけです。

基本の動作は次の通りです。

  1. 開始ボタンでタイマーをスタート

  2. LED matrixに現在のフェーズ進捗を表示

  3. 一時停止やスキップをボタンで操作

  4. フェーズが終わったら、チェックマーク表示後に次フェーズへ進む

このように、外付けの大きな表示器を使わなくても、進行状況を視覚的に把握できる専用タイマーとして成立します。

開発環境と実装のポイント

開発環境には Visual Studio Code と PlatformIO IDE を使用しました。PlatformIOでは、board = uno_r4_wifi、platform = renesas-ra、framework = arduino の構成で利用できます。

なぜ“数字表示”より“進捗表示”なのか

12×8 LED matrixは、広い文字表示には向いていません。一方で、

  • バー

  • アイコン

  • 点滅

  • 簡単なアニメーション

のような状態表示には非常に向いています。

そのため今回は、時刻を細かく大きく表示するのではなく、時間の進み具合を視覚的に把握できる進捗バーとして使っています。

フェーズごとに見え方を変える意味

作業中と休憩中で同じ表示にしてしまうと、離れた場所から見た時に違いが分かりにくくなります。そこで今回は、

  • 作業中: 左から増えるバー

  • 休憩中: 右から増えるバー

  • 停止中: ポーズアイコン

という形にして、今どの状態なのかが一目で分かるようにしました。

delay() ではなく millis() を使う理由

タイマー、ボタン操作、LED更新を同時に扱うため、今回は delay() に頼らず millis() ベースの非ブロッキング処理で構成しています。これにより、進捗表示中でもボタン操作がしやすくなります。

使用した主な部品

  • Arduino Uno R4 WiFi

  • タクトスイッチ × 2

  • ブレッドボード

  • ジャンパーピン

  • USB Type-C ケーブル

配線情報

今回は、2つのボタンを内部プルアップを使って接続します。そのため、外付けプルアップ抵抗は不要です。

配線表

部品

信号

接続先

補足

ボタン1

片側

D2

開始/一時停止

ボタン1

もう片側

GND

INPUT_PULLUPで使用

ボタン2

片側

D3

短押し: スキップ / 長押し: リセット

ボタン2

もう片側

GND

INPUT_PULLUP で使用

配線イメージを言葉で整理すると

  • ボタン1は D2 と GND の間に接続

  • ボタン2は D3 と GND の間に接続

  • ボタンはどちらも INPUT_PULLUP で読むため、押していない時は HIGH、押した時は LOW になります

実際のプログラム

今回のローカル表示版で使用したプログラムは以下の通りです。
オンボードLED matrixに進捗を表示しながら、ボタン操作でポモドーロタイマーを動かします。
※本プログラムは、生成AIを用いて作成しております。

#include <Arduino.h>
#include <Arduino_LED_Matrix.h>

ArduinoLEDMatrix matrix;

const int startPauseButtonPin = 2;
const int actionButtonPin = 3;

const unsigned long workDurationMs = 25UL * 60UL * 1000UL;
const unsigned long breakDurationMs = 5UL * 60UL * 1000UL;

const unsigned long debounceMs = 30;
const unsigned long longPressMs = 800;
const unsigned long renderIntervalMs = 120;
const unsigned long serialIntervalMs = 1000;

enum Phase {
  WORK_PHASE,
  BREAK_PHASE
};

struct ButtonState {
  int pin;
  bool stableState;
  bool lastReading;
  unsigned long lastChangeMs;
  unsigned long pressedAtMs;
  bool shortPressed;
  bool longPressed;
  bool longHandled;
};

ButtonState startPauseButton = {
  startPauseButtonPin, HIGH, HIGH, 0, 0, false, false, false
};

ButtonState actionButton = {
  actionButtonPin, HIGH, HIGH, 0, 0, false, false, false
};

Phase currentPhase = WORK_PHASE;
bool running = false;
unsigned long remainingMs = workDurationMs;

unsigned long lastTickMs = 0;
unsigned long lastRenderMs = 0;
unsigned long lastSerialMs = 0;

uint8_t frame[8][12];

void clearFrame() {
  for (int y = 0; y < 8; y++) {
    for (int x = 0; x < 12; x++) {
      frame[y][x] = 0;
    }
  }
}

void showFrame() {
  matrix.renderBitmap(frame, 8, 12);
}

unsigned long phaseTotalMs(Phase phase) {
  return (phase == WORK_PHASE) ? workDurationMs : breakDurationMs;
}

const char* phaseName(Phase phase) {
  return (phase == WORK_PHASE) ? "WORK" : "BREAK";
}

void updateButton(ButtonState &button, unsigned long now) {
  bool reading = digitalRead(button.pin);

  if (reading != button.lastReading) {
    button.lastChangeMs = now;
    button.lastReading = reading;
  }

  if ((now - button.lastChangeMs) > debounceMs && reading != button.stableState) {
    button.stableState = reading;

    if (button.stableState == LOW) {
      button.pressedAtMs = now;
      button.longHandled = false;
    } else {
      unsigned long pressDuration = now - button.pressedAtMs;
      if (!button.longHandled && pressDuration < longPressMs) {
        button.shortPressed = true;
      }
    }
  }

  if (button.stableState == LOW && !button.longHandled && (now - button.pressedAtMs >= longPressMs)) {
    button.longPressed = true;
    button.longHandled = true;
  }
}

bool consumeShortPress(ButtonState &button) {
  if (button.shortPressed) {
    button.shortPressed = false;
    return true;
  }
  return false;
}

bool consumeLongPress(ButtonState &button) {
  if (button.longPressed) {
    button.longPressed = false;
    return true;
  }
  return false;
}

void drawPauseIcon() {
  clearFrame();
  for (int y = 1; y <= 6; y++) {
    frame[y][4] = 1;
    frame[y][5] = 1;
    frame[y][7] = 1;
    frame[y][8] = 1;
  }
  showFrame();
}

void drawCheckMark() {
  clearFrame();
  frame[5][2] = 1;
  frame[6][3] = 1;
  frame[5][4] = 1;
  frame[4][5] = 1;
  frame[3][6] = 1;
  frame[2][7] = 1;
  frame[1][8] = 1;
  frame[0][9] = 1;
  showFrame();
}

void drawWorkFrame(int progressPercent) {
  clearFrame();
  int filledCols = map(progressPercent, 0, 100, 0, 12);
  filledCols = constrain(filledCols, 0, 12);

  for (int x = 0; x < filledCols; x++) {
    for (int y = 2; y < 8; y++) {
      frame[y][x] = 1;
    }
  }

  // 作業中の目印として中央に小さな点
  if ((millis() / 500) % 2 == 0) {
    frame[0][5] = 1;
    frame[0][6] = 1;
    frame[1][5] = 1;
    frame[1][6] = 1;
  }

  showFrame();
}

void drawBreakFrame(int progressPercent) {
  clearFrame();
  int filledCols = map(progressPercent, 0, 100, 0, 12);
  filledCols = constrain(filledCols, 0, 12);

  for (int i = 0; i < filledCols; i++) {
    int x = 11 - i;
    for (int y = 4; y < 8; y++) {
      frame[y][x] = 1;
    }
  }

  // 休憩中の目印として簡単なリラックス表示
  frame[1][3] = 1;
  frame[1][8] = 1;
  frame[2][4] = 1;
  frame[2][5] = 1;
  frame[2][6] = 1;
  frame[2][7] = 1;

  showFrame();
}

void showTransitionAnimation() {
  for (int i = 0; i < 3; i++) {
    drawCheckMark();
    delay(120);
    clearFrame();
    showFrame();
    delay(80);
  }
}

void resetToWorkPhase() {
  currentPhase = WORK_PHASE;
  remainingMs = workDurationMs;
  running = false;
  lastTickMs = millis();
}

void switchToNextPhase(bool keepRunning) {
  currentPhase = (currentPhase == WORK_PHASE) ? BREAK_PHASE : WORK_PHASE;
  remainingMs = phaseTotalMs(currentPhase);
  running = keepRunning;
  lastTickMs = millis();
}

void toggleRunning() {
  running = !running;
  lastTickMs = millis();
}

void handlePhaseComplete() {
  running = false;
  showTransitionAnimation();
  switchToNextPhase(true);
}

void updateTimer(unsigned long now) {
  if (!running) return;

  unsigned long elapsed = now - lastTickMs;
  lastTickMs = now;

  if (elapsed >= remainingMs) {
    remainingMs = 0;
    handlePhaseComplete();
    return;
  }

  remainingMs -= elapsed;
}

void renderState(unsigned long now) {
  if (now - lastRenderMs < renderIntervalMs) return;
  lastRenderMs = now;

  if (!running) {
    drawPauseIcon();
    return;
  }

  unsigned long totalMs = phaseTotalMs(currentPhase);
  unsigned long elapsedMs = totalMs - remainingMs;
  int progressPercent = (int)((elapsedMs * 100UL) / totalMs);

  if (currentPhase == WORK_PHASE) {
    drawWorkFrame(progressPercent);
  } else {
    drawBreakFrame(progressPercent);
  }
}

void printTimeMs(unsigned long ms) {
  unsigned long totalSec = ms / 1000;
  unsigned long minutes = totalSec / 60;
  unsigned long seconds = totalSec % 60;

  if (minutes < 10) Serial.print('0');
  Serial.print(minutes);
  Serial.print(':');
  if (seconds < 10) Serial.print('0');
  Serial.print(seconds);
}

void printStatus(unsigned long now) {
  if (now - lastSerialMs < serialIntervalMs) return;
  lastSerialMs = now;

  Serial.print("Phase: ");
  Serial.print(phaseName(currentPhase));
  Serial.print(" | State: ");
  Serial.print(running ? "RUNNING" : "PAUSED");
  Serial.print(" | Remaining: ");
  printTimeMs(remainingMs);
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  pinMode(startPauseButtonPin, INPUT_PULLUP);
  pinMode(actionButtonPin, INPUT_PULLUP);

  matrix.begin();

  resetToWorkPhase();

  Serial.println("Pomodoro timer started.");
  Serial.println("Button D2: Start/Pause");
  Serial.println("Button D3 short press: Skip phase");
  Serial.println("Button D3 long press: Reset to WORK");
}

void loop() {
  unsigned long now = millis();

  updateButton(startPauseButton, now);
  updateButton(actionButton, now);

  if (consumeShortPress(startPauseButton)) {
    toggleRunning();
  }

  if (consumeShortPress(actionButton)) {
    bool wasRunning = running;
    switchToNextPhase(wasRunning);
  }

  if (consumeLongPress(actionButton)) {
    resetToWorkPhase();
  }

  updateTimer(now);
  renderState(now);
  printStatus(now);
}

PlatformIO設定例

[env:uno_r4_wifi]
platform = renesas-ra
board = uno_r4_wifi
framework = arduino
monitor_speed = 115200

プログラムのポイント

  • Arduino_LED_Matrix を使って、オンボード12×8 LED matrix に直接描画しています。

  • 作業フェーズと休憩フェーズで表示の見え方を変えています。

  • millis() ベースでタイマーを進めているため、表示中でもボタン操作に反応しやすい構成です。

  • 開始/一時停止、スキップ、リセットを最低限の2ボタンで扱えるようにしています。

  • シリアルにも残り時間を出しているため、デバッグや動作確認がしやすいです。

動作確認

プログラムを書き込んだら、ボタンを押して動作を確認します。

基本操作

  • D2のボタンを押す→開始/一時停止

  • D3のボタンを短押し→次のフェーズへ移動

  • D3のボタンを長押し→作業フェーズの最初にリセット

表示の見え方

  • 作業中: 左からバーが増えていく

  • 休憩中: 右からバーが増えていく

  • 一時停止中: ポーズアイコン

  • 切替時: チェックマーク点滅

  • シリアル出力: 現在の状態を確認

これにより、PC画面を見なくても、今が集中時間なのか休憩時間なのかを机の上で把握しやすくなります。

発展編:通知やログ記録への拡張

オンボードマトリクスだけでも十分楽しいPoCですが、実際に使い込むなら次のような発展が考えられます。

  • ブザー追加
    フェーズ切替時に音を鳴らす

  • Wi-Fi通知
    SlackやWebhookへ「作業開始」「休憩開始」を送る

  • 実績ログ記録
    何セット完了したかを保存する

  • 長休憩モード
    4セットごとに長めの休憩へ切り替える

  • チーム共有タイマー
    全員で同じ集中時間を共有する

Uno R4 WiFiはWi-Fi機能を持っているため、単なるローカル表示だけでなく、将来的にネットワーク連携へ広げやすいのも面白いところです。

今後の展望・改善案

今回の試作は「オンボードLED matrixで進捗を見せること」が主目的ですが、さらに改善するなら次のような方向があります。

作業時間の可変化

25分固定ではなく、

  • 15分

  • 25分

  • 45分

のように切り替えられると、用途に合わせやすくなります。
また、より正確な時刻管理が必要な場合はRTCとの連携も検討できます。

アニメーション表現の強化

今はシンプルなバー表示ですが、フェーズによって

  • 「作業中は脈動する点」

  • 「休憩中はゆっくり点滅」

  • 「完了時は流れる演出」

のようにすると、より専用デバイス感が出ます。

ケース化

机上で常設するなら、ケースに入れて物理タイマーらしい見た目にすると完成度が上がります。

通知連携

Wi-Fi機能を活かして、スマホ通知やチャット通知まで連携すると、単体タイマーから小さな業務支援ツールに広がります。

ルネサス製品への展開

今回のPoCではArduino Uno R4 WiFiを利用しましたが、実際の製品化や業務利用を検討する場合は、ルネサスのRAファミリへの展開も可能です。

  • RA4M1
    シンプルな専用タイマーや状態表示機器

  • RA6シリーズ
    Wi-Fi/Bluetoothを活用した業務支援端末

  • クラウド連携構成
    作業履歴の蓄積やダッシュボードによる可視化

といった展開が考えられます。

まとめ

今回は、Arduino Uno R4 WiFiのオンボード12×8 LED matrixを使って、作業と休憩の切り替えを見える化するポモドーロタイマーを作ってみました。

今回のテーマは、単に時間を数えることではなく、

  • 集中しやすい区切りを作りたい

  • 休憩を忘れずに取りたい

  • PCやスマホに頼りすぎない専用タイマーがほしい

  • 机の上で一目で状態が分かるようにしたい

という困りごとに対して、シンプルだけれど実用的な可視化PoCを作ることにあります。

構成自体はシンプルですが、

  • Uno R4 WiFiのオンボードマトリクスを使う

  • ボタンで開始/停止/スキップ/リセットを行う

  • 作業と休憩を異なる見え方で表示する

  • シリアルでも残り時間を確認する

という流れを通して、“ただのタイマー”を少し楽しく、少し使いやすくする題材になりました。

PC画面の片隅のアプリよりも、机の上で静かに進捗を知らせてくれる専用デバイスとして使いたい時に、ちょうどよいテーマだと思います。

参考リンク