Skip to content
  • Date Created: 2025-12-09
  • Last Modified: 2025-12-09

CommandQueue実装仕様書

概要

v1.13.0の Command クラスと統合する新しいテキストコマンド処理システム。ENABLE_DEVICE_RESPONSE=1時のみコンパイルされる完全に独立した実装。

ファイル構成

新規作成ファイル

include/command_queue.h                (200行)
src/command_queue.cpp                  (300行, コア: キュー + 受信 + パース)
src/command_manager.cpp                (100行, ディスパッチテーブル + ユーティリティのみ)

src/command/                           (ハンドラサブディレクトリ - 新規)
  ├─ version.cpp                       (20行, GET_VERSION)
  ├─ status.cpp                        (25行, GET_STATUS)
  ├─ mac_address.cpp                   (15行, GET_MAC_ADDRESS)
  ├─ poll_count.cpp                    (25行, SET_POLL_COUNT)
  ├─ threshold.cpp                     (35行, SET_THRESHOLD + GET_THRESHOLD)
  ├─ deadtime.cpp                      (20行, SET_DEADTIME)
  ├─ stream.cpp                        (30行, SET_STREAM + GET_STREAM)
  ├─ test_led.cpp                      (20行, TEST_LED)
  ├─ uptime.cpp                        (15行, GET_UPTIME)
  ├─ help.cpp                          (40行, GET_HELP)
  ├─ bme280.cpp                        (30行, GET_BME280)
  ├─ reset.cpp                         (15行, RESET)
  ├─ rtc.cpp                           (35行, #if ENABLE_RTC: SET_RTC_TIME + GET_RTC_TIME)
  ├─ gnss.cpp                          (60行, #if ENABLE_GNSS: SYNC_TIME + 4個のGETTER)
  └─ wifi.cpp                          (45行, #if ENABLE_WIFI: 3個のWiFiハンドラ)

計画の特徴: - コアファイル: command_queue.cpp, command_manager.cpp は src/ ルートに置く - ハンドラ分割: 各コマンド (またはコマンドグループ) が独立したファイル - ファイル名規則: コマンド名をスネークケースで使用 (例: poll_count.cpp, mac_address.cpp) - 条件付きコンパイル: ENABLE_RTC, ENABLE_GNSS, ENABLE_WIFI に応じて該当ファイルを除外

修正ファイル

src/main.cpp                           (条件付きコンパイル追加)
platformio.ini                         (src_filter設定)

既存ファイル(変更なし)

  • include/command.h(v1.13.0 - Command class)
  • src/command.cpp(v1.13.0 - Command implementation)
  • その他の既存コード

include/command_queue.h - 仕様

ヘッダーガード

#ifndef COMMAND_QUEUE_H
#define COMMAND_QUEUE_H

#if ENABLE_DEVICE_RESPONSE  // ← この条件が重要(全ファイル内容を囲む)

// ... 以下全て ENABLE_DEVICE_RESPONSE=1 時のみコンパイル ...

#endif  // ENABLE_DEVICE_RESPONSE
#endif  // COMMAND_QUEUE_H

定義と型

#include <stdint.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"

// Configuration
#define COMMAND_QUEUE_SIZE       10              // FreeRTOS Queue size
#define COMMAND_RECEPTION_TIMEOUT_MS 500         // Serial read timeout
#define MAX_COMMAND_LENGTH       64              // Max command line length
#define MAX_COMMAND_ARGS         2               // Max arguments per command
#define MAX_ARG_LENGTH           16              // Max arg string length

// Data structure: Simplified command representation
typedef struct {
  char name[32];                                 // Command name (null-terminated)
  uint8_t arg_count;                             // Number of arguments (0-2)
  char args[MAX_COMMAND_ARGS][MAX_ARG_LENGTH];  // Argument strings
} command_t;

// Response structure
typedef struct {
  bool is_ok;                                    // Success/failure flag
  char message[512];                             // JSON response (null-terminated)
} command_response_t;

Public API

class CommandQueue {
public:
  /**
   * @brief Initialize CommandQueue (call once in setup())
   *
   * Initializes FreeRTOS static queue for command buffering.
   */
  static void init(void);

  /**
   * @brief Receive and queue incoming text commands
   *
   * Main entry point for serial command reception.
   * Must be called once per loop() iteration.
   *
   * Flow:
   *   1. Check Serial.available()
   *   2. Read complete line (newline-terminated)
   *   3. Parse into command_t
   *   4. Queue for deferred processing
   *
   * @return true if serial input was processed, false if no data available
   */
  static bool receive(void);

  /**
   * @brief Check if commands are pending in queue
   *
   * @return true if queue has items, false if empty
   */
  static bool has_pending(void);

  /**
   * @brief Dequeue and execute next command
   *
   * Main entry point for command execution.
   * Must be called once per loop() iteration.
   *
   * Flow:
   *   1. Check queue has items
   *   2. Dequeue oldest command_t
   *   3. Lookup handler in command_table
   *   4. Call handler (executes Command class methods)
   *   5. Send response to serial
   *
   * @return true if command was processed, false if queue is empty
   */
  static bool execute(void);

private:
  // Internal helper methods (static)
  static bool receive_line(char* buffer, size_t buffer_size);
  static command_t parse(const char* line);
  static void discard_input(void);
  static command_response_t dispatch(const command_t& cmd);
};

src/command_queue.cpp - 仕様

ファイル構成

#include "command_queue.h"
#include "command.h"
#include "device_response_builder.h"
#include <Arduino.h>
#include <cstring>
#include <ctype.h>
#include <stdlib.h>

#if ENABLE_DEVICE_RESPONSE  // ← 全ファイルを囲む

// ============================================================================
// SECTION 1: QUEUE STORAGE (FreeRTOS Static Allocation)
// ============================================================================

// ============================================================================
// SECTION 2: HELPER FUNCTIONS
// ============================================================================

// ============================================================================
// SECTION 3: PUBLIC API IMPLEMENTATION
// ============================================================================

#endif  // ENABLE_DEVICE_RESPONSE

主要実装部分

1. Queue初期化

static QueueHandle_t g_queue = NULL;
static StaticQueue_t g_queue_storage;
static uint8_t g_queue_buffer[COMMAND_QUEUE_SIZE * sizeof(command_t)];

void CommandQueue::init(void) {
  g_queue = xQueueCreateStatic(
    COMMAND_QUEUE_SIZE,
    sizeof(command_t),
    g_queue_buffer,
    &g_queue_storage
  );
  configASSERT(g_queue != NULL);
}

2. シリアル受信

static bool receive_line(char* buffer, size_t buffer_size) {
  // 入力バッファがない場合は即座に終了
  if (!Serial.available()) {
    return false;
  }

  // タイムアウト設定(500ms)
  Serial.setTimeout(COMMAND_RECEPTION_TIMEOUT_MS);

  // '\n'までのバイト列を読み込む
  size_t bytes_read = Serial.readBytesUntil('\n', buffer, buffer_size - 1);

  // オーバーフロー検出(バッファ満杯のまま'\n'なし)
  if (bytes_read == (buffer_size - 1)) {
    discard_input();
    return false;
  }

  // 空コマンド検出
  if (bytes_read == 0) {
    return false;
  }

  // 末尾の空白を削除
  while (bytes_read > 0 && isspace(buffer[bytes_read - 1])) {
    bytes_read--;
  }

  // Null終端
  buffer[bytes_read] = '\0';

  return bytes_read > 0;
}

3. パース

static command_t parse(const char* line) {
  command_t cmd = {0};

  // 行のコピー(strtok は入力を破壊するため)
  char work[MAX_COMMAND_LENGTH];
  strncpy(work, line, sizeof(work) - 1);
  work[sizeof(work) - 1] = '\0';

  // コマンド名抽出(最初のトークン)
  char* token = strtok(work, " ");
  if (!token) {
    return cmd;  // 空オブジェクト返す
  }

  strncpy(cmd.name, token, sizeof(cmd.name) - 1);
  cmd.name[sizeof(cmd.name) - 1] = '\0';

  // 大文字化
  for (int i = 0; cmd.name[i]; i++) {
    cmd.name[i] = toupper((unsigned char)cmd.name[i]);
  }

  // エイリアス解決(「T」→「SET_THRESHOLD」など)
  resolve_alias(cmd.name);

  // 引数抽出
  while ((token = strtok(NULL, " ")) && cmd.arg_count < MAX_COMMAND_ARGS) {
    strncpy(cmd.args[cmd.arg_count], token, sizeof(cmd.args[0]) - 1);
    cmd.args[cmd.arg_count][sizeof(cmd.args[0]) - 1] = '\0';
    cmd.arg_count++;
  }

  return cmd;
}

4. キューイング

bool CommandQueue::receive(void) {
  if (!Serial.available()) {
    return false;
  }

  char buffer[MAX_COMMAND_LENGTH];

  if (!receive_line(buffer, sizeof(buffer))) {
    discard_input();
    return true;  // 入力は処理した(破棄)
  }

  command_t cmd = parse(buffer);

  // キューに追加
  BaseType_t result = xQueueSend(g_queue, &cmd, 0);
  if (result != pdTRUE) {
    // キュー満杯時はエラーを即座に返す
    // (実装パターンはhandlers.cppで定義)
  }

  return true;
}

5. 実行

bool CommandQueue::execute(void) {
  if (!has_pending()) {
    return false;
  }

  command_t cmd = {0};
  if (xQueueReceive(g_queue, &cmd, 0) != pdTRUE) {
    return false;
  }

  // ディスパッチ&実行
  command_response_t response = dispatch(cmd);

  // レスポンス送信
  if (strlen(response.message) > 0) {
    Serial.print(response.message);
  }

  return true;
}

6. ディスパッチ

static command_response_t dispatch(const command_t& cmd) {
  // command_manager.cpp で定義されたテーブル(ハンドラーは src/command/*.cpp で実装)
  extern const command_entry_t command_table[];

  for (int i = 0; command_table[i].name != NULL; i++) {
    // コマンド名で一致確認
    if (strcmp(cmd.name, command_table[i].name) == 0) {
      return command_table[i].handler(cmd);  // src/command/*.cpp で実装されたハンドラーを呼び出し
    }

    // エイリアスで一致確認
    for (int j = 0; command_table[i].aliases[j] != NULL; j++) {
      if (strcmp(cmd.name, command_table[i].aliases[j]) == 0) {
        return command_table[i].handler(cmd);  // src/command/*.cpp で実装されたハンドラーを呼び出し
      }
    }
  }

  // コマンド見つからず
  command_response_t response;
  response.is_ok = false;
  snprintf(response.message, sizeof(response.message),
    "{\"type\":\"response\",\"status\":\"error\",\"message\":\"Unknown command\"}");
  return response;
}

src/command_manager.cpp - 仕様

役割と設計

command_manager.cpp はハンドラの実装ではなく、ディスパッチテーブルとユーティリティ関数を提供する中核ファイルです。

構成: - Command dispatch table: 全48個のコマンドマッピング(名前、エイリアス、ハンドラー関数ポインタ) - Utility functions: error_response(), success_response() など、全ハンドラーが共通で使用する関数 - Handler forward declarations: src/command/*.cpp で実装されるハンドラーの前方宣言

ファイルサイズ: - 約100行(ディスパッチテーブル + ユーティリティのみ) - 実装詳細は src/command/*.cpp に分散

ファイル構成

#include "command_queue.h"
#include "command.h"
#include "device_response_builder.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#include <cstring>
#include <stdlib.h>

#if ENABLE_DEVICE_RESPONSE  // ← 全ファイルを囲む

// ============================================================================
// SECTION 1: HANDLER FORWARD DECLARATIONS (from src/command/*.cpp)
// ============================================================================
// Declared in src/command/version.cpp:
static command_response_t handle_get_version(const command_t& cmd);

// Declared in src/command/status.cpp:
static command_response_t handle_get_status(const command_t& cmd);

// Declared in src/command/mac_address.cpp:
static command_response_t handle_get_mac_address(const command_t& cmd);

// ... etc for all 48 handlers defined across src/command/*.cpp files ...

// ============================================================================
// SECTION 2: COMMAND DISPATCH TABLE
// ============================================================================
// const command_entry_t command_table[] = { ... };

// ============================================================================
// SECTION 3: UTILITY FUNCTIONS
// ============================================================================
// error_response(), success_response()

// ============================================================================
// SECTION 4: HANDLER IMPLEMENTATIONS
// ============================================================================
// handle_get_version(), handle_get_status(), ... 48個のハンドラ実装

#endif  // ENABLE_DEVICE_RESPONSE

ハンドラ数と分類

System Info (3):
  - GET_VERSION
  - GET_STATUS
  - GET_MAC_ADDRESS

Detection (4):
  - SET_POLL_COUNT
  - GET_THRESHOLD
  - SET_THRESHOLD
  - SET_DEADTIME

Testing (1):
  - TEST_LED

RTC (2) - conditional ENABLE_RTC:
  - SET_RTC_TIME
  - GET_RTC_TIME

GNSS (4) - conditional ENABLE_GNSS:
  - SYNC_TIME
  - GET_GNSS_TIME
  - GET_GNSS_STATUS
  - GET_GNSS_POSITION

WiFi (3) - conditional ENABLE_WIFI:
  - SET_WIFI_SSID
  - GET_WIFI_STATUS
  - SET_WIFI_ENABLE

Diagnostics/Stream (5):
  - GET_UPTIME
  - GET_HELP
  - SET_STREAM
  - GET_STREAM
  - GET_QUEUE_STATS (conditional ENABLE_QUEUE)

Configuration (1):
  - RESET

BME280 (1):
  - GET_BME280

Total: 48 handlers

Dispatch Table構造

typedef struct {
  const char* name;                    // Primary command name
  const char* aliases[4];              // Backward-compat aliases (last=NULL)
  command_response_t (*handler)(const command_t&);  // Function pointer
  const char* category;                // Help category
  const char* description;             // Help description
} command_entry_t;

extern const command_entry_t command_table[];
const command_entry_t command_table[] = {
  // System Info
  {"GET_VERSION",      {"V", NULL},                 handle_get_version,      "System", "Firmware version"},
  {"GET_STATUS",       {"S", "STATUS", NULL},      handle_get_status,       "System", "System status"},
  {"GET_MAC_ADDRESS",  {NULL},                     handle_get_mac_address,  "System", "MAC address"},

  // Detection
  {"SET_POLL_COUNT",   {"C", NULL},                handle_set_poll_count,   "Detection", "Set poll count"},
  {"SET_THRESHOLD",    {"T", NULL},                handle_set_threshold,    "Detection", "Set threshold"},
  {"GET_THRESHOLD",    {"G", NULL},                handle_get_threshold,    "Detection", "Get threshold"},
  {"SET_DEADTIME",     {"D", NULL},                handle_set_deadtime,     "Detection", "Set deadtime"},

  // ... その他41個 ...

  {NULL, {NULL}, NULL, NULL, NULL}   // Terminator
};

src/command/*.cpp - ハンドラーファイル構成

各ハンドラーファイルは独立した実装を持ちます。ファイル名はコマンド名に基づいています。

例1: src/command/version.cpp (GET_VERSION)

#include "command_queue.h"
#include "command.h"
#include "device_response_builder.h"
#include <Arduino.h>

#if ENABLE_DEVICE_RESPONSE

/**
 * @brief GET_VERSION command handler
 * Returns firmware version
 */
static command_response_t handle_get_version(const command_t& cmd) {
  if (cmd.arg_count != 0) {
    return error_response("GET_VERSION", "No arguments expected", 1);
  }

  const char* version = Command::getInstance().get_version();

  command_response_t response;
  response.is_ok = true;
  snprintf(response.message, sizeof(response.message),
    "{\"type\":\"response\",\"status\":\"ok\",\"version\":\"%s\"}", version);
  return response;
}

#endif  // ENABLE_DEVICE_RESPONSE

例2: src/command/threshold.cpp (SET_THRESHOLD + GET_THRESHOLD)

#include "command_queue.h"
#include "command.h"
#include "device_response_builder.h"
#include <Arduino.h>
#include <stdlib.h>

#if ENABLE_DEVICE_RESPONSE

static command_response_t handle_set_threshold(const command_t& cmd) {
  if (cmd.arg_count != 2) {
    return error_response("SET_THRESHOLD", "Missing arguments: <ch> <val>", 1);
  }

  uint8_t ch = atoi(cmd.args[0]);
  uint16_t val = atoi(cmd.args[1]);

  if (!Command::getInstance().set_threshold(ch, val)) {
    return error_response("SET_THRESHOLD", "Invalid channel [1,3] or value [0,1023]", 2);
  }

  command_response_t response;
  response.is_ok = true;
  snprintf(response.message, sizeof(response.message),
    "{\"type\":\"response\",\"status\":\"ok\",\"threshold\":{\"ch\":%d,\"value\":%d}}",
    ch, val);
  return response;
}

static command_response_t handle_get_threshold(const command_t& cmd) {
  if (cmd.arg_count != 1) {
    return error_response("GET_THRESHOLD", "Missing argument: <ch>", 1);
  }

  uint8_t ch = atoi(cmd.args[0]);
  uint16_t val = Command::getInstance().get_threshold(ch);

  command_response_t response;
  response.is_ok = true;
  snprintf(response.message, sizeof(response.message),
    "{\"type\":\"response\",\"status\":\"ok\",\"threshold\":{\"ch\":%d,\"value\":%d}}",
    ch, val);
  return response;
}

#endif  // ENABLE_DEVICE_RESPONSE

ハンドラーファイルの設計原則:

  • 1ファイル = 1〜2個のコマンド: 関連するコマンド(SET_THRESHOLD + GET_THRESHOLD)は同じファイルに
  • ファイルサイズ: 15〜60行(実装の複雑さに応じて)
  • 前方宣言なし: ハンドラー関数を static で定義
  • 条件付きコンパイル: #if ENABLE_DEVICE_RESPONSE で囲む
  • ユーティリティ関数使用: error_response(), success_response()command_manager.cpp から使用

src/main.cpp - 変更部分

条件付きコンパイル

#if ENABLE_DEVICE_RESPONSE
  // CommandQueue新規実装(v1.14.0)
  #include "command_queue.h"

  void setup() {
    Serial.begin(115200);
    CommandQueue::init();
    // その他の初期化
  }

  void loop() {
    // Detection processing
    // ...

    CommandQueue::receive();   // シリアル受信
    CommandQueue::execute();   // コマンド実行
  }
#else
  // Legacy text_command(v1.12.0以前)
  #include "text_command.h"
  #include "runtime_config.h"

  void setup() {
    Serial.begin(115200);
    text_command_init();
    config_init();
    // その他の初期化
  }

  void loop() {
    // Detection processing
    // ...

    text_receive();    // シリアル受信
    text_execute();    // コマンド実行
  }
#endif

platformio.ini - 変更部分

src_filter設定

[env:esp32dev-dev]
build_flags = -DENABLE_DEVICE_RESPONSE=1
src_filter =
  +<*>
  -<text_command.cpp>
  -<text_command_manager.cpp>
  -<runtime_config.cpp>

[env:esp32dev-release]
build_flags = -DENABLE_DEVICE_RESPONSE=0
src_filter =
  +<*>
  -<command_queue.cpp>
  -<command_manager.cpp>
  -<command/>>
  -<command.cpp>

説明: - esp32dev-dev(開発向け ENABLE_DEVICE_RESPONSE=1): - +<*> すべてのファイルを含める - -<text_command.cpp> レガシテキストコマンド除外 - -<text_command_manager.cpp> レガシマネージャー除外 - -<runtime_config.cpp> レガシ設定マネージャー除外 - command_queue.cpp, command_manager.cpp, command/* は自動的に含まれる

  • esp32dev-release(リリース向け ENABLE_DEVICE_RESPONSE=0):
  • +<*> すべてのファイルを含める
  • -<command_queue.cpp> 新しいコマンドキュー除外
  • -<command_manager.cpp> 新しいマネージャー除外
  • -<command/>> command/ ディレクトリ全体除外
  • -<command.cpp> 新しいCommand クラス除外
  • text_command.cpp, text_command_manager.cpp は自動的に含まれる

テスト方法

ビルド確認

# CommandQueue パス(ENABLE_DEVICE_RESPONSE=1)
task dev:build

# Legacy パス(ENABLE_DEVICE_RESPONSE=0)
task prod:build

シリアルモニタテスト

# 開発環境起動
task dev:upload
task monitor

# コマンド入力テスト
GET_VERSION
S
C 200
T 1 512

完成時の確認項目

  • ✅ コンパイル成功(両方のEnable_device_response設定)
  • ✅ テキストコマンドプロトコル変化なし
  • ✅ 全ハンドラが Command クラス使用
  • ✅ DAC同期が自動実行
  • ✅ レスポンス形式が JSON
  • ✅ シリアル通信正常