下記の記事では、Arduino UNO Qで、Web UI から MCU を操作する例と、expose_api機能について調査を行ったが、本記事では、Web UI とMCUとの間で双方向にやり取りを行う例 を調査したので紹介する。
また、expose_api機能に関するコードを記述する2種類の書式についても調査結果を紹介する。

(注)本記事の内容は正確性を保証するものではない。
(2026年1月8日 追記)YouTube動画を追加しました。 https://youtu.be/tYzCwgNJgNA

Web UI とMCUとの間で双方向にやり取りを行う例
Web UI + センサ値表示 + 制御(双方向)の例を紹介してください
ChatGPT 5.2 Instantの回答
以下は、Arduino UNO Q × Arduino App Lab で
「Web UI にセンサ値を表示しつつ、Web UI から MCU を制御する(双方向)」の 実用最小構成サンプルです。
👉 表示(MCU → Web) と 制御(Web → MCU) を同時に行います。
全体構成(まずここを押さえる)
- ボード:Arduino UNO Q
- 開発環境:Arduino App Lab
[ センサ ]
↓
MCU ── notify ──▶ Python ── send_message ──▶ Web UI(表示)
▲ │
│ └─ expose_api ── Web UI(操作)
└────── call ◀─────────────────────────────
センサで取得した値はMCU(Arduino)に入り、MCUは notify でその値をPython側へ送ります。
Pythonは受け取ったデータを整形し、send_message によってWeb UIへプッシュ配信するため、ブラウザ画面はリアルタイムに更新されます(温度表示など)。
一方、操作は逆方向です。Web UI側のボタンやスライダー操作は、Pythonで公開している expose_api(例:POST /led、POST /threshold)にHTTPリクエストとして届きます。
Pythonはその内容に応じて call を実行し、MCU側で provide された関数(例:set_led、set_threshold)を呼び出します。
これにより、Web UIからの操作がMCUの動作(LED制御やしきい値変更)に反映されます。
まとめると、センサ値は「MCU → Python → Web UI」へ通知・表示される流れ(notify / send_message)、
操作は「Web UI → Python → MCU」へ伝わる流れ(expose_api / call)になっており、
Pythonが両者をつなぐハブとして機能する構成です。
デモの内容(完成形)
- センサ値(例:温度)を リアルタイム表示
- Web UI の
- ON / OFF ボタン → LED制御
- スライダー → しきい値変更
- MCU はしきい値を使ってローカル制御も可能
1️⃣ app.yaml(Web UI Brick)
App Lab の GUI で
WebUI – HTML Brick を追加してください。
bricks:
- arduino:web_ui
2️⃣ MCU 側(Arduinoスケッチ)
役割
- センサ値取得(ここではダミー)
- Python からの制御命令を受信
- センサ値を Python に送信
動作フロー(ざっくり)
setup()- LEDピンを出力に設定
- Bridgeを初期化
- 外部から呼べる関数として
set_ledとset_thresholdを公開
loop()(1秒ごと)- ダミー温度
tempを生成 on_tempイベントで外部へ温度を通知- しきい値でLEDを自動制御
- ダミー温度
#include <Arduino_RouterBridge.h>
// Arduino_RouterBridge: MCUと外部(例: App Lab)の間で
// 関数呼び出し(provide)やイベント通知(notify)を行うためのブリッジ
const int LED_PIN = 13;
// LEDを接続するピン番号。多くのArduinoでは13番ピンが基板上LEDに接続
float threshold = 25.0;
// 温度のしきい値。temp > threshold ならLEDを点灯する想定
void set_led(bool on) {
// 外部から「LEDをON/OFFしたい」と呼ばれる想定の関数
// on が true なら HIGH、false なら LOW
digitalWrite(LED_PIN, on ? HIGH : LOW);
}
void set_threshold(float v) {
// 外部から「しきい値を変更したい」と呼ばれる想定の関数
threshold = v;
}
void setup() {
// 起動時に1回だけ呼ばれる初期化
pinMode(LED_PIN, OUTPUT);
// LEDピンを出力モードに
Bridge.begin();
// Bridge機能の初期化(通信の準備)
Bridge.provide("set_led", set_led);
// 外部から "set_led" という名前で set_led(bool) を呼べるように公開
Bridge.provide("set_threshold", set_threshold);
// 外部から "set_threshold" という名前で set_threshold(float) を呼べるように公開
}
void loop() {
// setup()後、繰り返し呼ばれるメインループ
float temp = 20.0 + random(-50, 50) / 10.0; // ダミー温度
// 20.0 を中心に、-5.0〜+4.9 の揺らぎを足した擬似温度
// random(-50, 50) は -50〜49 の整数 → /10.0 で -5.0〜4.9
Bridge.notify("on_temp", temp);
// MCU側から外部へ、"on_temp" というイベント名で温度を通知
// 外部側では "on_temp" を購読して受け取る想定
// MCU側ローカル制御(例)
digitalWrite(LED_PIN, temp > threshold ? HIGH : LOW);
// temp が threshold を超えたらLED点灯、超えなければ消灯
// ※ 外部から set_led() を呼んでも、ここで上書きされる点に注意
delay(1000);
// 1秒待ってから次ループへ
}
3️⃣ Python 側(双方向ハブ)
役割
- MCU → Web UI:センサ値を push
- Web UI → MCU:操作を中継
ざっくり動作フロー
- MCU → App(Python): MCU側が
Bridge.notify("on_temp", temp)を送る
→ Python側でBridge.provide("on_temp", on_temp)によりon_temp()が呼ばれる
→ui.send_message("temperature", {...})で Web UI に温度を送る - Web UI → App(Python) → MCU: ブラウザが
POST /ledまたはPOST /thresholdを送る
→@ui.expose_api(...)の関数が呼ばれる
→Bridge.call("set_led", ...)/Bridge.call("set_threshold", ...)で MCU 側関数を実行
from arduino.app_utils import App, Bridge
# App: アプリの起動・イベントループを管理する枠組み
# Bridge: MCU(Arduino側スケッチ)とメッセージ/関数呼び出しでやり取りする窓口
from arduino.app_bricks.web_ui import WebUI
# WebUI: ブラウザで表示できるUIを提供し、API(expose_api)やメッセージ送信(send_message)ができる
import time
ui = WebUI()
# Web UI のインスタンスを作成(ブラウザ側との通信・API公開に使う)
# ----------------------------
# MCU → Web UI(温度の受信→画面へ反映)
# ----------------------------
def on_temp(value: float):
"""
MCU側から通知された温度値 value を受け取り、
Web UI 側へ 'temperature' というチャンネル名で送る。
ts はミリ秒のタイムスタンプ(表示更新やログ用)。
"""
ui.send_message("temperature", {
"value": value, # 温度値
"ts": int(time.time() * 1000) # 現在時刻(ミリ秒)
})
Bridge.provide("on_temp", on_temp)
# MCU側が Bridge.notify("on_temp", temp) を送ってきたときに、
# この on_temp() を呼び出して受け取れるように登録(イベントハンドラの紐付け)
# ----------------------------
# Web UI → MCU(ブラウザからの操作→MCU関数を呼ぶ)
# ----------------------------
@ui.expose_api("POST", "/led")
def api_led(data):
"""
ブラウザ(HTTP)から POST /led が来たときに呼ばれるAPI。
data にはリクエストボディ(JSON等)が入る想定。
例: {"on": true}
"""
Bridge.call("set_led", bool(data["on"]))
# MCU側で Bridge.provide("set_led", set_led) されている関数を呼ぶ
# 引数は bool に変換して渡す
return {"ok": True}
# ブラウザへ返すレスポンス(成功したことを返す)
@ui.expose_api("POST", "/threshold")
def api_threshold(data):
"""
ブラウザから POST /threshold が来たときに呼ばれるAPI。
例: {"value": 27.5}
"""
Bridge.call("set_threshold", float(data["value"]))
# MCU側で公開されている set_threshold(float) を呼ぶ
# 引数は float に変換して渡す
return {"ok": True}
App.run()
# アプリを起動してイベントループを回す。
# これにより
# - MCUからの notify を受けて on_temp() が動く
# - Web UI のAPI(/led, /threshold)が待ち受ける
# などが継続的に処理される
4️⃣ Web UI(assets/index.html)
このHTMLがやっていること(要点)
- 表示:
#tempに温度、#tsに更新時刻、#thにしきい値を表示する“枠”を用意 - 操作:LED ON/OFF ボタンと、しきい値スライダーを用意(クリック/操作で
app.jsの関数を呼ぶ) - 通信ライブラリ読み込み:
socket.ioとapp.jsを読み込む(実際の通信処理はapp.js側)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<!-- 文字コードをUTF-8に。日本語表示などで必須 -->
<title>UNO Q Sensor & Control</title>
<!-- ブラウザのタブに出るタイトル -->
</head>
<body>
<h1>Sensor Monitor</h1>
<!-- 画面の見出し -->
<p>
Temperature:
<strong><span id="temp">--</span> °C</strong>
<!-- 温度表示エリア。
span#temp の中身を JavaScript(app.js) が更新する想定。
初期表示は "--" -->
</p>
<p><small id="ts">--</small></p>
<!-- タイムスタンプ表示エリア。
small#ts の中身を JavaScript(app.js) が更新する想定。
例: 受信時刻や「最新更新」など -->
<h2>Control</h2>
<!-- 操作UIの見出し -->
<button onclick="led(true)">LED ON</button>
<button onclick="led(false)">LED OFF</button>
<!-- クリックすると led(true/false) を呼ぶ。
led() 関数は app.js 側で定義されていて、
POST /led を投げる等の処理をする想定。 -->
<p>
Threshold:
<input type="range" min="0" max="50" step="0.5"
value="25" oninput="setThreshold(this.value)">
<!-- しきい値を調整するスライダー(range)。
min/max/step で 0〜50 を 0.5刻み。
value="25" は初期値。
つまみを動かすたび oninput で setThreshold(値) を呼ぶ。
setThreshold() は app.js 側で定義され、POST /threshold を送る想定。 -->
<span id="th">25</span> °C
<!-- 現在のしきい値表示。初期表示25。
app.js が slider の値に合わせて span#th を更新する想定。 -->
</p>
<script src="libs/socket.io.min.js"></script>
<!-- socket.io クライアントライブラリ。
サーバ(App側)とWeb UIがリアルタイム通信するために使うことが多い。
例: 温度の push 更新を受け取る -->
<script src="app.js"></script>
<!-- このページの本体ロジック。
- socket.io で 'temperature' メッセージを受け取り #temp/#ts を更新
- led(on) や setThreshold(value) を定義し、APIにPOSTする
などを担当 -->
</body>
</html>
5️⃣ Web UI(assets/app.js)
役割
socket.io で温度のプッシュ更新を受け取り表示し、ボタン/スライダー操作で HTTP POST を送って MCU 側の関数呼び出しをトリガーします。
// socket.io の接続を作る。
// `window.location.host` は「今表示しているページのホスト名:ポート」。
// 例: http://192.168.0.10:3000 のような同一ホストに接続する想定。
const socket = io(`http://${window.location.host}`);
// HTML側で用意した表示要素を取得
// <span id="temp">, <small id="ts">, <span id="th"> を参照して更新する
const tempEl = document.getElementById("temp");
const tsEl = document.getElementById("ts");
const thEl = document.getElementById("th");
// ----------------------------
// MCU → Web UI(温度の受信→画面更新)
// ----------------------------
// サーバ(Python側WebUI)が send_message("temperature", {...}) で送ってくる
// "temperature" イベントを受け取る。
// data の想定: { value: number, ts: ミリ秒UNIX時刻 }
socket.on("temperature", (data) => {
// 温度を小数1桁に整形して表示
tempEl.textContent = data.value.toFixed(1);
// ミリ秒タイムスタンプを Date にしてローカル日時文字列に変換して表示
tsEl.textContent = new Date(data.ts).toLocaleString();
});
// ----------------------------
// Web UI → MCU(操作→APIへPOST)
// ----------------------------
// HTMLのボタン onclick="led(true/false)" から呼ばれる関数
function led(on) {
// /led に JSON を POST する
// Python側: @ui.expose_api("POST", "/led") が受け取り、
// Bridge.call("set_led", ...) でMCUの set_led を呼ぶ想定
fetch("/led", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ on }) // 例: { "on": true }
});
// ※このコードはレスポンス待ちやエラー処理をしていない(最小例)
}
// HTMLのスライダー oninput="setThreshold(this.value)" から呼ばれる関数
function setThreshold(v) {
// 画面上の「現在のしきい値表示」をまず更新
thEl.textContent = v;
// /threshold に JSON を POST する
// Python側: @ui.expose_api("POST", "/threshold") が受け取り、
// Bridge.call("set_threshold", ...) でMCUの threshold を更新する想定
fetch("/threshold", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ value: v }) // 例: { "value": "27.5" }(this.value は文字列になりがち)
});
// ※こちらもレスポンス待ち・エラー処理なし(最小例)
}
6️⃣ 実行方法
- App Lab でプロジェクト作成
- WebUI Brick を追加
- sketch / python / assets を配置
- Deploy
- ブラウザで開く
http://<UNO Q の IP アドレス>:7000/
✔ 温度が自動更新
✔ スライダー変更 → LED 挙動が変わる
→ 双方向通信成功 🎉
設計ポイント(実務的に重要)
| 項目 | ベストプラクティス |
|---|---|
| 表示 | WebSocket(send_message) |
| 操作 | REST API(expose_api) |
| MCU | センサ・GPIO・安全制御 |
| Python | UI連携・検証・ロジック |
まとめ
- 表示:MCU → Python → Web UI
- 制御:Web UI → Python → MCU
- UNO Q では
Webダッシュボード+制御盤+AI前処理を1台で実装可能
expose_api機能に関するコードを記述する2種類の書式

上記の記事では、expose_api機能は、「ui.expose_api(“GET”, “/history/{sensor_type}/{duration}”, get_sensor_history)」という書式で紹介されたが、本記事の例では、下記のように定義部分が続く書式で紹介された。2つの書式について調査した。
@ui.expose_api("POST", "/led")
def api_led(data):
"""
ブラウザ(HTTP)から POST /led が来たときに呼ばれるAPI。
data にはリクエストボディ(JSON等)が入る想定。
例: {"on": true}
"""
Bridge.call("set_led", bool(data["on"]))
# MCU側で Bridge.provide("set_led", set_led) されている関数を呼ぶ
# 引数は bool に変換して渡す
return {"ok": True}
# ブラウザへ返すレスポンス(成功したことを返す)
ARDUINO UNO QのApp Labのexpose_api機能の書式には下記の2種類があるようです。下記の2種類の書式について説明してください。
ui.expose_api(“GET”, “/history/{sensor_type}/{duration}”, get_sensor_history)
@ui.expose_api(“POST”, “/infer”)
def api_infer():
Gemini 3 Proの回答
Arduino UNO QのApp Lab(Pythonベースの開発環境)における expose_api の2つの記述方法は、Pythonの言語仕様に基づいた「デコレータ形式」と「関数引数形式」の違いです。
機能として「Web APIのエンドポイントを作成する」という点は同じですが、コードの書き方や利用シーンが異なります。
それぞれ詳しく解説します。
1. デコレータ形式(@ を使う方法)
この書き方は、関数の定義と同時にAPIとしての登録を行う、現代的なPythonフレームワーク(FlaskやFastAPIなど)で最も一般的なスタイルです。
Python
@ui.expose_api("POST", "/infer")
def api_infer():
# 処理内容
pass
- 特徴: 関数定義のすぐ上に
@ui.expose_api(...)を記述します。 - メリット: 「この関数がどのURL(パス)に対応しているか」が一目でわかるため、可読性が高く、コードがすっきりします。
- 動作:
api_inferという関数が定義されると同時に、自動的に/inferというAPIとして登録されます。 - 推奨シーン: 基本的に、新しくAPI用の関数を書く場合はこちらが推奨されます。
2. 関数引数形式(関数オブジェクトを渡す方法)
この書き方は、すでにある関数を後からAPIとして紐付ける、あるいはコールバックとして登録する伝統的なスタイルです。
Python
# 先に関数を定義する
def get_sensor_history(sensor_type, duration):
# 処理内容
pass
# 後でAPIとして登録する(関数名 get_sensor_history を引数として渡す)
ui.expose_api("GET", "/history/{sensor_type}/{duration}", get_sensor_history)
- 特徴: expose_api の第3引数に、実行させたい関数の名前(関数オブジェクト)を渡します。
- メリット:
- すでに別の場所で定義されている関数や、ライブラリ内の関数をAPI化したい場合に便利です。
- APIの登録処理をコードの末尾などにまとめて書きたい場合に有効です。
- 動作: 指定したURL(例:
/history/...)にアクセスがあった際、第3引数で渡したget_sensor_history関数を呼び出すように設定します。
具体的なコードの機能解説
質問にある2つのコードは、書き方の違いだけでなく、機能(HTTPメソッドやパラメータ)も異なります。
A. パスパラメータ付きGETリクエスト
Python
ui.expose_api("GET", "/history/{sensor_type}/{duration}", get_sensor_history)
- HTTPメソッド:
GET(主にデータを取得するために使います) - パスパラメータ:
{sensor_type}や{duration}のように波括弧 {} で囲まれた部分は可変パラメータです。- 例:
/history/temp/10mにアクセスすると、get_sensor_history関数の引数にtempと10mが渡されて実行されます。
- 例:
- 用途: センサーの履歴データなどを条件指定して取得する場合に使われます。
B. シンプルなPOSTリクエスト
Python
@ui.expose_api("POST", "/infer")
def api_infer():
- HTTPメソッド:
POST(主にデータを送信したり、処理を実行させたりするために使います) - パス:
/infer(固定のパスです) - 用途: 推論(Inference)を実行させる指示を送る場合などに使われます。通常、POSTリクエストはURLではなく「リクエストボディ(Body)」にデータ(画像データやJSONなど)を含めて送信します。
まとめ:使い分けの指針
| 特徴 | デコレータ形式 (@) | 関数引数形式 (func渡し) |
| 見た目 | 関数とURLがセットで直感的 | 登録処理と関数定義が分離 |
| 主な用途 | 新規にAPI用の処理を書く場合 | 既存の関数をAPI化する場合 |
| 可読性 | 高い(推奨) | 定義場所が離れると少し読みづらい |
どちらを使っても動きますが、自分で処理を書く場合はデコレータ形式(@)を使うのが一般的です。
それぞれのAPIの中で、具体的にどのようにデータを受け取るか(JSONデータのパース方法など)についても解説が必要でしょうか?
それぞれのAPIの中で、具体的にどのようにデータを受け取るか(JSONデータのパース方法など)について説明してください
Gemini 3 Proの回答
Arduino UNO QのApp Lab(Python環境)において、Webブラウザや外部アプリから送られてきたデータを受け取る方法は、GET(URLの一部として受け取る)とPOST(JSONデータとして受け取る)でアプローチが異なります。
それぞれ具体的なコード例と共に解説します。
1. GETリクエストの場合:URLの「パス」からデータを受け取る
expose_api のURL指定に含まれる {}(波括弧)の部分は、自動的にPython関数の引数にマッピング(割り当て)されます。これを「パスパラメータ」と呼びます。
書式例
Python
# URL定義: /history/{sensor_type}/{duration}
ui.expose_api("GET", "/history/{sensor_type}/{duration}", get_sensor_history)
データの受け取り方(Python側)
定義する関数の引数名を、URLの {} の中身と同じ名前にします。
Python
def get_sensor_history(sensor_type, duration):
# ここに来るデータは基本的に「文字列 (str)」です
print(f"センサーの種類: {sensor_type}")
print(f"期間: {duration}")
# 数値として扱いたい場合は変換が必要です
time_val = int(duration)
# 処理結果を辞書(dict)で返すと、自動的にJSONとしてクライアントに返されます
return {"status": "ok", "data": [10, 20, 30]}
- クライアントが /history/temp/60 にアクセスした場合:
- sensor_type には “temp” が入ります。
- duration には “60” が入ります。
2. POSTリクエストの場合:JSONデータを受け取る
POSTリクエストは、URLではなく「リクエストボディ(Body)」の中にJSONデータを入れて送られてきます。
App LabのようなPythonフレームワークでは、このJSONデータを受け取るために、関数の引数を用意するか、リクエストオブジェクトを使用します。
パターンA:引数で受け取る(最も一般的)
関数の引数に data や body などの変数を定義しておくと、フレームワークが自動的にJSONを辞書(dictionary)に変換して渡してくれるケースが多いです。
Python
@ui.expose_api("POST", "/infer")
def api_infer(data): # 引数を1つ用意する
# 送られてきたJSON: {"image": "base64...", "mode": "fast"} の場合
# dataは既に辞書型(dict)になっています(パース不要)
mode = data.get("mode") # "fast" を取得
image_data = data.get("image")
print(f"推論モード: {mode}")
# 処理...
result = "cat"
return {"result": result, "confidence": 0.98}
パターンB:Requestオブジェクトを使う(フレームワークによる)
もし引数で受け取れない仕様の場合(あるいはより詳細なヘッダー情報などが必要な場合)、request オブジェクトを通じて生のデータにアクセスします。
Python
# ※App Labの仕様によりますが、Flask等の標準的な書き方は以下の通りです
from flask import request # (※ライブラリ構成による)
@ui.expose_api("POST", "/infer")
def api_infer():
# requestオブジェクトからJSONを取得
json_data = request.json # または request.get_json()
mode = json_data["mode"]
# ...
ポイント: Arduino UNO QのApp Lab環境がFastAPIベースかFlaskベースかによりますが、現代的なAPIの書き方としては「パターンA(引数に定義する)」をまず試すのが正解です。送信されたJSONが自動的にPythonの辞書型に変換されて
dataに入ります。
まとめ
| 通信方式 | データの場所 | 受け取り方 | 特徴 |
| GET | URLの中/history/temp/10 | 関数の引数def func(sensor_type, duration): | 引数名とURL内の {名前} を一致させるだけでOK。データは文字列。 |
| POST | Bodyの中{"mode": "fast"} | 関数の引数def func(data): | JSONが自動的にパースされ、Pythonの辞書(dict)として引数に入ってくるのが一般的。 |
クライアント(送信側)からデータを送る際は、POSTの場合は必ずヘッダーに Content-Type: application/json を付与してJSON形式で送る必要があります。
