XIAO ESP32-S3を使った自動操舵ONOFFスイッチのスケッチ

このコードの目的は:

✅ ESP32-S3(XIAO ESP32S3)を USB HID マウスとして動作させる
✅ GPIOスイッチ入力でマウスのクリックや移動を制御する
✅ Wi-Fiアクセスポイントを起動して、スマホやPCのブラウザからマウス座標やディレイ時間を設定・保存できるようにする
✅ 保存した設定値は電源を切っても保持される
✅ 通常モードでは保存した設定値を使って自動で動作する

🎯 ✅ 全体構成の概要
このコードは以下の機能で構成されています:

1️⃣ 設定モードと通常モードの切り替え
 → 電源ON時に特定のスイッチを長押しすると設定モードに入る
 → 設定モードでは Wi-Fi AP を起動し、Webブラウザから設定画面を提供
 → 通常モードでは保存済みの設定値で動作

2️⃣ Webブラウザ設定画面
 → マウス移動量(座標値)と初期ディレイ時間を入力できる
 → 入力値はブラウザで範囲チェックされる(±127や1000~60000msなど)
 → サーバ側でも入力値の範囲制限を行い安全性を確保
 → 設定値を保存すると Preferences に記録(電源OFFでも保持)

3️⃣ 保存された設定値に基づく動作
 → 通常モードでは loop() に入ったときに一度だけ保存されたディレイ時間だけ待ち、その後保存された座標にマウスを相対移動
 → GPIOスイッチを押すとクリックや追加の移動操作ができる

4️⃣ 設定モード時の補助動作
 → 設定モード中はLEDが点灯
 → カーソルを右上隅に移動して操作しづらい位置に退避

🎯 ✅ このコードで実現すること
このコードを使うと:

✅ 一度スマホやPCから Wi-Fi 経由で移動量やディレイ時間を設定
✅ 設定内容がデバイス内部に保存される
✅ 電源を入れるたび自動で設定通り動作(自動操舵ONOFFその他スイッチとして)
✅ 操作はGPIOスイッチで簡単に制御
✅ 設定値を変更したいときは再び設定モードに入るだけ

という 「設定が簡単で一度設定すれば自律的に動作するカスタムUSBデバイス」 を実現できます。

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include "Adafruit_TinyUSB.h"

// GPIO定義
const int tactA = 0;
const int tactB = 1;
const int setupSwitch = 2;
const int LED_PIN = 3;

// Wi-Fi設定
const char* ssid = "ESP32S3-AP";

// Webサーバ
WebServer server(80);

// USB HIDマウス
Adafruit_USBD_HID usb_hid;
uint8_t mouse_report[4] = {0};

// 座標変数(初期値0)
int CURSOR_X = 0;
int CURSOR_Y = 0;
int MOVE_FILL_X = 0;
int MOVE_FILL_Y = 0;
int MOVE_RETURN_X = 0;
int MOVE_RETURN_Y = 0;
unsigned long initialMoveDelayMs = 10000; // デフォルト10秒

Preferences prefs;
bool setupMode = false;
bool movedInitial = false;

// HTMLページ
String htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <title>自動操舵ONOFF設定</title>
  <style>
    body { font-family: Arial, sans-serif; background: #f0f0f0; padding: 20px; }
    fieldset { background: #fff; border: 1px solid #ccc; padding: 15px; margin-bottom: 20px; }
    legend { font-weight: bold; }
    label { display: inline-block; width: 140px; }
    input[type=number] { width: 80px; }
    .note { font-size: 0.9em; color: #555; }
    .button { padding: 10px 20px; margin-top: 10px; }
  </style>
</head>
<body>
  <h2>自動操舵 カーソル位置設定</h2>
  <form action="/set" method="POST">
    <fieldset>
      <legend>自動操舵ONOFF</legend>
      <label><b>CURSOR_X</b>:</label>
      <input type="number" name="CURSOR_X" value="%CURSOR_X%" min="-127" max="127" required>
      <small class="note">現在値: %CURSOR_X%</small><br>

      <label><b>CURSOR_Y</b>:</label>
      <input type="number" name="CURSOR_Y" value="%CURSOR_Y%" min="-127" max="127" required>
      <small class="note">現在値: %CURSOR_Y%</small><br>
    </fieldset>

    <fieldset>
      <legend>カスタムONOFF</legend>
      <label><b>MOVE_FILL_X</b>:</label>
      <input type="number" name="MOVE_FILL_X" value="%MOVE_FILL_X%" min="-127" max="127" required>
      <small class="note">現在値: %MOVE_FILL_X%</small><br>

      <label><b>MOVE_FILL_Y</b>:</label>
      <input type="number" name="MOVE_FILL_Y" value="%MOVE_FILL_Y%" min="-127" max="127" required>
      <small class="note">現在値: %MOVE_FILL_Y%</small><br>

      <label><b>MOVE_RETURN_X</b>:</label>
      <input type="number" name="MOVE_RETURN_X" value="%MOVE_RETURN_X%" min="-127" max="127" required>
      <small class="note">現在値: %MOVE_RETURN_X%</small><br>

      <label><b>MOVE_RETURN_Y</b>:</label>
      <input type="number" name="MOVE_RETURN_Y" value="%MOVE_RETURN_Y%" min="-127" max="127" required>
      <small class="note">現在値: %MOVE_RETURN_Y%</small><br>
    </fieldset>

    <fieldset>
      <legend>初期ディレイ</legend>
      <label><b>initialMoveDelaySec</b>:</label>
      <input type="number" name="initialMoveDelaySec" value="%initialMoveDelaySec%" min="1" max="60" required>
      <small class="note">現在値: %initialMoveDelaySec% (秒)</small><br>
    </fieldset>

    <input type="submit" value="保存" class="button">
    <p class="note">※座標: -127~+127、ディレイ: 1~60秒</p>
  </form>

  <form action="/testclick" method="GET">
    <input type="submit" value="自動操舵テスト" class="button">
    <small class="note">→ CURSOR_X, CURSOR_Y に移動後クリック</small>
  </form>

  <form action="/testmove" method="GET">
    <input type="submit" value="カスタムONOFFテスト" class="button">
    <small class="note">→ MOVE_FILL_X, MOVE_FILL_Y に移動後クリック → MOVE_RETURN_X, MOVE_RETURN_Y に戻る</small>
  </form>
</body>
</html>
)rawliteral";

int constrainMouseValue(int value) {
  if (value < -127) return -127;
  if (value > 127) return 127;
  return value;
}

unsigned long constrainDelaySeconds(unsigned long value) {
  if (value < 1) return 1;
  if (value > 60) return 60;
  return value;
}

String processor(const String& var) {
  if (var == "CURSOR_X") return String(CURSOR_X);
  if (var == "CURSOR_Y") return String(CURSOR_Y);
  if (var == "MOVE_FILL_X") return String(MOVE_FILL_X);
  if (var == "MOVE_FILL_Y") return String(MOVE_FILL_Y);
  if (var == "MOVE_RETURN_X") return String(MOVE_RETURN_X);
  if (var == "MOVE_RETURN_Y") return String(MOVE_RETURN_Y);
  if (var == "initialMoveDelaySec") return String(initialMoveDelayMs / 1000);
  return "";
}

void handleRoot() {
  String page = htmlPage;
  page.replace("%CURSOR_X%", String(CURSOR_X));
  page.replace("%CURSOR_Y%", String(CURSOR_Y));
  page.replace("%MOVE_FILL_X%", String(MOVE_FILL_X));
  page.replace("%MOVE_FILL_Y%", String(MOVE_FILL_Y));
  page.replace("%MOVE_RETURN_X%", String(MOVE_RETURN_X));
  page.replace("%MOVE_RETURN_Y%", String(MOVE_RETURN_Y));
  page.replace("%initialMoveDelaySec%", String(initialMoveDelayMs / 1000));
  server.send(200, "text/html", page);
}

void handleSet() {
  if (server.hasArg("CURSOR_X")) CURSOR_X = constrainMouseValue(server.arg("CURSOR_X").toInt());
  if (server.hasArg("CURSOR_Y")) CURSOR_Y = constrainMouseValue(server.arg("CURSOR_Y").toInt());
  if (server.hasArg("MOVE_FILL_X")) MOVE_FILL_X = constrainMouseValue(server.arg("MOVE_FILL_X").toInt());
  if (server.hasArg("MOVE_FILL_Y")) MOVE_FILL_Y = constrainMouseValue(server.arg("MOVE_FILL_Y").toInt());
  if (server.hasArg("MOVE_RETURN_X")) MOVE_RETURN_X = constrainMouseValue(server.arg("MOVE_RETURN_X").toInt());
  if (server.hasArg("MOVE_RETURN_Y")) MOVE_RETURN_Y = constrainMouseValue(server.arg("MOVE_RETURN_Y").toInt());
  if (server.hasArg("initialMoveDelaySec")) initialMoveDelayMs = constrainDelaySeconds(server.arg("initialMoveDelaySec").toInt()) * 1000;

  prefs.putInt("CURSOR_X", CURSOR_X);
  prefs.putInt("CURSOR_Y", CURSOR_Y);
  prefs.putInt("MOVE_FILL_X", MOVE_FILL_X);
  prefs.putInt("MOVE_FILL_Y", MOVE_FILL_Y);
  prefs.putInt("MOVE_RETURN_X", MOVE_RETURN_X);
  prefs.putInt("MOVE_RETURN_Y", MOVE_RETURN_Y);
  prefs.putULong("initialMoveDelayMs", initialMoveDelayMs);

  server.sendHeader("Location", "/");
  server.send(303);
}

void handleTestClick() {
  mouse_report[0] = 0;
  mouse_report[1] = CURSOR_X;
  mouse_report[2] = CURSOR_Y;
  usb_hid.sendReport(1, mouse_report, 4);
  delay(100);

  mouse_report[0] = 1;
  usb_hid.sendReport(1, mouse_report, 4);
  delay(100);

  mouse_report[0] = 0;
  usb_hid.sendReport(1, mouse_report, 4);

  server.sendHeader("Location", "/");
  server.send(303);
}

void handleTestMove() {
  mouse_report[0] = 0;
  mouse_report[1] = MOVE_FILL_X;
  mouse_report[2] = MOVE_FILL_Y;
  usb_hid.sendReport(1, mouse_report, 4);
  delay(100);

  mouse_report[0] = 1;
  usb_hid.sendReport(1, mouse_report, 4);
  delay(100);

  mouse_report[0] = 0;
  mouse_report[1] = MOVE_RETURN_X;
  mouse_report[2] = MOVE_RETURN_Y;
  usb_hid.sendReport(1, mouse_report, 4);

  server.sendHeader("Location", "/");
  server.send(303);
}

void setup() {
  pinMode(tactA, INPUT_PULLDOWN);
  pinMode(tactB, INPUT_PULLDOWN);
  pinMode(setupSwitch, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  usb_hid.begin();
  prefs.begin("config", false);

  CURSOR_X = prefs.getInt("CURSOR_X", CURSOR_X);
  CURSOR_Y = prefs.getInt("CURSOR_Y", CURSOR_Y);
  MOVE_FILL_X = prefs.getInt("MOVE_FILL_X", MOVE_FILL_X);
  MOVE_FILL_Y = prefs.getInt("MOVE_FILL_Y", MOVE_FILL_Y);
  MOVE_RETURN_X = prefs.getInt("MOVE_RETURN_X", MOVE_RETURN_X);
  MOVE_RETURN_Y = prefs.getInt("MOVE_RETURN_Y", MOVE_RETURN_Y);
  initialMoveDelayMs = prefs.getULong("initialMoveDelayMs", initialMoveDelayMs);

  unsigned long pressTime = 0;
  const unsigned long threshold = 2000;

  if (digitalRead(setupSwitch) == LOW) {
    pressTime = millis();
    while (digitalRead(setupSwitch) == LOW) {
      if (millis() - pressTime >= threshold) {
        setupMode = true;
        break;
      }
    }
  }

  if (setupMode) {
    digitalWrite(LED_PIN, HIGH);
    WiFi.softAP(ssid);
    server.on("/", handleRoot);
    server.on("/set", HTTP_POST, handleSet);
    server.on("/testclick", HTTP_GET, handleTestClick);
    server.on("/testmove", HTTP_GET, handleTestMove);
    server.begin();

    for (int i = 0; i < 10; i++) {
      mouse_report[0] = 0;
      mouse_report[1] = -127;
      mouse_report[2] = -127;
      usb_hid.sendReport(1, mouse_report, 4);
      delay(50);
    }
    for (int i = 0; i < 20; i++) {
      mouse_report[0] = 0;
      mouse_report[1] = 127;
      mouse_report[2] = 0;
      usb_hid.sendReport(1, mouse_report, 4);
      delay(50);
    }
  } else {
    digitalWrite(LED_PIN, LOW);
  }

  while (!TinyUSBDevice.mounted()) delay(10);
}

void loop() {
  if (!setupMode && !movedInitial) {
    delay(initialMoveDelayMs);
    mouse_report[0] = 0;
    mouse_report[1] = CURSOR_X;
    mouse_report[2] = CURSOR_Y;
    usb_hid.sendReport(1, mouse_report, 4);
    movedInitial = true;
  }

  if (setupMode) {
    server.handleClient();
  }

  bool nowA = digitalRead(tactA);
  bool nowB = digitalRead(tactB);

  if (nowA) {
    mouse_report[0] = 1;
    usb_hid.sendReport(1, mouse_report, 4);
    delay(100);
    mouse_report[0] = 0;
    usb_hid.sendReport(1, mouse_report, 4);
  }

  if (nowB) {
    mouse_report[0] = 0;
    mouse_report[1] = MOVE_FILL_X;
    mouse_report[2] = MOVE_FILL_Y;
    usb_hid.sendReport(1, mouse_report, 4);
    delay(100);

    mouse_report[0] = 1;
    usb_hid.sendReport(1, mouse_report, 4);
    delay(100);

    mouse_report[0] = 0;
    mouse_report[1] = MOVE_RETURN_X;
    mouse_report[2] = MOVE_RETURN_Y;
    usb_hid.sendReport(1, mouse_report, 4);
  }

  delay(10);
}
🎯 ✅ 2️⃣ 各パートの説明
🚩 ① 変数・定義
const int tactA = 0;
const int tactB = 1;
onst int setupSwitch = 2;
const int LED_PIN = 3;
GPIOピンの番号
tactA / tactB: 操作用スイッチ
setupSwitch: 設定モードに入るためのスイッチ
LED_PIN: 設定モード中はLED点灯

📝 座標関連変数 & ディレイ変数も初期値0/10000で定義
int CURSOR_X = 0;
int CURSOR_Y = 0;
unsigned long initialMoveDelayMs = 10000; // 10秒
Preferences用の保存領域 prefs、Webサーバ server、USB HID usb_hid も初期化。

🚩 ② setup() の流れ
✅ GPIOピンモード初期化
✅ USB HID 開始
✅ Preferencesから保存済み値を読み込み(座標、delay値)
CURSOR_X = prefs.getInt("CURSOR_X", CURSOR_X);
initialMoveDelayMs = prefs.getULong("initialMoveDelayMs", initialMoveDelayMs);
✅ setupSwitch を2秒間長押し → 設定モードに入る

→ 設定モードでは:

Wi-Fi AP モード開始

Webサーバ起動(ブラウザ設定用ページ)

LED点灯

マウスカーソルを右上隅に相対移動する処理

🚩 ③ 設定モードのWeb UI
ブラウザからアクセスすると:

✅ 現在の CURSOR_X, CURSOR_Y や initialMoveDelayMs が入力欄に反映
✅ フォームから値を送信 → handleSet() で受信
✅ サーバ側でも 座標値は -127~127 に制限、delay値は 1000~60000 に制限
✅ 保存した値は Preferences に記録 → 電源OFFでも保持
prefs.putInt("CURSOR_X", CURSOR_X);
prefs.putULong("initialMoveDelayMs", initialMoveDelayMs);
✅ Web画面には 「現在値」表示 や 入力範囲説明 も記載

🚩 ④ loop() の通常モード処理
✅ 通常モード(設定モードじゃない)では loop() に入ったとき一度だけ:
delay(initialMoveDelayMs);
mouse_report[1] = CURSOR_X;
mouse_report[2] = CURSOR_Y;
usb_hid.sendReport(1, mouse_report, 4);
movedInitial = true;
→ 保存 delay 時間だけ待つ → 保存された CURSOR_X, CURSOR_Y に相対移動
→ movedInitial=true にして再度移動しないように制御。

🚩 ⑤ 操作用GPIOの処理(常時有効)
✅ tactA を押す → マウス左クリック
✅ tactB を押す → MOVE_FILL_X, MOVE_FILL_Y に移動 → クリック → MOVE_RETURN_X, MOVE_RETURN_Y に戻る

→ これらは loop 内で GPIO入力が HIGH になったら即座に実行

🚩 ⑥ 設定モード時は Web サーバ動作
設定モード時:

✅ server.handleClient() で ブラウザからのリクエストに応答

→ / → 設定画面
→ /set → 設定保存
→ /testclick → CURSOR_X, CURSOR_Y に移動&クリック
→ /testmove → MOVE_FILL_X, MOVE_FILL_Y に移動→クリック→戻り

🎯 ✅ 全体の動作イメージ
1️⃣ 電源ON
→ setupSwitch 長押し → 設定モード(Wi-Fi AP + Web UI)
→ setupSwitch 長押しなし → 通常モード

2️⃣ 通常モードの流れ:

→ loop() に入ったタイミングで 保存済み delay 時間待機
→ 1度だけ CURSOR_X, CURSOR_Y に相対移動
→ 以降は GPIO操作や外部コマンドでマウス動作

🎯 ✅ このコードの特徴まとめ
✅ ブラウザ上で GUI から操作・設定保存可能
✅ 保存値は電源OFFでも維持
✅ loop開始時の自動移動タイミング delay も設定可能
✅ 設定モード時のみ Wi-Fi 起動 → 通常モードは省電力

  • このエントリーをはてなブックマークに追加
  • follow us in feedly

この記事の著者

momo

1966年訓子府町生まれの訓子府育ち。玉葱や米、メロンを栽培する農家です。一眼レフを本格的に始めたのは2005年。仕事の時でもいつでもカメラを持ち歩く自称農場カメラマン。普段の生活を撮るのが主で、その他ストロボを使っての商品撮影、スタジオ撮影も。愛好家グループで年1回写真展を行っている。農機具の改造や作製、電子工作など、モノづくりが大好きです。

この著者の最新の記事

関連記事

コメント

  1. この記事へのコメントはありません。

  1. この記事へのトラックバックはありません。

2025年5月
 1234
567891011
12131415161718
19202122232425
262728293031  

カテゴリー

ページ上部へ戻る