Skip to content

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

アーキテクチャの理解

  1. メモリが単一ソース: RuntimeConfig g_config がすべての設定値の唯一のソース
  2. GET: メモリから読み込み
  3. POST: メモリに書き込み + HW反映

  4. GETとPOSTの違い:

  5. GET /api/status: 読み込みのみ(内部状態変化なし)
  6. POST /api/threshold: 読み込み+書き込み+HW反映(即座に反映)
  7. GET /api/sensor-data: 読み込みのみ(最新イベントデータ返却)

  8. バリデーション:

  9. チャネル: 1-3 (3チャネル対応)
  10. スレッショルド値: 0-4095 (12ビットADC対応)
  11. JSON形式チェック: 必須フィールド確認

  12. リアルタイム性:

  13. POST実行直後、DACハードウェアがスレッショルド値を変更
  14. GET実行時には常に現在のメモリ状態を返す
  15. センサーデータは検出イベント時に自動更新

  16. ArduinoJsonライブラリの活用:

  17. StaticJsonDocument: スタックメモリで固定サイズ確保
  18. deserializeJson: HTTP POSTボディから構造化データ抽出
  19. serializeJson: C++オブジェクトからJSON文字列生成

実装上の注意点

  1. グローバル変数の管理:
  2. g_config: ランタイム設定(複数スレッドからアクセス)
  3. g_latest_sensor_data: 最新センサーデータ(detection イベントで更新)
  4. スレッドセーフ: FreeRTOSミューテックスで保護が必要

  5. メモリ効率:

  6. StaticJsonDocument<512>: 最大512バイトの固定サイズ
  7. embedded HTML圧縮時のメモリ削減を考慮

  8. エラーハンドリング:

  9. JSONパースエラー: 400 Bad Request
  10. バリデーションエラー: 400 Bad Request
  11. 設定反映エラー: 500 Internal Server Error

  12. パフォーマンス:

  13. JSON生成・パース: CPU負荷がある(SPIバスと競合の可能性)
  14. GET リクエストは読み込みのみで軽量
  15. 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実装ガイド
  • エラーメッセージ一覧表