PythonとArduinoで作るブラウザIoTダッシュボード!Dashでセンサーデータをスマホからも確認する方法
「Arduinoのセンサーデータ、ブラウザのかっこいい画面でリアルタイムに見たい!」
そう思ったことはありませんか?😊
PythonのDashライブラリを使えば、ArduinoのセンサーデータをWebブラウザ上のダッシュボードとして表示できます。しかもスマホからもLAN内でアクセス可能。専用サーバーやクラウドは不要で、手元のPCだけで完結します。
シリアル通信の基本やMatplotlibによる簡易グラフ表示については、別記事で詳しく解説しています。この記事では「Dashを使ったブラウザダッシュボード構築」に特化して、Dash固有の設計ポイントを深く掘り下げます。
📋 この記事でできるようになること

- threadingを使ってシリアル受信をメインスレッドから分離する
- Dashの
dcc.Intervalとコールバックでリアルタイム更新を実現する - 現在値カードUI(温度・湿度を大きく表示)をDashで作る
- PC・スマホ両方からLAN内でダッシュボードにアクセスする
対象読者・難易度
- ✅ Arduinoでセンサーの読み取りができる方
- ✅ Pythonの基本文法がわかる方(ループ・関数・ライブラリのインポートができる程度)
- ✅ pyserialでArduinoからデータ受信の経験がある方(未経験の方は後述の関連記事を先にどうぞ)
- 🔰 難易度:中級
「むずかしそう」を「できそう」に変えるのが目標です。一緒に進めていきましょう!
🔧 事前準備とライブラリのインストール
シリアル通信・センサーの基礎は関連記事で確認しよう
pyserialの導入方法やArduinoとPCをシリアルで繋ぐ基礎については、以下の記事でゼロから解説しています。この記事ではそれらを前提として進めます。
- 👉 PythonとArduinoをシリアル通信で連携させる方法【pyserial入門・サンプルコード付き】
- 👉 Python × Arduino シリアル通信入門!センサーデータをリアルタイム可視化する方法
Pythonライブラリのインストール
今回のダッシュボード構築に必要なライブラリをインストールしましょう。
pip install pyserial dash plotly
- pyserial:ArduinoのシリアルポートからPythonでデータを受信するために必須
- dash:Plotlyが開発したブラウザベースのダッシュボードフレームワーク
- plotly:Dashの内部で使われるインタラクティブグラフライブラリ(dashと一緒にインストールされることが多い)
Arduino側のスケッチ(簡易確認)
Arduino(DHT11センサー)側は、「温度,湿度」のCSV形式でシリアル送信する以下のスケッチを使います。詳細な解説は上記の関連記事を参照してください。
// Arduino スケッチ:DHT11センサーのデータをシリアル送信
#include <DHT.h>
#define DHTPIN 2
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(9600);
dht.begin();
}
void loop() {
delay(2000); // 2秒ごとに計測
float humidity = dht.readHumidity();
float temperature = dht.readTemperature();
if (isnan(humidity) || isnan(temperature)) {
Serial.println("ERROR");
return;
}
// 「温度,湿度」の形式でCSV送信
Serial.print(temperature);
Serial.print(",");
Serial.println(humidity);
}
Arduino IDEのシリアルモニタに「25.50,60.20」のような数値が流れてくればOKです!✅
なぜthreadingでシリアル受信を分離するのか
Dashダッシュボードを作るうえで、最初につまずきやすいのが「シリアル受信とDashサーバーの共存」という問題です。
Dashはサーバーとして動作し、ブラウザからのリクエストを待ち受けます。一方、Arduinoからのシリアルデータは非同期に流れ込んできます。この2つを同一スレッドで順番に処理しようとすると、どちらかの処理がブロックされてしまいます。
解決策は「シリアル受信を別スレッドに分離する」ことです。Pythonのthreadingモジュールを使えば、バックグラウンドでシリアルデータを読み続けながら、メインスレッドでDashサーバーを動かせます。
import threading
from collections import deque
import serial
import time
# --- 設定 ---
PORT = 'COM3' # ← 自分の環境に合わせて変更(Mac: '/dev/tty.usbmodemXXXX')
BAUD_RATE = 9600
MAX_POINTS = 60 # グラフに保持するデータ点数(2秒間隔×60=約2分)
# スレッド間で共有するデータキュー
temps = deque([None] * MAX_POINTS, maxlen=MAX_POINTS)
hums = deque([None] * MAX_POINTS, maxlen=MAX_POINTS)
# 最新値を保持する変数(現在値カード用)
latest = {'temp': '--', 'hum': '--'}
# スレッドセーフのためにLockを使う
data_lock = threading.Lock()
def serial_reader():
"""バックグラウンドでシリアルポートを読み続けるスレッド関数"""
try:
ser = serial.Serial(PORT, BAUD_RATE, timeout=1)
time.sleep(2) # Arduino起動待ち
print(f"シリアル接続成功: {PORT}")
except serial.SerialException as e:
print(f"シリアル接続失敗: {e}")
return
while True:
try:
line = ser.readline().decode('utf-8').strip()
if ',' in line:
parts = line.split(',')
if len(parts) == 2:
temp = float(parts[0])
hum = float(parts[1])
with data_lock: # ロックしてから書き込む
temps.append(temp)
hums.append(hum)
latest['temp'] = f"{temp:.1f}"
latest['hum'] = f"{hum:.1f}"
except (ValueError, UnicodeDecodeError):
pass # パース失敗はスキップ
except serial.SerialException:
print("シリアル接続が切断されました")
break
# バックグラウンドスレッドを起動(daemon=Trueでメインが終わると自動終了)
serial_thread = threading.Thread(target=serial_reader, daemon=True)
serial_thread.start()
ここが重要です👇
daemon=Trueを指定することで、Dashサーバーを停止(Ctrl+C)すると受信スレッドも自動で終了しますthreading.Lock()でデータの読み書きを保護しています。Dashのコールバックがデータを読む瞬間と、受信スレッドが書き込む瞬間が重ならないようにするためですdequeは最大件数を超えると古いデータを自動で削除するため、メモリが無制限に増えません
Dashアプリの基本構造:レイアウトを作る
Dashアプリは大きく「レイアウト(見た目の定義)」と「コールバック(動きの定義)」の2つに分かれます。まずはレイアウトを作りましょう。
import dash
from dash import dcc, html
import plotly.graph_objs as go
app = dash.Dash(__name__)
app.layout = html.Div([
# タイトルバー
html.H1(
'Arduino IoT ブラウザダッシュボード',
style={
'textAlign': 'center',
'color': '#2c3e50',
'fontFamily': 'sans-serif',
'marginBottom': '20px'
}
),
# 現在値カードのエリア(温度・湿度を大きく表示)
html.Div([
html.Div([
html.P('現在の温度', style={'fontSize': '14px', 'color': '#888', 'margin': '0'}),
html.H2(id='card-temp', children='-- ℃',
style={'fontSize': '48px', 'color': '#e74c3c', 'margin': '5px 0'}),
], style={
'background': '#fff',
'borderRadius': '12px',
'padding': '20px 30px',
'boxShadow': '0 2px 10px rgba(0,0,0,.1)',
'textAlign': 'center',
'flex': '1'
}),
html.Div([
html.P('現在の湿度', style={'fontSize': '14px', 'color': '#888', 'margin': '0'}),
html.H2(id='card-hum', children='-- %',
style={'fontSize': '48px', 'color': '#3498db', 'margin': '5px 0'}),
], style={
'background': '#fff',
'borderRadius': '12px',
'padding': '20px 30px',
'boxShadow': '0 2px 10px rgba(0,0,0,.1)',
'textAlign': 'center',
'flex': '1'
}),
], style={'display': 'flex', 'gap': '20px', 'marginBottom': '20px'}),
# 温度グラフ
dcc.Graph(id='graph-temp'),
# 湿度グラフ
dcc.Graph(id='graph-hum'),
# 定期更新タイマー(2000ms = 2秒ごとにコールバックを呼ぶ)
dcc.Interval(
id='interval-update',
interval=2000,
n_intervals=0
),
], style={'backgroundColor': '#f0f2f5', 'minHeight': '100vh', 'padding': '20px'})
レイアウトのポイントをまとめます👇
id='card-temp'・id='card-hum':現在値カード。コールバックからこのIDに値を流し込みますdcc.Graph(id='graph-temp'):Plotlyのインタラクティブグラフを埋め込む場所。同じくコールバックで図を更新しますdcc.Interval:これがDashのリアルタイム更新の核心です。interval=2000で2秒ごとにコールバックを自動発火させます
コールバック設計:dcc.Intervalで自動更新する
Dashのリアルタイム更新はdcc.Interval→コールバック関数という流れで実現します。n_intervals(タイマーが何回発火したかのカウント)が変化するたびにコールバックが呼ばれ、グラフと現在値カードが更新される仕組みです。
from dash import Input, Output
import plotly.graph_objs as go
@app.callback(
Output('card-temp', 'children'),
Output('card-hum', 'children'),
Output('graph-temp', 'figure'),
Output('graph-hum', 'figure'),
Input('interval-update', 'n_intervals')
)
def update_dashboard(n):
"""dcc.Intervalが発火するたびに呼ばれる更新関数"""
# Lockしてからデータを安全に読み取る
with data_lock:
temp_list = list(temps)
hum_list = list(hums)
current_temp = latest['temp']
current_hum = latest['hum']
# --- 現在値カードの文字列 ---
card_temp_text = f"{current_temp} ℃"
card_hum_text = f"{current_hum} %"
# --- 温度グラフ ---
fig_temp = go.Figure()
fig_temp.add_trace(go.Scatter(
y=[v for v in temp_list if v is not None],
mode='lines',
line=dict(color='#e74c3c', width=2),
name='温度 (℃)'
))
fig_temp.update_layout(
title='温度の推移(直近60サンプル・約2分)',
yaxis=dict(title='温度 (℃)', range=[0, 50]),
xaxis=dict(title='サンプル番号'),
paper_bgcolor='#fff',
plot_bgcolor='#f8f9fa',
margin=dict(l=40, r=20, t=40, b=40),
showlegend=False
)
# --- 湿度グラフ ---
fig_hum = go.Figure()
fig_hum.add_trace(go.Scatter(
y=[v for v in hum_list if v is not None],
mode='lines',
line=dict(color='#3498db', width=2),
name='湿度 (%)'
))
fig_hum.update_layout(
title='湿度の推移(直近60サンプル・約2分)',
yaxis=dict(title='湿度 (%)', range=[0, 100]),
xaxis=dict(title='サンプル番号'),
paper_bgcolor='#fff',
plot_bgcolor='#f8f9fa',
margin=dict(l=40, r=20, t=40, b=40),
showlegend=False
)
return card_temp_text, card_hum_text, fig_temp, fig_hum
コールバック設計の大事なポイントです👇
@app.callback:デコレータで「このInputが変わったらこのOutputを更新する」という関係を宣言します。Dashはこの宣言を見て自動的にコールバックを配線します- 複数のOutputをタプルで返す:
return card_temp_text, card_hum_text, fig_temp, fig_humの順番が、Outputの宣言順と対応しています data_lockでロック:受信スレッドとコールバックが同時にデータにアクセスしないよう、読み取り時もLockを取りますv is not Noneでフィルタ:初期化時にNoneを入れたdequeから、未受信のNoneを除外してグラフ描画します
アラートラインを追加する(応用:閾値超過を可視化)
温度が一定値を超えたらグラフ上に警告ラインを表示すると、モニタリングとして実用的になります。Plotlyではadd_hlineで簡単に追加できます。
# 温度グラフに警告ライン(35℃)を追加する例
TEMP_ALERT = 35 # ℃
fig_temp.add_hline(
y=TEMP_ALERT,
line_dash='dash',
line_color='orange',
annotation_text=f'警告ライン ({TEMP_ALERT}℃)',
annotation_position='bottom right'
)
さらに現在値カードの色も動的に変えると視覚的にわかりやすくなります。
# コールバック内で条件によって文字色を変える
card_color = '#e74c3c' if current_temp != '--' and float(current_temp) >= TEMP_ALERT else '#2c3e50'
コールバック側にOutput('card-temp', 'style')を追加し、色文字列だけでなく{'fontSize': '48px', 'color': card_color, 'margin': '5px 0'}のようにstyle辞書ごと返します(色だけを返すと他のスタイルが失われてレイアウトが崩れます)。これで超過時に赤くハイライトするカードUIが作れます。
スマホからもアクセス!LAN内でダッシュボードを共有する
Dashダッシュボードの大きなメリットのひとつが、同じWi-Fi(LAN)内ならスマホやタブレットからもブラウザでアクセスできる点です。
サーバー起動時の設定
デフォルトではlocalhost(127.0.0.1)のみでリスンするため、スマホからはアクセスできません。host='0.0.0.0'を指定することで、PCのLAN IPアドレスに対してもリクエストを受け付けるようになります。
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8050, debug=False)
⚠️ debug=Falseにしているのは理由があります。debug=TrueはDashのコードリローダーが動くため、スレッドが二重起動されてシリアルポートの競合エラーが起きることがあります。本番稼働時は必ずdebug=Falseにしましょう。
PCのIPアドレスを確認する
# Windowsの場合(コマンドプロンプト)
ipconfig
# → 「IPv4 アドレス」を確認(例: 192.168.1.10)
# Mac / Linuxの場合(ターミナル)
ifconfig | grep 'inet '
# → en0やwlan0の inet を確認(例: 192.168.1.10)
スマホからアクセスする
PCと同じWi-Fiに繋いだスマホのブラウザで以下のURLを開くだけです。
http://192.168.1.10:8050
# ← PCのIPアドレスとポート番号8050に合わせて変更してください
Plotlyのグラフはスマホのタッチ操作にも対応しており、ピンチズームや長押しでデータ値の確認ができます。これがMatplotlibにはできないDashならではの強みです。
コード全体をまとめて確認する
ここまでのすべてのコードを1つのファイルにまとめた完成版です。PORTだけ自分の環境に合わせて変更してください。
# dashboard.py ─ Arduino Dash ブラウザダッシュボード 完成版
import threading
import time
from collections import deque
import serial
import dash
from dash import dcc, html, Input, Output
import plotly.graph_objs as go
# =====================
# 設定
# =====================
PORT = 'COM3' # ← 自分の環境に合わせて変更
BAUD_RATE = 9600
MAX_POINTS = 60
TEMP_ALERT = 35 # 温度警告閾値(℃)
# =====================
# 共有データ
# =====================
temps = deque([None] * MAX_POINTS, maxlen=MAX_POINTS)
hums = deque([None] * MAX_POINTS, maxlen=MAX_POINTS)
latest = {'temp': '--', 'hum': '--'}
data_lock = threading.Lock()
# =====================
# シリアル受信スレッド
# =====================
def serial_reader():
try:
ser = serial.Serial(PORT, BAUD_RATE, timeout=1)
time.sleep(2)
print(f'シリアル接続成功: {PORT}')
except serial.SerialException as e:
print(f'シリアル接続失敗: {e}')
return
while True:
try:
line = ser.readline().decode('utf-8').strip()
if ',' in line:
parts = line.split(',')
if len(parts) == 2:
temp = float(parts[0])
hum = float(parts[1])
with data_lock:
temps.append(temp)
hums.append(hum)
latest['temp'] = f'{temp:.1f}'
latest['hum'] = f'{hum:.1f}'
except (ValueError, UnicodeDecodeError):
pass
except serial.SerialException:
print('シリアル接続が切断されました')
break
serial_thread = threading.Thread(target=serial_reader, daemon=True)
serial_thread.start()
# =====================
# Dashアプリ定義
# =====================
app = dash.Dash(__name__)
card_style = {
'background': '#fff',
'borderRadius': '12px',
'padding': '20px 30px',
'boxShadow': '0 2px 10px rgba(0,0,0,.1)',
'textAlign': 'center',
'flex': '1'
}
app.layout = html.Div([
html.H1('Arduino IoT ブラウザダッシュボード',
style={'textAlign': 'center', 'color': '#2c3e50', 'fontFamily': 'sans-serif'}),
html.Div([
html.Div([
html.P('現在の温度', style={'fontSize': '14px', 'color': '#888', 'margin': '0'}),
html.H2(id='card-temp', children='-- ℃',
style={'fontSize': '48px', 'color': '#e74c3c', 'margin': '5px 0'}),
], style=card_style),
html.Div([
html.P('現在の湿度', style={'fontSize': '14px', 'color': '#888', 'margin': '0'}),
html.H2(id='card-hum', children='-- %',
style={'fontSize': '48px', 'color': '#3498db', 'margin': '5px 0'}),
], style=card_style),
], style={'display': 'flex', 'gap': '20px', 'marginBottom': '20px'}),
dcc.Graph(id='graph-temp'),
dcc.Graph(id='graph-hum'),
dcc.Interval(id='interval-update', interval=2000, n_intervals=0),
], style={'backgroundColor': '#f0f2f5', 'minHeight': '100vh', 'padding': '20px'})
# =====================
# コールバック
# =====================
@app.callback(
Output('card-temp', 'children'),
Output('card-hum', 'children'),
Output('graph-temp', 'figure'),
Output('graph-hum', 'figure'),
Input('interval-update', 'n_intervals')
)
def update_dashboard(n):
with data_lock:
temp_list = list(temps)
hum_list = list(hums)
current_temp = latest['temp']
current_hum = latest['hum']
temp_vals = [v for v in temp_list if v is not None]
hum_vals = [v for v in hum_list if v is not None]
# 温度グラフ
fig_temp = go.Figure()
fig_temp.add_trace(go.Scatter(y=temp_vals, mode='lines',
line=dict(color='#e74c3c', width=2), name='温度'))
fig_temp.add_hline(y=TEMP_ALERT, line_dash='dash', line_color='orange',
annotation_text=f'警告ライン ({TEMP_ALERT}℃)',
annotation_position='bottom right')
fig_temp.update_layout(title='温度の推移(直近60サンプル・約2分)',
yaxis=dict(title='温度 (℃)', range=[0, 50]),
xaxis=dict(title='サンプル番号'),
paper_bgcolor='#fff', plot_bgcolor='#f8f9fa',
margin=dict(l=40, r=20, t=40, b=40), showlegend=False)
# 湿度グラフ
fig_hum = go.Figure()
fig_hum.add_trace(go.Scatter(y=hum_vals, mode='lines',
line=dict(color='#3498db', width=2), name='湿度'))
fig_hum.update_layout(title='湿度の推移(直近60サンプル・約2分)',
yaxis=dict(title='湿度 (%)', range=[0, 100]),
xaxis=dict(title='サンプル番号'),
paper_bgcolor='#fff', plot_bgcolor='#f8f9fa',
margin=dict(l=40, r=20, t=40, b=40), showlegend=False)
return f'{current_temp} ℃', f'{current_hum} %', fig_temp, fig_hum
# =====================
# 起動
# =====================
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8050, debug=False)
実行コマンドはこちら👇
python dashboard.py
ターミナルにDash is running on http://0.0.0.0:8050/と表示されたら成功です。ブラウザでhttp://localhost:8050を開いてみましょう!
トラブルシューティング:よくあるエラーと対処法
❶ シリアルポートが見つからない / 接続できない
Arduinoが接続されているポートを確認しましょう。Windowsはデバイスマネージャーで「COM〇」、Mac/Linuxはls /dev/tty.*で確認できます。PORT変数をそのポート名に変更してください。
# Mac / Linuxで接続ポートを調べる
ls /dev/tty.*
# 例: /dev/tty.usbmodem14201
❷ debug=Trueにしたらシリアルポートエラーが出る
Dashのdebug=Trueはリローダーがプロセスを2回起動するため、受信スレッドが二重に立ち上がりシリアルポートの競合が起きます。スレッドを使う場合は必ずdebug=Falseにしてください。
❸ グラフが更新されない・データがNoneのまま
スケッチの送信形式(カンマ区切り・改行あり)を確認してください。Arduino IDEのシリアルモニタで「25.50,60.20」のような形式で流れているかチェックします。また、BAUD_RATEがArduino側と一致しているかも確認しましょう。
❹ スマホからアクセスできない
PCとスマホが同じWi-Fiに接続されているか確認してください。接続できていてもWindowsファイアウォールがポート8050をブロックしている場合があります。Windowsの「受信の規則」でポート8050を許可するか、一時的にファイアウォールをオフにして試してみましょう。
MatplotlibとDashの違いを整理しよう
「Matplotlibじゃダメなの?」という疑問をお持ちの方もいると思います。両者の特徴を整理しておきましょう。
| 観点 | Matplotlib | Dash(Plotly) |
|---|---|---|
| 動作環境 | デスクトップウィンドウ | Webブラウザ |
| 他端末アクセス | ❌ 不可 | ✅ LAN内なら可 |
| スマホ対応 | ❌ 不可 | ✅ タッチ操作対応 |
| インタラクティブ性 | △ 限定的 | ✅ ズーム・ホバー等 |
| UIカスタマイズ | △ グラフ中心 | ✅ カード・ボタン等も自由 |
| 手軽さ・学習コスト | ✅ 低い | △ やや高め |
「家のどこからでもセンサーデータを見たい」「スマホでチェックしたい」という用途には、Dashが圧倒的に向いています。一方、「とにかく手早くグラフを確認したい」という用途ではMatplotlibで十分です。
発展:複数センサーのデータを1つのダッシュボードに集約する
今回はDHT11の温湿度2系統でしたが、同じ構造を拡張すれば複数センサーのデータを1画面に集約できます。
# 複数センサー対応のデータ管理例
data_store = {
'temp': deque([None] * MAX_POINTS, maxlen=MAX_POINTS),
'hum': deque([None] * MAX_POINTS, maxlen=MAX_POINTS),
'lux': deque([None] * MAX_POINTS, maxlen=MAX_POINTS), # 照度センサー追加
}
ArduinoのCSV形式を「温度,湿度,照度」の3値に拡張し、受信スレッドと辞書を合わせて更新すればOKです。コールバックのOutputにグラフを追加するだけで、ダッシュボードを拡張できます。
より本格的なIoTモニタリングシステムの全体像については、以下の記事も参考にしてみてください。
👉 ArduinoとPythonで作るIoT温湿度モニタリングシステム【ゼロから動かす入門ガイド】
まとめ
この記事では、PythonのDashライブラリを使ってArduinoセンサーデータをブラウザダッシュボードとして表示する方法を解説しました。
- ✅ threadingで受信スレッドを分離することでDashサーバーとシリアル受信が干渉しない
- ✅
dcc.Intervalとコールバックの組み合わせがDashリアルタイム更新の核心 - ✅ 現在値カードUIでひと目で状況がわかるダッシュボードが実現できる
- ✅
host='0.0.0.0'で同一LAN内のスマホからもアクセス可能 - ✅ Matplotlibにはできないブラウザベースのインタラクティブ操作がDashの強み
「Arduinoのデータを見る」から「いつでもどこでも確認できるIoTダッシュボード」への進化、ぜひ体験してみてください!🚀
こちらも読まれています
📡 Arduinoをもっと深く学ぼう!
Arduino・ラズパイ・ロボットプログラミングを体系的に学びたい方へ。おすすめのUdemyコースや電子部品もまとめています。
📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。





