【ルネサス技術コラム】Arduino Uno R4 WiFiとOLEDで家族の在宅表示 | ブラウザ連動で父・母・子の状態を見える化してみた

在宅状況

家族それぞれが学校や仕事、外出などで行動する中、「今、誰が家にいるのか」を簡単に確認したいと思うことがあります。そんなとき、家の中で「今、誰が在宅しているか」をひと目で確認できると便利です。
今回も「エレクトロニクスを簡単に」をテーマにRenesas RA4M1搭載のArduino Uno R4 WiFi、OLEDディスプレイ、3つのボタンを使って、父・母・子の在宅/外出状態を表示するPoCを作ります。
Arduino Uno R4 WiFiは、メインMCUにRenesas RA4M1、無線機能にESP32-S3を搭載しており、Arduino Cloudとも互換性があります。
今回はこのボードの表示機能とWebサーバー機能を活用し、家族の在宅状況を見える化する仕組みを実装してみます。

さらに、Uno R4 WiFiのWebサーバー機能を使って、同じ状態をスマホやPCのブラウザでも確認できるようにします。

注意
本記事は在宅状態表示のPoCです。
実際の施錠管理や防犯用途での利用を想定したものではありません。
運用時には認証やアクセス制御を考慮してください。

目次

この記事で目指すこと

このPoCでは、次のことを行います。

  1. 父・母・子に対応する3つのボタンを用意する

  2. ボタンを押すたびに在宅/外出を切り替える

  3. OLEDに日本語と顔アイコンで表示する

  4. 同じ状態をブラウザでも確認できるようにする

小さな表示デモですが、在宅状態をデータとして扱う土台にできるのがポイントです。

このPoCのポイント

在宅/外出の情報は、表示するだけでなく他のIoT機器を動かす条件として使いやすいです。

たとえば、

  • 誰かが在宅になったら照明をON

  • 全員外出なら空調を省エネモード

  • 子が在宅になったら通知を送る

  • ブラウザ画面で別の部屋から状態確認

といった連動につなげやすくなります。

今回は入力をボタンにしていますが、後から RFID / NFC / BLE / スマホ連携に置き換えても、表示や状態管理の考え方はそのまま活かせます。

使用部品

  • Arduino Uno R4 WiFi

  • SSD1306系 OLEDディスプレイ (I2C、128x64)

  • タクトスイッチ3個

  • ブレッドボード

  • ジャンパーワイヤ

ポイント
ボタンは INPUT_PULLUP を使う前提にすると、外付け抵抗なしで配線しやすいです。 その場合、押していないとき HIGH押したとき LOW になります。

Arduino Uno R4 WiFi を使う理由

Arduino Uno R4 WiFiはRenesas RA4M1を搭載し、ESP32-S3によるWi-Fi接続にも対応しています。 今回のような在宅表示だけでなく、MQTTによる状態共有やクラウド連携、スマートホーム機器との連携へ発展させやすい点も魅力です。

配線

OLEDの接続例

部品

端子

接続先

OLED

VCC

5V または 3.3V

OLED

GND

GND

OLED

SDA

SDA

OLED

SCL

SCL

ボタンの接続例

ボタン

接続先

父ボタン

D2とGND

母ボタン

D3とGND

子ボタン

D4とGND

仕組み

全体の流れはシンプルです。

  1. ボタン入力で父・母・子の状態を切り替える

  2. 状態を Arduino 内で保持する

  3. OLED に日本語と顔アイコンで表示する

  4. Webサーバーでブラウザ向け画面を返す

[ボタン] -> [Arduino Uno R4 WiFi] -> OLED表示
                                  -> Web表示 -> [スマホ / PCブラウザ]

今回はコード量を抑えるため、ブラウザ側はページ再読み込みで状態更新するシンプル構成にしています。

サンプルコード

以下は、顔アイコン付きOLED表示ブラウザ表示をまとめたサンプルです。
※本サンプルコードは、生成AIを用いて実装しております。

#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <WiFiS3.h>

#define BTN_FATHER 2
#define BTN_MOTHER 3
#define BTN_CHILD  4

const char *WIFI_SSID = "YOUR_WIFI_SSID";
const char *WIFI_PASS = "YOUR_WIFI_PASSWORD";

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
WiFiServer server(80);

struct Member {
  const char *name;
  uint8_t pin;
  bool atHome;
  bool lastState;
  unsigned long lastMs;
};

Member members[3] = {
  {"父", BTN_FATHER, true,  HIGH, 0},
  {"母", BTN_MOTHER, false, HIGH, 0},
  {"子", BTN_CHILD,  true,  HIGH, 0},
};

String localIpText = "0.0.0.0";
bool wifiConnected = false;
const unsigned long debounceMs = 40;

int countAtHome() {
  int count = 0;
  for (auto &m : members) if (m.atHome) count++;
  return count;
}

bool waitForValidIp(unsigned long timeoutMs = 10000) {
  unsigned long start = millis();
  while (millis() - start < timeoutMs) {
    IPAddress ip = WiFi.localIP();
    if (ip != IPAddress(0, 0, 0, 0)) {
      localIpText = ip.toString();
      return true;
    }
    delay(300);
  }
  return false;
}

void showMessage(const char *l1, const char *l2 = "", const char *l3 = "") {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_unifont_t_japanese1);
  u8g2.drawUTF8(0, 18, l1);
  u8g2.drawUTF8(0, 36, l2);
  u8g2.drawUTF8(0, 54, l3);
  u8g2.sendBuffer();
}

void drawFace(int x, int y, bool atHome) {
  u8g2.drawCircle(x + 8, y + 8, 7, U8G2_DRAW_ALL);
  u8g2.drawDisc(x + 5, y + 6, 1, U8G2_DRAW_ALL);
  u8g2.drawDisc(x + 11, y + 6, 1, U8G2_DRAW_ALL);
  if (atHome) {
    u8g2.drawLine(x + 4, y + 11, x + 5, y + 12);
    u8g2.drawLine(x + 5, y + 12, x + 11, y + 12);
    u8g2.drawLine(x + 11, y + 12, x + 12, y + 11);
    u8g2.drawDisc(x + 18, y + 3, 3, U8G2_DRAW_ALL);
  } else {
    u8g2.drawLine(x + 5, y + 11, x + 11, y + 11);
    u8g2.drawCircle(x + 18, y + 3, 3, U8G2_DRAW_ALL);
  }
}

void drawCard(int x, const char *name, bool atHome) {
  u8g2.drawRFrame(x, 18, 38, 44, 3);
  u8g2.setFont(u8g2_font_unifont_t_japanese1);
  u8g2.drawUTF8(x + 14, 28, name);
  drawFace(x + 6, 30, atHome);
  u8g2.drawRFrame(x + 4, 49, 30, 10, 2);
  u8g2.drawUTF8(x + 7, 57, atHome ? "在宅" : "外出");
}

void drawScreen() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_unifont_t_japanese1);
  u8g2.drawUTF8(0, 12, "在宅確認");
  char buf[8];
  snprintf(buf, sizeof(buf), "%d/3", countAtHome());
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.drawStr(104, 11, buf);
  drawCard(2, members[0].name, members[0].atHome);
  drawCard(45, members[1].name, members[1].atHome);
  drawCard(88, members[2].name, members[2].atHome);
  u8g2.sendBuffer();
}

void updateButton(Member &m) {
  bool reading = digitalRead(m.pin);
  if (reading != m.lastState) {
    m.lastMs = millis();
    m.lastState = reading;
  }
  if ((millis() - m.lastMs) > debounceMs && reading == LOW) {
    m.atHome = !m.atHome;
    while (digitalRead(m.pin) == LOW) delay(1);
  }
}

void connectWiFi() {
  showMessage("Wi-Fi接続中...");
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    delay(3000);
  }
  if (WiFi.status() == WL_CONNECTED && waitForValidIp()) {
    wifiConnected = true;
    server.begin();
    showMessage("Wi-Fi接続OK", localIpText.c_str());
  } else {
    wifiConnected = false;
    showMessage("Wi-Fi未接続", "OLED表示のみ");
    delay(1500);
  }
}

String pageHtml() {
  String html = R"HTML(
<!DOCTYPE html><html lang="ja"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="2">
<title>在宅状況</title>
<style>
body{font-family: sans-serif;background:#f6f7fb;margin:0;padding: 20px; color:#222}
.wrap{max-width:760px; margin:auto}
h1{font-size:24px}
.count{font-size: 28px; font-weight:700;color:#2563eb;margin:12px 0 20px}
.grid{display:grid; grid-template-columns:repeat(auto-fit, minmax(160px, 1fr)); gap:16px}
.card{background:#fff;border:1px solid #dde3ea; border-radius: 16px;padding:18px; text-align:center}
.face{font-size:42px}
.name{font-size:20px; font-weight: 700;margin:8px 0}
.badge{display:inline-block;padding:6px 12px; border-radius: 999px; font-weight:700}
.home{background:#dcfce7;color:#166534}
.away{background:#e2e8f0;color:#475569}
</style></head><body><div class="wrap">
<h1>在宅状況</h1>
<div class="count">)HTML";
  html += String(countAtHome()) + "/3 在宅</div><div class='grid'>";
  for (auto &m : members) {
    html += "<div class='card'><div class='face'>";
    html += m.atHome ? "😊" : "🏠";
    html += "</div><div class='name'>";
    html += m.name;
    html += "</div><div class='badge ";
    html += m.atHome ? "home'>在宅" : "away'>外出";
    html += "</div></div>";
  }
  html += "</div></div></body></html>";
  return html;
}

void handleClient() {
  if (!wifiConnected) return;
  WiFiClient client = server.available();
  if (!client) return;
  while (client.connected() && !client.available()) delay(1);
  String req = client.readStringUntil('\r');
  client.readStringUntil('\n');
  while (client.available()) {
    String line = client.readStringUntil('\n');
    if (line == "\r") break;
  }
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html; charset=utf-8");
  client.println("Connection: close");
  client.println();
  client.print(pageHtml());
  delay(1);
  client.stop();
}

void setup() {
  pinMode(BTN_FATHER, INPUT_PULLUP);
  pinMode(BTN_MOTHER, INPUT_PULLUP);
  pinMode(BTN_CHILD,  INPUT_PULLUP);
  Wire.begin();
  u8g2.begin();
  showMessage("起動中...");
  delay(500);
  connectWiFi();
}

void loop() {
  for (auto &m : members) updateButton(m);
  handleClient();
  drawScreen();
  delay(50);
}

PlatformIO設定例

WiFiS3 は Uno R4 WiFi の環境側に含まれるため、ここでは lib_deps に追加していません。

[env:uno_r4_wifi]
platform = renesas-ra
board = uno_r4_wifi
framework = arduino
monitor_speed = 115200
lib_deps =
  olikraus/U8g2

表示内容の見方

OLEDでは、父・母・子を3つのカードで表示します。

  • 笑顔 + 在宅

  • 無表情 + 外出

  • 右上の 2/3 は在宅人数

OLEDに表示されたIPアドレスへアクセスします。

[http://192.168.xxx.xxx/](http://192.168.xxx.xxx/)

在宅状況

IoT連動の発展例

この在宅状態は、次のようなIoT連動に広げやすいです。

  • 誰かが在宅なら照明ON

  • 全員外出なら空調を省エネモード

  • 子が在宅になったら通知

  • 在宅人数でダッシュボード表示を切り替え

PoCとしてはシンプルですが、状態データの出発点として使いやすい題材です。

調整ポイント

1.Wi-Fi設定

const char *WIFI_SSID = "YOUR_WIFI_SSID";
const char *WIFI_PASS = "YOUR_WIFI_PASSWORD";

2.初期状態

{"父", BTN_FATHER, true,  HIGH, 0},
{"母", BTN_MOTHER, false, HIGH, 0},
{"子", BTN_CHILD,  true,  HIGH, 0},

ここで初期の在宅/外出を変更できます。

注意点

  • ボタンは INPUT_PULLUP なのでGND側配線です

  • 再起動すると状態は初期値に戻ります

  • ブラウザ表示は同じWi-Fiネットワーク内向けです

改善案

  • 在宅状態をEEPROMへ保存

  • MQTTで状態配信

  • LINE通知連携

  • RFID/NFCによる自動判定

  • BLEビーコンによる入退室検出

  • 今回のPoCはArduino Uno R4 WiFiを用いていますが、同様の仕組みはRenesas RAファミリへ展開可能です。
    たとえばRA6シリーズWi-Fi/BLE対応モジュールを利用することで、低消費電力化や常時接続型のホームIoTデバイスとして発展させることができます。将来的にはMQTTによる状態共有や、スマートホームサービスとの連携も考えられます。

まとめ

今回は、Arduino Uno R4 WiFiとOLEDを使って、父・母・子の在宅/外出を顔アイコンと日本語で表示し、ブラウザでも確認できるPoCを作りました。

ポイントは次の通りです。

  • 3つのボタンで家族ごとの状態を切り替える

  • OLEDに日本語と顔アイコンで見やすく表示する

  • ブラウザでも同じ状態を確認できる

  • 将来的に照明、空調、通知などのIoT連動へ広げやすい

まずはこの形で動かし、次にRFIDやNFC、BLEなどへ入力方法を置き換えていくと発展させやすいです。

参考リンク