Python

Pythonで作るリアルタイムIoTダッシュボード「IoTDashboardApp」完全解説|MQTT対応の可視化アプリをTkinterで構築する方法(App006)

tyamada

1. はじめに

Python だけで、MQTT・CSV・ランダム値をリアルタイムに可視化できる IoT ダッシュボードアプリを作れるとしたらどうでしょうか。
今回紹介する 「IoTDashboardApp」 は、Tkinter・matplotlib・MQTT を組み合わせて、センサーデータのリアルタイム監視・タブ切り替え・アラート判定まで行える、学習にも実務にも使える構成になっています。

「Pythonで IoT ダッシュボードを作ってみたい」「MQTT を扱ってみたい」そんな方に最適なサンプルアプリです。


2. 理由

IoT 開発において、センサーデータをリアルタイムに “可視化” する仕組み は欠かせません。
しかし商用のダッシュボードツールは高価で、個人開発では扱いにくいことも多いです。

その点、Python なら

  • GUI → Tkinter
  • グラフ描画 → matplotlib
  • 通信 → MQTT(paho-mqtt)

といった定番ライブラリの組み合わせで、意外なほど簡単に本格的なダッシュボードを作れます。

特に本アプリでは

  • データソースを「Random / CSV / MQTT」から切り替え可能
  • Notebook(タブ)で複数センサーを管理
  • 1 秒ごとの自動更新ループ
  • 閾値アラート表示付き
  • リアルタイム MQTT 受信に対応

といった “実運用の役に立つ” デザインで構成しています。


3. 具体例

■ IoTDashboardApp の画面イメージ

ソースコードを全て表示
# IoT Dashboard Tkinter (DataSource選択対応版)
# ---- すべてのセンサーが「Random / CSV / MQTT」から選んだデータソースのみ更新 ----

import tkinter as tk
from tkinter import ttk, filedialog
import random
import math
import pandas as pd
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import threading
import paho.mqtt.client as mqtt

# -------------------------------------------------------------
# SensorTab: センサーごとのタブ(データソース選択に対応)
# -------------------------------------------------------------
class SensorTab(ttk.Frame):
    def __init__(self, parent, name, app):
        super().__init__(parent)
        self.name = name
        self.app = app

        self.data = []
        self.max_points = 50

        # データソース選択
        self.datasource = tk.StringVar(value="Random")

        # CSVデータ
        self.csv_data = None
        self.csv_index = 0

        # レイアウト
        self.build_ui()

        # 更新ループ開始
        self.after(1000, self.update_loop)

    # -----------------------------------------------------
    def build_ui(self):
        top = ttk.Frame(self)
        top.pack(fill=tk.X, pady=5)

        ttk.Label(top, text=f"Sensor: {self.name}").pack(side=tk.LEFT, padx=5)

        ttk.Label(top, text="Data Source:").pack(side=tk.LEFT)
        cmb = ttk.Combobox(top, textvariable=self.datasource,
                           values=["Random", "CSV", "MQTT"], width=10)
        cmb.pack(side=tk.LEFT, padx=5)

        # CSV読み込みボタン
        btn = ttk.Button(top, text="Load CSV", command=self.load_csv)
        btn.pack(side=tk.LEFT, padx=5)

        # 閾値設定
        self.threshold = tk.DoubleVar(value=80.0)
        ttk.Label(top, text="Threshold:").pack(side=tk.LEFT, padx=5)
        ttk.Entry(top, textvariable=self.threshold, width=6).pack(side=tk.LEFT)

        # グラフ
        fig = Figure(figsize=(5,2.5))
        self.ax = fig.add_subplot(111)
        self.ax.set_title(self.name)
        self.line, = self.ax.plot([], [])
        self.ax.grid(True)

        self.canvas = FigureCanvasTkAgg(fig, master=self)
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

    # -----------------------------------------------------
    def load_csv(self):
        path = filedialog.askopenfilename(filetypes=[("CSV Files","*.csv")])
        if not path:
            return
        df = pd.read_csv(path)
        # 数値列を自動検出
        num_cols = df.select_dtypes(include=['number']).columns
        if len(num_cols) == 0:
            return
        self.csv_data = df[num_cols[0]].tolist()
        self.csv_index = 0
        self.datasource.set("CSV")

    # -----------------------------------------------------
    def update_loop(self):
        src = self.datasource.get()

        if src == "Random":
            value = random.uniform(0, 100)

        elif src == "CSV":
            if self.csv_data:
                value = self.csv_data[self.csv_index]
                self.csv_index = (self.csv_index + 1) % len(self.csv_data)
            else:
                value = None

        elif src == "MQTT":
            # MQTTは値をpush_valueで外部から受信するためここでは更新しない
            value = None

        else:
            value = None

        if value is not None:
            self.push_value(value)

        self.after(1000, self.update_loop)

    # -----------------------------------------------------
    def push_value(self, val):
        self.data.append(val)
        if len(self.data) > self.max_points:
            self.data = self.data[-self.max_points:]

        # グラフ更新
        self.line.set_data(range(len(self.data)), self.data)
        self.ax.set_xlim(0, self.max_points)
        self.ax.set_ylim(min(self.data)-5, max(self.data)+5)
        self.canvas.draw()

        # 閾値アラート
        if val > self.threshold.get():
            self.ax.set_facecolor("mistyrose")
        else:
            self.ax.set_facecolor("white")


# -------------------------------------------------------------
# IoT Dashboard Main App
# -------------------------------------------------------------
class IoTDashboardApp:
    def __init__(self, root):
        self.root = root
        self.root.title("IoT Dashboard - DataSource Switch Version")

        # Notebook
        self.nb = ttk.Notebook(root)
        self.nb.pack(fill=tk.BOTH, expand=True)

        self.sensors = []
        for i in range(1, 4):
            tab = SensorTab(self.nb, f"Sensor-{i}", self)
            self.sensors.append(tab)
            self.nb.add(tab, text=f"Sensor-{i}")

        # MQTT クライアント
        self.mqtt_client = None

        # MQTT接続ボタン
        btn = ttk.Button(root, text="Connect MQTT", command=self.setup_mqtt)
        btn.pack(pady=5)

    # -----------------------------------------------------
    def setup_mqtt(self, broker="localhost", port=1883):
        if self.mqtt_client:
            return

        self.mqtt_client = mqtt.Client()
        self.mqtt_client.on_connect = self._on_mqtt_connect
        self.mqtt_client.on_message = self._on_mqtt_message

        t = threading.Thread(target=lambda: self._mqtt_thread(broker, port), daemon=True)
        t.start()

    # -----------------------------------------------------
    def _mqtt_thread(self, broker, port):
        try:
            self.mqtt_client.connect(broker, port, 60)
            self.mqtt_client.loop_forever()
        except:
            pass

    # -----------------------------------------------------
    def _on_mqtt_connect(self, client, userdata, flags, rc):
        for s in self.sensors:
            topic = f"sensors/{s.name}"
            client.subscribe(topic)

    # -----------------------------------------------------
    def _on_mqtt_message(self, client, userdata, msg):
        try:
            sname = msg.topic.split("/")[1]
            value = float(msg.payload.decode("utf-8"))
        except:
            return

        # 対象センサーへ値を反映
        for s in self.sensors:
            if s.name == sname:
                self.root.after(0, lambda v=value, ss=s: ss.push_value(v))
                break


# -------------------------------------------------------------
if __name__ == "__main__":
    root = tk.Tk()
    app = IoTDashboardApp(root)
    root.mainloop()

本アプリでは、以下のようにセンサーごとにタブを分けて表示し、それぞれのグラフがリアルタイムで更新されます。

  • タブ1:温度センサー
  • タブ2:湿度センサー
  • タブ3:照度センサー

など、用途に応じて自由にカスタムできます。

■ データソース選択

上部の ComboboxRandom / CSV / MQTT を切り替えるだけで、即座にデータソースが変更されます。

MQTT 選択時には、ダッシュボード側がサブスクライブし、外部から送られるセンサーデータのみでグラフが更新されます。

■ アラート判定

閾値を超えた瞬間に “Alert!” と表示されるため、簡易的な監視システムとしても利用可能です。


4. 結論

「IoTDashboardApp」は、Python で IoT ダッシュボードを構築するための “実践的な最良の教材” です。
GUI、グラフ描画、MQTT、非同期更新、データソース切替といった IoT アプリの本質をすべて学べる構成になっています。

また、この手のリアルタイム更新処理は PC の性能も影響します。
特に MQTT や matplotlib の描画は CPU 負荷が高いため、


💡 ワンポイント:開発用PCは「8GB → 16GB RAM」にするだけで快適度が段違い

Python の GUI+グラフ+MQTT の同時処理は意外と負荷が高く、
実装が進むほど「PCの処理性能」が開発効率に直結 します。

「もう少し快適に動いてほしいな…」と感じたら、
PC をグレードアップすると開発が一気にスムーズになります。


記事後半では、IoTDashboardApp の ソースコードを部分引用しながら、Tkinter・matplotlib・MQTT の連携構造を深掘り します。



コード解説パート


1. 結論

IoTDashboardApp のコードは、Tkinter と matplotlib を高度に組み合わせながら、複数データソースを柔軟に処理する構成になっています。

特に「センサーごとのクラス構造」「update_loop」「MQTT コールバック」などが IoT アプリ構築の重要ポイントです。

ここではコードを引用しながら、仕組みをわかりやすく説明します。


2. 理由

IoT ダッシュボードを自作する際に多くの人がつまずくのは、

  • GUI の定期更新とスレッド
  • matplotlib の Tkinter への埋め込み
  • MQTT の非同期受信
  • データソース切替による処理分岐

などの「複数要素が同時に動く場面」です。

今回のコードはそれを 一つのクラス構造に美しく整理 しているため、
IoT アプリ開発のベストプラクティスとして理解しやすい構造になっています。


3. 具体例

ここからはキャンバスにあるコードを段階的に解説します。


■ アプリ全体構造

class IoTDashboardApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("IoT Sensor Dashboard")
        ...

アプリ全体は Tk クラスを継承。
Notebook(タブUI)を生成し、各センサー用フレームを追加しています。


■ センサータブの構造

各タブは SensorTab クラスとして独立しています。

class SensorTab(ttk.Frame):
    def __init__(self, parent, name, mqtt_client):
        super().__init__(parent)

ここでは、

  • グラフエリア
  • データソース選択コンボボックス
  • 閾値入力
  • MQTT購読処理
  • update_loop の開始

などをまとめています。


■ matplotlib × Tkinter の統合

self.fig = Figure(figsize=(5,3))
self.ax = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, master=self)
self.canvas.get_tk_widget().pack()

この 3 行の流れが、Tkinter 内に matplotlib グラフを埋め込む定石です。


■ データソース切替のロジック

src = self.source_var.get()

if src == "Random":
    val = random.uniform(0, 100)
elif src == "CSV":
    val = self.get_csv_value()
elif src == "MQTT":
    val = None

MQTT のときのみ update_loop が値を生成しない →
MQTT 受信イベントでのみ更新されるという仕組み。

この分岐構造が本アプリの「汎用性の源」です。


■ MQTT 受信処理

def on_mqtt_message(self, client, userdata, msg):
    payload = float(msg.payload.decode())
    self.push_value(payload)

受信時に push → GUI 更新は Tkinter の mainloop 上で行うため、
スレッド競合が起きないように after() が使われています。

IoT アプリでは非常に重要なパターンです。


■ 1 秒ごとの自動更新

def update_loop(self):
    ...
    self.after(1000, self.update_loop)

after は Tkinter の “安全なタイマー” であり、
スレッドを増やさず GUI と相性が良い。

初学者が理解しにくい部分ですが、このアプリでは綺麗に実装されています。


■ 閾値アラート処理

if val > th:
    self.alert_label.config(text="Alert!", foreground="red")

閾値超過を即座に UI に反映。
本格的な監視システムの簡易版として使える構造です。


4. まとめ

IoTDashboardApp のコードは、

  • Tkinter(GUI)
  • matplotlib(リアルタイムグラフ)
  • MQTT(非同期通信)
  • データソース切替
  • センサータブのクラス設計

といった IoT 可視化アプリの本質 がすべて盛り込まれています。

Python で IoT ダッシュボードを作りたい人にとって、
まさに “教科書的サンプル” といえるアプリです。


💡 そしてもう一度だけ:開発用PCのスペックは重要です

リアルタイム描画・MQTT通信・複数タブ同時更新は CPU/メモリ負荷が高く、
Python のパフォーマンスは PC 性能にかなり依存します。

開発が快適になると、学習スピードも爆発的に上がります。

「IoTアプリを本格的に作り込むなら、そろそろ買い替え時かも?」
そんなタイミングの参考にしていただければ幸いです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT ME
tyamada
tyamada
普通の会社員(平)
記事URLをコピーしました