Progress Log: APIエンドポイントの動作メカニズム¶
Task Description¶
WiFi対応実装(パターンC)で提供するHTTPサーバーAPIエンドポイントの仕組みを詳細に解説。以下の3つのエンドポイントについて、リクエスト処理からレスポンス返却までの内部動作、メモリ状態の変化、ハードウェアとの連携を整理。
GET /api/status- ステータス情報取得POST /api/threshold- スレッショルド設定変更GET /api/sensor-data- リアルタイムセンサーデータ取得
Outcome¶
既存runtime_configメソッドの再利用¶
WiFi API実装では、既存の runtime_config.cpp に実装済みのメソッドを直接再利用 します。設定管理ロジックの二重実装を避け、Serial通信とWiFi通信が同一の設定機構を共有します。
再利用するメソッド一覧¶
config_get_poll_count()- GET /api/statusで使用config_get_threshold(ch)- GET /api/statusで使用config_set_threshold(ch, val)- POST /api/thresholdで使用(SPI DAC制御も含む)config_get_version()- GET /api/statusで使用config_get_debug_mode()- GET /api/statusで使用config_set_interval()/config_get_interval()- 検出間隔制御用
アーキテクチャの分離¶
- serial_protocol.cpp - 通信層(UART I/O)
- runtime_config.cpp - 設定層(状態管理)← 共有
- wifi_server.cpp - 通信層(HTTP I/O)
両通信層が runtime_config の同一メソッドセットを呼び出すため、設定値の一貫性が保証されます。
重要な特徴¶
- protocol-agnostic: Serial/WiFiいずれの通信手段でも同じ設定APIを使用
- SPI DAC統合:
config_set_threshold()内で自動的にDACへの値送信が行われる - メモリ効率:
static RuntimeConfig g_configはグローバル変数として一度だけ定義 - 検証済み: 既存のSerial実装で動作確認済みのコード
1. GET /api/status の仕組み¶
リクエスト形式¶
GET http://192.168.4.1/api/status HTTP/1.1
レスポンス処理フロー¶
Step 1: HTTPサーバーがリクエストを受け取る
// wifi_server.cpp に実装(snake_case 統一)
void wifi_handle_status() {
// ランタイム設定を取得
// ↓ 内部メモリから現在の値を読み込む
}
Step 2: 内部データをJSONに変換
// ランタイム設定から情報を取得:
uint16_t poll_count = config_get_poll_count(); // 例: 100
uint16_t threshold1 = config_get_threshold(1); // 例: 2048
uint16_t threshold2 = config_get_threshold(2); // 例: 2048
uint16_t threshold3 = config_get_threshold(3); // 例: 2048
const char* version = config_get_version(); // "1.8.0"
uint8_t debug_mode = config_get_debug_mode(); // 0 or 1
Step 3: JSON文字列を組み立て
// ArduinoJsonライブラリを使用
StaticJsonDocument<512> doc;
doc["version"] = config_get_version();
doc["poll_count"] = config_get_poll_count();
doc["threshold1"] = config_get_threshold(1);
doc["threshold2"] = config_get_threshold(2);
doc["threshold3"] = config_get_threshold(3);
doc["debug_mode"] = config_get_debug_mode();
doc["wifi_enabled"] = true;
// JSONを文字列に変換
String jsonString;
serializeJson(doc, jsonString);
// HTTPレスポンス送信
server.send(200, "application/json", jsonString);
Step 4: ブラウザーが受け取る
{
"version": "1.8.0",
"poll_count": 100,
"threshold1": 2048,
"threshold2": 2048,
"threshold3": 2048,
"debug_mode": 0,
"wifi_enabled": true
}
メモリの内部状態¶
// include/runtime_config.h で定義
typedef struct {
uint16_t poll_count; // ← GET /api/status でここを読み込む
uint16_t threshold1; // ← GET /api/status でここを読み込む
uint16_t threshold2; // ← GET /api/status でここを読み込む
uint16_t threshold3; // ← GET /api/status でここを読み込む
const char* firmware_version;
uint8_t debug_mode;
uint16_t interval_ms;
} RuntimeConfig;
// グローバル変数に保存
static RuntimeConfig g_config = {
.poll_count = 100,
.threshold1 = 0,
.threshold2 = 0,
.threshold3 = 0,
...
};
重要な特性¶
- 読み込みのみ: 内部メモリを読むだけで、値は変わらない
- 構成値の代表: firmware_version、debug_modeはコンパイル時固定
- リアルタイム: 常に現在のメモリ状態を返す(キャッシュなし)
2. POST /api/threshold の仕組み¶
リクエスト形式¶
POST http://192.168.4.1/api/threshold HTTP/1.1
Content-Type: application/json
Content-Length: 23
{
"ch": 1,
"value": 2500
}
レスポンス処理フロー¶
Step 1: POSTリクエストボディを受け取る
// wifi_server.cpp に実装
void handleThreshold() {
// ブラウザーから送信されたJSON文字列を取得
String body = server.arg("plain"); // {"ch": 1, "value": 2500}
// JSONをパース
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, body);
if (error) {
server.send(400, "application/json", "{\"error\":\"invalid_json\"}");
return;
}
}
Step 2: JSONから値を抽出
// パースされたJSONから値を取得
uint8_t channel = doc["ch"].as<uint8_t>(); // 1
uint16_t value = doc["value"].as<uint16_t>(); // 2500
// バリデーション
if (channel < 1 || channel > 3) {
server.send(400, "application/json", "{\"error\":\"invalid_channel\"}");
return;
}
if (value < 0 || value > 4095) {
server.send(400, "application/json", "{\"error\":\"invalid_value\"}");
return;
}
Step 3: ランタイム設定を更新
// runtime_config.cpp の関数を呼び出し
bool success = config_set_threshold(channel, value);
// ↓
// 内部: RuntimeConfig g_config に値を保存
// g_config.threshold1 = 2500 (channel=1の場合)
if (!success) {
server.send(500, "application/json", "{\"error\":\"set_failed\"}");
return;
}
Step 4: SPIコマンドをDACに送信
// runtime_config.cpp 内で自動実行
// config_set_threshold() 内部:
void config_set_threshold(uint8_t ch, uint16_t val) {
// メモリに保存
if (ch == 1) g_config.threshold_ch1 = val;
else if (ch == 2) g_config.threshold_ch2 = val;
else if (ch == 3) g_config.threshold_ch3 = val;
// SPI DACに即座に送信
spi_set_threshold(ch, val); // ← DACハードウェアを制御
// ↓
// SPIで2バイト送信: [0x23, 0x4A] のようなバイト列
// DACハードウェアがこれを受け取り、スレッショルド値を変更
}
Step 5: ブラウザーにレスポンス
// 成功時
StaticJsonDocument<256> response;
response["status"] = "ok";
response["channel"] = channel;
response["value"] = value;
String jsonString;
serializeJson(response, jsonString);
server.send(200, "application/json", jsonString);
Step 6: ブラウザーが受け取る
{
"status": "ok",
"channel": 1,
"value": 2500
}
メモリ状態の変化¶
POST前:
RuntimeConfig g_config {
threshold1: 0 ← SPI DACも0
}
POST実行後:
RuntimeConfig g_config {
threshold1: 2500 ← 即座に更新
}
SPI DAC チップ: threshold=2500 ← SPIで即座に反映
重要な特性¶
- 読み込み + 書き込み: JSONから値を抽出してメモリに保存
- HW連携: SPIでDACに即座に反映(ハードウェア制御)
- バリデーション: channel (1-3), value (0-4095) の範囲チェック
- 即座反映: POSTレスポンス返却時にはスレッショルドが変わっている
3. GET /api/sensor-data の仕組み¶
リクエスト形式¶
GET http://192.168.4.1/api/sensor-data HTTP/1.1
レスポンス処理フロー¶
Step 1: 最新センサーデータを取得
// wifi_server.cpp に実装
void handleSensorData() {
// グローバル変数から最新データを取得
// (detection イベント時に更新)
}
Step 2: sensor_data_t 構造体をJSONに変換
// sensor_data.h で定義
typedef struct {
uint16_t signal1; // チャネル1検出カウント (0-100)
uint16_t signal2; // チャネル2検出カウント (0-100)
uint16_t signal3; // チャネル3検出カウント (0-100)
int sensorValue; // ADC値 (0-4095)
#if ENABLE_BME280
float tmp_c; // 温度
float atm_pa; // 気圧
float hmd_pct; // 湿度
#endif
#if ENABLE_TIMESTAMP
uint32_t uptime_ms; // ブート以降の経過時間(ms)
uint64_t duration_us; // 前回検出からの経過時間(μs)
#endif
} sensor_data_t;
// JSONに変換
StaticJsonDocument<512> doc;
doc["signal1"] = latest_sensor_data.signal1;
doc["signal2"] = latest_sensor_data.signal2;
doc["signal3"] = latest_sensor_data.signal3;
doc["adc"] = latest_sensor_data.sensorValue;
#if ENABLE_BME280
doc["temp"] = latest_sensor_data.tmp_c;
doc["pressure"] = latest_sensor_data.atm_pa;
doc["humidity"] = latest_sensor_data.hmd_pct;
#endif
String jsonString;
serializeJson(doc, jsonString);
server.send(200, "application/json", jsonString);
Step 3: ブラウザーが受け取る
{
"signal1": 45,
"signal2": 38,
"signal3": 42,
"adc": 2048,
"temp": 25.3,
"pressure": 1013.25,
"humidity": 55.2,
"uptime_ms": 3600000
}
重要な特性¶
- 読み込みのみ: 内部メモリを読むだけで値は変わらない
- 動的フィールド: ENABLE_BME280、ENABLE_TIMESTAMPフラグで出力フィールドが変わる
- イベント駆動: 最新データは検出イベント時に更新される
- リアルタイム: 常に最新の読み込みデータを返す
4. データフロー図¶
GET /api/status¶
ブラウザーからのリクエスト
↓
HTTPサーバー (wifi_server.cpp)
↓
ランタイム設定読み込み (runtime_config.cpp)
↓
メモリ g_config から値を取得
- poll_count (現在: 100)
- threshold1 (現在: 2048)
- threshold2 (現在: 2048)
- threshold3 (現在: 2048)
- firmware_version (コンパイル時固定: "1.8.0")
- debug_mode (コンパイル時固定: 0)
↓
JSONに変換 (ArduinoJson)
↓
HTTPレスポンスとしてブラウザーに送信
↓
ブラウザーのJavaScriptがJSONをパース
↓
UIに表示(バージョン、ポーリング数、スレッショルド値)
POST /api/threshold¶
ブラウザーからのリクエスト
{"ch": 1, "value": 2500}
↓
HTTPサーバー (wifi_server.cpp)
↓
JSONをパース
channel = 1
value = 2500
↓
バリデーション (1-3, 0-4095)
↓
ランタイム設定に保存 (runtime_config.cpp)
g_config.threshold_ch1 = 2500
↓
SPIコマンド生成 (spi_control.cpp)
threshold 2500 → bytes [0x23, 0x4A]
↓
SPI通信でDACハードウェアに送信
↓
DACチップが受け取り、検出スレッショルドを変更
↓
HTTPレスポンスとしてブラウザーに確認
{"status": "ok", "channel": 1, "value": 2500}
↓
ブラウザーのUIが更新(スライダー値反映)
GET /api/sensor-data¶
ブラウザーからのリクエスト
↓
HTTPサーバー (wifi_server.cpp)
↓
最新センサーデータ読み込み
(検出イベント時に更新されたsensor_data_t)
↓
JSONに変換 (sensor_formatter.cpp)
↓
HTTPレスポンスとしてブラウザーに送信
↓
ブラウザーのJavaScriptがJSONをパース
↓
UIにリアルタイム表示(値の自動更新)
5. 実装コード例(wifi_server.cpp)¶
#include <WebServer.h>
#include <ArduinoJson.h>
#include "runtime_config.h"
#include "spi_control.h"
#include "sensor_formatter.h"
WebServer server(80);
// ============================================================================
// GET /api/status - ステータス取得
// ============================================================================
void handleStatus() {
StaticJsonDocument<512> doc;
// ランタイム設定をJSONに変換
doc["version"] = config_get_version();
doc["poll_count"] = config_get_poll_count();
doc["threshold_ch1"] = config_get_threshold(1);
doc["threshold_ch2"] = config_get_threshold(2);
doc["threshold_ch3"] = config_get_threshold(3);
doc["debug_mode"] = config_get_debug_mode();
doc["wifi_enabled"] = true;
// JSONをシリアライズ
String jsonString;
serializeJson(doc, jsonString);
// HTTPレスポンス送信
server.send(200, "application/json", jsonString);
}
// ============================================================================
// POST /api/threshold - スレッショルド設定
// ============================================================================
void handleThreshold() {
// POSTボディを取得
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"error\":\"no_body\"}");
return;
}
String body = server.arg("plain");
// JSONをパース
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, body);
if (error) {
server.send(400, "application/json", "{\"error\":\"invalid_json\"}");
return;
}
// 必須フィールドを抽出
if (!doc.containsKey("ch") || !doc.containsKey("value")) {
server.send(400, "application/json", "{\"error\":\"missing_fields\"}");
return;
}
uint8_t channel = doc["ch"].as<uint8_t>();
uint16_t value = doc["value"].as<uint16_t>();
// バリデーション
if (channel < 1 || channel > 3) {
server.send(400, "application/json", "{\"error\":\"invalid_channel\"}");
return;
}
if (value < 0 || value > 4095) {
server.send(400, "application/json", "{\"error\":\"invalid_value\"}");
return;
}
// スレッショルド値を設定(メモリ+SPI DAC)
if (config_set_threshold(channel, value)) {
StaticJsonDocument<256> response;
response["status"] = "ok";
response["channel"] = channel;
response["value"] = value;
String jsonString;
serializeJson(response, jsonString);
server.send(200, "application/json", jsonString);
} else {
server.send(500, "application/json", "{\"error\":\"set_failed\"}");
}
}
// ============================================================================
// GET /api/sensor-data - センサーデータ取得
// ============================================================================
void handleSensorData() {
// グローバル変数から最新データを取得
extern sensor_data_t g_latest_sensor_data;
StaticJsonDocument<512> doc;
// コアフィールド
doc["signal1"] = g_latest_sensor_data.signal1;
doc["signal2"] = g_latest_sensor_data.signal2;
doc["signal3"] = g_latest_sensor_data.signal3;
doc["adc"] = g_latest_sensor_data.sensorValue;
#if ENABLE_BME280
doc["temp"] = g_latest_sensor_data.tmp_c;
doc["pressure"] = g_latest_sensor_data.atm_pa;
doc["humidity"] = g_latest_sensor_data.hmd_pct;
#endif
#if ENABLE_TIMESTAMP
doc["uptime_ms"] = g_latest_sensor_data.uptime_ms;
doc["duration_us"] = g_latest_sensor_data.duration_us;
#endif
String jsonString;
serializeJson(doc, jsonString);
server.send(200, "application/json", jsonString);
}
void wifi_server_init() {
// APIエンドポイント登録
server.on("/api/status", HTTP_GET, handleStatus);
server.on("/api/threshold", HTTP_POST, handleThreshold);
server.on("/api/sensor-data", HTTP_GET, handleSensorData);
server.begin();
}
6. ブラウザー側のJavaScript例¶
// GET /api/status でステータス取得
async function updateStatus() {
const response = await fetch('/api/status');
const data = await response.json();
console.log('Status:', data);
// UIを更新
document.getElementById('version').textContent = data.version;
document.getElementById('poll').textContent = data.poll_count;
document.getElementById('ch1').value = data.threshold_ch1;
document.getElementById('ch2').value = data.threshold_ch2;
document.getElementById('ch3').value = data.threshold_ch3;
}
// POST /api/threshold でスレッショルド変更
async function setThreshold(channel, value) {
const response = await fetch('/api/threshold', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ch: channel,
value: parseInt(value)
})
});
const data = await response.json();
if (data.status === 'ok') {
console.log(`Channel ${channel} threshold set to ${value}`);
} else {
console.error('Error:', data.error);
}
}
// GET /api/sensor-data でセンサーデータ取得
async function updateSensorData() {
const response = await fetch('/api/sensor-data');
const data = await response.json();
console.log('Sensor Data:', data);
// UIを更新
document.getElementById('signal1').textContent = data.signal1;
document.getElementById('signal2').textContent = data.signal2;
document.getElementById('signal3').textContent = data.signal3;
document.getElementById('adc').textContent = data.adc;
if (data.temp !== undefined) {
document.getElementById('temp').textContent = data.temp.toFixed(1);
document.getElementById('pressure').textContent = data.pressure.toFixed(0);
document.getElementById('humidity').textContent = data.humidity.toFixed(1);
}
}
// スライダーイベント
document.getElementById('ch1').addEventListener('input', (e) => {
setThreshold(1, e.target.value);
});
// 1秒ごとにステータスを更新
setInterval(updateStatus, 1000);
setInterval(updateSensorData, 1000);
Learnings¶
アーキテクチャの理解¶
- メモリが単一ソース: RuntimeConfig g_config がすべての設定値の唯一のソース
- GET: メモリから読み込み
-
POST: メモリに書き込み + HW反映
-
GETとPOSTの違い:
- GET /api/status: 読み込みのみ(内部状態変化なし)
- POST /api/threshold: 読み込み+書き込み+HW反映(即座に反映)
-
GET /api/sensor-data: 読み込みのみ(最新イベントデータ返却)
-
バリデーション:
- チャネル: 1-3 (3チャネル対応)
- スレッショルド値: 0-4095 (12ビットADC対応)
-
JSON形式チェック: 必須フィールド確認
-
リアルタイム性:
- POST実行直後、DACハードウェアがスレッショルド値を変更
- GET実行時には常に現在のメモリ状態を返す
-
センサーデータは検出イベント時に自動更新
-
ArduinoJsonライブラリの活用:
- StaticJsonDocument: スタックメモリで固定サイズ確保
- deserializeJson: HTTP POSTボディから構造化データ抽出
- serializeJson: C++オブジェクトからJSON文字列生成
実装上の注意点¶
- グローバル変数の管理:
- g_config: ランタイム設定(複数スレッドからアクセス)
- g_latest_sensor_data: 最新センサーデータ(detection イベントで更新)
-
スレッドセーフ: FreeRTOSミューテックスで保護が必要
-
メモリ効率:
- StaticJsonDocument<512>: 最大512バイトの固定サイズ
-
embedded HTML圧縮時のメモリ削減を考慮
-
エラーハンドリング:
- JSONパースエラー: 400 Bad Request
- バリデーションエラー: 400 Bad Request
-
設定反映エラー: 500 Internal Server Error
-
パフォーマンス:
- JSON生成・パース: CPU負荷がある(SPIバスと競合の可能性)
- GET リクエストは読み込みのみで軽量
- POST + SPI送信は複数ステップで時間がかかる
Next Steps¶
実装準備¶
- wifi_server.cpp テンプレート作成
- ArduinoJsonライブラリ設定 (platformio.ini)
- エラーレスポンスの統一フォーマット定義
- HTTPヘッダー (Content-Type, Content-Encoding) 設定
テスト計画¶
- curl/PostmanでのAPIテスト
- JSONパースエラーケース確認
- バリデーション境界値テスト(ch=0, ch=4, value=-1, value=4096)
- 複数APIの同時アクセステスト
ドキュメント¶
- API仕様書作成 (OpenAPI形式推奨)
- ブラウザーUI実装ガイド
- エラーメッセージ一覧表