連載一覧
- 第1回:プロジェクト概要
- 第2回:ESP32-S3 の環境構築
- 第3回:I/O の実装(ADC / PWM / DIO)
- 第4回:通信プロトコル(JSON コマンド)
- 第4.5回:デバイス化対応
- 第5回:Python アプリの API 実装
- 第6回:デバイスとしてまとめる(設計編)
- 第7回:実装編・応用編(GUI / アプリ化 / 拡張の可能性)
はじめに
前回(第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_configget_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)
- タイムアウト処理
- エラー処理
- テストツール
を実装していきます。