0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ESP32‑S3 × Python で作る小型I/Oデバイス(第4.5回:デバイス化対応)

0
Last updated at Posted at 2026-03-08

連載一覧

はじめに

前回(第4回)では、ESP32‑S3 と Python を JSON コマンドでやり取りするための最小構成の通信ループを実装しました。
しかし、実際に「USB でつなぐだけで使える I/O デバイス」として仕上げるには、

  • ピンの役割を固定する
  • エラーコードを統一する
  • ADC のノイズ対策
  • PWM の初期化
  • PWM 設定(周波数・分解能)の永続化 ← ★2026/4/3追記
  • help / get_status の実装
  • pin_id → GPIO の抽象化

といった修正が必要になります。

そこで今回は、第4.5回としてデバイス化を見据えた改良版ファームウェアを作成します。
あわせて、このファームウェアが採用する最小限の JSON コマンド仕様も整理します。

前回からの変更点

1. 入出力ピンの固定

最小構成では "pin": 5 のように GPIO を直接指定していましたが、これは誤設定やノイズで意図しないピンを操作するリスクがあります。
→ pin_id(0〜N)で抽象化し、内部で GPIO にマッピングする方式に変更。

2. エラーコードの統一

最小構成では "message": "invalid pin" のような文字列ベースでしたが、Python API を作るときに扱いづらい。
→ "code": "INVALID_ADC_PIN_ID" のように機械可読なエラーコードを導入。

3. ADC は平均化

ESP32‑S3 の ADC はノイズが多いため、数回の値を平均化して安定した値が得られるようにしました。

4. get_status コマンドの追加

uptime や free_heap を返すことで、デバイスの状態を簡単に確認できます。

5. help コマンドの追加

外部ドキュメントを見なくても、デバイス自身が「使い方」を返せるようにしました。

6. PWM 設定(周波数・分解能)の永続化 ← ★2026/4/3追記

従来は PWM の duty(0–255)だけを設定していましたが、
PWM の周波数(freq)と分解能(res)も設定できるように拡張しました。

  • set_pwm_config
  • get_pwm_config

の 2 つのコマンドを追加し、
Preferences(NVS)に保存して電源再投入後も保持されます。

また、誤設定を防ぐために以下の範囲チェックを導入しています:

項目 許容範囲 理由
freq 1〜20000 Hz LED やサーボ用途で安全に使える範囲
res 1〜16 bit Arduino-ESP32 の LEDC API の安全範囲

pin_id と GPIO の対応について

pin_id → GPIO 対応表

種類 pin_id GPIO
DIO_IN 0–5 4, 5, 6, 7, 8, 9
DIO_OUT 0–5 10, 11, 12, 13, 14, 15
ADC 0–1 1, 2
PWM 0–1 38, 39

🔧 プロトコルの基本構造

PC → ESP32(送信)

{"cmd": "<command>", ...}

ESP32 → PC(応答)

成功時:

{"status":"ok", ...}

エラー時:

{"status":"error","code":"<ERROR_CODE>","detail":"<説明>"}

コマンド一覧

コマンド 説明 コマンド例
read_di デジタル入力を取得 {"cmd":"read_di","pin_id":0}
set_do デジタル出力を設定 {"cmd":"set_do","pin_id":1,"value":1}
read_adc ADC 値を取得 {"cmd":"read_adc","pin_id":0}
set_pwm PWM duty を設定(0–255) {"cmd":"set_pwm","pin_id":1,"duty":128}
get_pwm_config PWM の周波数・分解能を取得 {"cmd":"get_pwm_config"}
set_pwm_config PWM の周波数・分解能を設定(NVS に保存) {"cmd":"set_pwm_config","freq":5000,"res":8}
get_status デバイス状態を返す {"cmd":"get_status"}
get_io_state 全 I/O の状態を返す(※PWM 設定は含まない) {"cmd":"get_io_state"}
help コマンド一覧を返す {"cmd":"help"}
ping 通信確認 {"cmd":"ping"}

★2026/4/3 pwm_configのコマンドを追記

エラーコード一覧

code 意味
ERR_JSON_PARSE JSON が壊れている/パースできない
ERR_MISSING_CMD cmd が無い
ERR_INVALID_CMD_TYPE cmd が文字列ではない
ERR_INVALID_DIO_IN_PIN_ID 入力 pin_id が範囲外(0–5)
ERR_INVALID_DIO_OUT_PIN_ID 出力 pin_id が範囲外(0–5)
ERR_INVALID_ADC_PIN_ID ADC pin_id が範囲外(0–1)
ERR_INVALID_PWM_PIN_ID PWM pin_id が範囲外(0–1)
ERR_INVALID_VALUE set_do の value が 0/1 以外
ERR_INVALID_FREQ freq が範囲外(1〜20000)
ERR_INVALID_RES res が範囲外(1〜16)
ERR_UNKNOWN_COMMAND 未定義コマンド

ESP32‑S3 改良版ファームウェア

コードはこちら
#include <Arduino.h>
#include <ArduinoJson.h>
#include <Preferences.h>

// ------------------------------------------------------------
// 固定ピンマッピング
// ------------------------------------------------------------
const int DIO_IN_PINS[6]  = {4, 5, 6, 7, 8, 9};
const int DIO_OUT_PINS[6] = {10, 11, 12, 13, 14, 15};
const int ADC_PINS[2]     = {1, 2};
const int PWM_PINS[2]     = {38, 39};

// デフォルト PWM 設定
const int DEFAULT_PWM_FREQ = 5000;
const int DEFAULT_PWM_RES  = 8;

int PWM_FREQ = DEFAULT_PWM_FREQ;
int PWM_RES  = DEFAULT_PWM_RES;
int PWM_DUTY[2] = {0, 0};

Preferences prefs;

// ------------------------------------------------------------
// PIN_ID 範囲チェック
// ------------------------------------------------------------
bool checkRange(int id, int max) {
    return (id >= 0 && id < max);
}

// ------------------------------------------------------------
// ADC 平均化
// ------------------------------------------------------------
int readADC(int gpio) {
    const int N = 8;
    int sum = 0;
    for (int i = 0; i < N; i++) {
        sum += analogRead(gpio);
    }
    return sum / N;
}

// ------------------------------------------------------------
// エラー応答
// ------------------------------------------------------------
void sendError(const char* cmd, const char* code, const char* detail) {
    StaticJsonDocument<192> res;
    res["status"] = "error";
    res["cmd"]    = cmd;
    res["code"]   = code;
    res["detail"] = detail;
    serializeJson(res, Serial);
    Serial.println();
}

// ------------------------------------------------------------
// 非ブロッキング行読み取り
// ------------------------------------------------------------
bool readLine(String& out) {
    static String buf;

    while (Serial.available()) {
        char c = Serial.read();
        if (c == '\n') {
            out = buf;
            buf = "";
            return true;
        }
        buf += c;
    }
    return false;
}

// ------------------------------------------------------------
// PWM 設定の保存・読み込み
// ------------------------------------------------------------
int loadPWMFreq() {
    prefs.begin("esp32io", true);
    int v = prefs.getInt("pwm_freq", DEFAULT_PWM_FREQ);
    prefs.end();
    return v;
}

int loadPWMRes() {
    prefs.begin("esp32io", true);
    int v = prefs.getInt("pwm_res", DEFAULT_PWM_RES);
    prefs.end();
    return v;
}

void savePWMFreq(int v) {
    prefs.begin("esp32io", false);
    prefs.putInt("pwm_freq", v);
    prefs.end();
}

void savePWMRes(int v) {
    prefs.begin("esp32io", false);
    prefs.putInt("pwm_res", v);
    prefs.end();
}

// ------------------------------------------------------------
// 初期化
// ------------------------------------------------------------
void setup() {
    Serial.begin(115200);
    delay(100);

    PWM_FREQ = loadPWMFreq();
    PWM_RES  = loadPWMRes();

    for (int i = 0; i < 6; i++) {
        pinMode(DIO_IN_PINS[i], INPUT);
        pinMode(DIO_OUT_PINS[i], OUTPUT);
        digitalWrite(DIO_OUT_PINS[i], LOW);
    }

    for (int i = 0; i < 2; i++) {
        ledcAttach(PWM_PINS[i], PWM_FREQ, PWM_RES);
        ledcWrite(PWM_PINS[i], 0);
        PWM_DUTY[i] = 0;
    }
}

// ------------------------------------------------------------
// メインループ
// ------------------------------------------------------------
void loop() {
    yield();

    String line;
    if (!readLine(line)) return;
    line.trim();
    if (line.length() == 0) return;

    StaticJsonDocument<384> doc;
    auto err = deserializeJson(doc, line);
    if (err) {
        sendError("unknown", "ERR_JSON_PARSE", err.c_str());
        return;
    }

    if (!doc.containsKey("cmd")) {
        sendError("unknown", "ERR_MISSING_CMD", "cmd field is required");
        return;
    }
    if (!doc["cmd"].is<const char*>()) {
        sendError("unknown", "ERR_INVALID_CMD_TYPE", "cmd must be a string");
        return;
    }

    const char* cmd = doc["cmd"];

    // ------------------------------------------------------------
    // help
    // ------------------------------------------------------------
    if (strcmp(cmd, "help") == 0) {
        StaticJsonDocument<256> res;
        res["status"] = "ok";
        JsonArray arr = res.createNestedArray("commands");
        arr.add("read_di");
        arr.add("set_do");
        arr.add("read_adc");
        arr.add("set_pwm");
        arr.add("get_status");
        arr.add("get_io_state");
        arr.add("get_pwm_config");
        arr.add("set_pwm_config");
        arr.add("ping");
        arr.add("help");
        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "get_status") == 0) {
        StaticJsonDocument<192> res;
        res["status"] = "ok";
        res["uptime_ms"] = millis();
        res["free_heap"] = esp_get_free_heap_size();
        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "get_io_state") == 0) {
        StaticJsonDocument<384> res;
        res["status"] = "ok";

        JsonArray di = res.createNestedArray("dio_in");
        for (int i = 0; i < 6; i++) di.add(digitalRead(DIO_IN_PINS[i]));

        JsonArray doo = res.createNestedArray("dio_out");
        for (int i = 0; i < 6; i++) doo.add(digitalRead(DIO_OUT_PINS[i]));

        JsonArray adc = res.createNestedArray("adc");
        for (int i = 0; i < 2; i++) adc.add(readADC(ADC_PINS[i]));

        JsonArray pwm = res.createNestedArray("pwm");
        for (int i = 0; i < 2; i++) pwm.add(PWM_DUTY[i]);

        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "read_di") == 0) {
        int id = doc["pin_id"];
        if (!checkRange(id, 6)) {
            sendError(cmd, "ERR_INVALID_DIO_IN_PIN_ID", "pin_id must be 0-5");
            return;
        }

        int value = digitalRead(DIO_IN_PINS[id]);

        StaticJsonDocument<128> res;
        res["status"] = "ok";
        res["value"] = value;
        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "set_do") == 0) {
        int id = doc["pin_id"];
        int value = doc["value"];

        if (!checkRange(id, 6)) {
            sendError(cmd, "ERR_INVALID_DIO_OUT_PIN_ID", "pin_id must be 0-5");
            return;
        }
        if (!(value == 0 || value == 1)) {
            sendError(cmd, "ERR_INVALID_VALUE", "value must be 0 or 1");
            return;
        }

        digitalWrite(DIO_OUT_PINS[id], value ? HIGH : LOW);

        StaticJsonDocument<96> res;
        res["status"] = "ok";
        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "read_adc") == 0) {
        int id = doc["pin_id"];
        if (!checkRange(id, 2)) {
            sendError(cmd, "ERR_INVALID_ADC_PIN_ID", "pin_id must be 0-1");
            return;
        }

        int value = readADC(ADC_PINS[id]);

        StaticJsonDocument<128> res;
        res["status"] = "ok";
        res["value"] = value;
        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "set_pwm") == 0) {
        int id = doc["pin_id"];
        int duty = doc["duty"];

        if (!checkRange(id, 2)) {
            sendError(cmd, "ERR_INVALID_PWM_PIN_ID", "pin_id must be 0-1");
            return;
        }

        duty = constrain(duty, 0, 255);
        ledcWrite(PWM_PINS[id], duty);
        PWM_DUTY[id] = duty;

        StaticJsonDocument<96> res;
        res["status"] = "ok";
        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "get_pwm_config") == 0) {
        StaticJsonDocument<128> res;
        res["status"] = "ok";
        res["freq"] = PWM_FREQ;
        res["res"]  = PWM_RES;
        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "set_pwm_config") == 0) {
        if (!doc.containsKey("freq") || !doc.containsKey("res")) {
            sendError(cmd, "ERR_MISSING_PARAM", "freq and res are required");
            return;
        }

        int freq = doc["freq"];
        int res  = doc["res"];

        if (freq < 1 || freq > 20000) {
            sendError(cmd, "ERR_INVALID_FREQ", "freq must be 1-20000");
            return;
        }

        if (res < 1 || res > 16) {
            sendError(cmd, "ERR_INVALID_RES", "res must be 1-16");
            return;
        }

        savePWMFreq(freq);
        savePWMRes(res);

        PWM_FREQ = freq;
        PWM_RES  = res;

        for (int i = 0; i < 2; i++) {
            ledcAttach(PWM_PINS[i], PWM_FREQ, PWM_RES);
            ledcWrite(PWM_PINS[i], 0);
            PWM_DUTY[i] = 0;
        }

        StaticJsonDocument<128> resdoc;
        resdoc["status"] = "ok";
        resdoc["freq"] = freq;
        resdoc["res"]  = res;
        serializeJson(resdoc, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else if (strcmp(cmd, "ping") == 0) {
        StaticJsonDocument<96> res;
        res["status"] = "ok";
        res["message"] = "pong";
        serializeJson(res, Serial);
        Serial.println();
    }

    // ------------------------------------------------------------
    else {
        sendError(cmd, "ERR_UNKNOWN_COMMAND", "command not recognized");
    }
}

まとめ

  • 第4回の最小構成を発展させ、デバイス化を見据えた改良版ファームウェアを実装した
  • pin_id による抽象化で安全性と拡張性が向上
  • エラーコード体系を統一し、Python API から扱いやすくした
  • PWM の周波数・分解能を設定できるようになり、設定値は NVS に保存されるようになった ← ★2026/4/3追記
  • 次回の Python API(Device クラス)実装の準備が整った

次回予告

次回は、この改良版ファームウェアを前提に

  • Device クラス(Python API)
  • タイムアウト処理
  • エラー処理
  • テストツール

を実装していきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?