---
# System prepended metadata

title: 帆船專題 ESP32-S

---

# 帆船專題 ESP32-S
ESP32為系列產品，由上海的中國公司"樂鑫資訊科技(Espressif)"建立和開發，而此系列主打低成本、低功耗，並且整合Wi-Fi和雙模藍芽。有許多Maker透過ESP32的開發板做IoT相關技術的應用，如遠端查看目前的大氣壓力、溫度濕度、倒車雷達更甚至整合在水務上，去監控水的流速及水壓等，讓人類的生活更進步。ESP32板子上有許多針腳其中數字稱為GPIO，另外還有5V、3V3、GND及可以做進階應用Pin(Tx Rx等)。
- VIN3.3V & 5V：主要供給需要使用到電的零件或感測器。
- GND：主要作為接地，使電源有迴路。
- GPIO：主要作為控制零件的開啟或關閉，或者讀取感測器的數值，某些GPIO都有額外的敘述，這些敘述代表這些腳位有不同的功能，例如：只允許輸入，不允許輸出、預設給Esp32s內部記憶體使用...等等功能。
![image](https://hackmd.io/_uploads/H1JAqrL0el.png)

## 腳位
![image](https://hackmd.io/_uploads/BJ2LArLAxl.png)

## 伺服馬達測試
```
#include <ESP32Servo.h>

Servo myservo;

void setup() {
  myservo.setPeriodHertz(50);
  myservo.attach(15, 500, 2500); // GPIO15, min 0.5ms, max 2.5ms
}

void loop() {
  for (int pos = 0; pos <= 180; pos++) {
    myservo.write(pos);
    delay(15);
  }
}
```
## 藍芽測試
```
#include <BluetoothSerial.h>

char command;

BluetoothSerial SerialBT;

int led = 2;  //LED

void setup() {
  Serial.begin(115200); // 建議使用 115200鮑率
  pinMode(led, OUTPUT);
  SerialBT.begin("ESP32_BT");
}

void loop() {
  // put your main code here, to run repeatedly:
  if (SerialBT.available()) {
    command = SerialBT.read();
    if(command == 'F'){
      //收到1，開啟LED
      digitalWrite(led,HIGH);
      Serial.println("收到1");
    }
    if(command == 'B'){
      //收到2，關閉LED
      digitalWrite(led,LOW);
      Serial.println("收到2");
    }
  delay(10);
}
```
## 藍牙1 
```
#include <BluetoothSerial.h>
#include <ESP32Servo.h> //請先安裝ESP32Servo程式庫

char command;

BluetoothSerial SerialBT;
Servo myservo1,myservo2; //建立一個伺服馬達物件
int led = 2;  //LED
int servoPin1 = 15;       // 定義信號線接腳 (GPIO 15)
int servoPin2 = 16;       // 定義信號線接腳 (GPIO 16)

void setup() {
  Serial.begin(115200); // 建議使用 115200鮑率
  pinMode(led, OUTPUT);
  //Serial.println("ESP32 OK!");
  //SG90的脈衝寬度500~2400
  myservo1.setPeriodHertz(50);        // 設定 PWM 頻率為 50Hz
  myservo1.attach(servoPin1, 500, 2500);     // 接腳 GPIO15，最小/最大脈衝寬度
  myservo2.setPeriodHertz(50);        // 設定 PWM 頻率為 50Hz
  myservo2.attach(servoPin2, 500, 2500);  // 接腳 GPIO16，最小/最大脈衝寬度
  SerialBT.begin("ESP32_BT");
}

void loop() {
  // put your main code here, to run repeatedly:
  //Serial.print("Hello World!"); // 傳送字串
    //char value;
  if (SerialBT.available()) {
    command = SerialBT.read();
    if(command == 'F'){
      //收到1，開啟LED
      digitalWrite(led,HIGH);
      myservo1.write(90);  //角度0~180
      Serial.println("收到1");
    }
    if(command == 'B'){
      //收到2，關閉LED
      digitalWrite(led,LOW);
      myservo1.write(0);  //角度0~180
      Serial.println("收到2");
    }
    if(command == 'L'){
      //收到1，開啟LED
      digitalWrite(led,HIGH);
      myservo2.write(90); //角度0~180
      Serial.println("收到1");
    }
    if(command == 'R'){
      //收到2，關閉LED
      digitalWrite(led,LOW);
      myservo2.write(0);  //角度0~180
      Serial.println("收到2");
    }
  }

  delay(10);
}
```
## 藍牙2
```
#include <BluetoothSerial.h>
#include <ESP32Servo.h> //請先安裝ESP32Servo程式庫

char command;

BluetoothSerial SerialBT;
Servo myservo1,myservo2; //建立一個伺服馬達物件
int led = 2;  //LED
int servoPin1 = 15;       // 定義信號線接腳 (GPIO 15)
int servoPin2 = 16;       // 定義信號線接腳 (GPIO 16)
int angle1 = 0;
int angle2 = 0;

void setup() {
  Serial.begin(115200); // 建議使用 115200鮑率
  pinMode(led, OUTPUT);
  //Serial.println("ESP32 OK!");
  //SG90的脈衝寬度500~2400
  myservo1.setPeriodHertz(50);        // 設定 PWM 頻率為 50Hz
  myservo1.attach(servoPin1, 500, 2500);     // 接腳 GPIO15，最小/最大脈衝寬度
  myservo2.setPeriodHertz(50);        // 設定 PWM 頻率為 50Hz
  myservo2.attach(servoPin2, 500, 2500);  // 接腳 GPIO16，最小/最大脈衝寬度
  myservo1.write(angle1);  //角度0
  myservo2.write(angle2);  //角度0
  SerialBT.begin("ESP32_BT");
}

void loop() {
  // put your main code here, to run repeatedly:
  //Serial.print("Hello World!"); // 傳送字串
    //char value;
  if (SerialBT.available()) {
    command = SerialBT.read();
    if(command == 'F'){
      //收到1，開啟LED
      digitalWrite(led,HIGH);
      angle1 += 10;
      if (angle1 > 90) {
        angle1 = 90;
      }
      myservo1.write(angle1);  //角度
      Serial.println("舵增加");
    }
    if(command == 'B'){
      //收到2，關閉LED
      digitalWrite(led,LOW);
      angle1 -= 10;
      if (angle1 < 0) {
        angle1 = 0;
      }
      myservo1.write(angle1);  //角度
      Serial.println("舵減少");
    }
    if(command == 'L'){
      //收到1，開啟LED
      digitalWrite(led,HIGH);
      angle2 += 10;
      if (angle2 > 90) {
        angle2 = 90;
      }
      myservo2.write(angle2);  //角度
      Serial.println("帆增加");
    }
    if(command == 'R'){
      //收到2，關閉LED
      digitalWrite(led,LOW);
      angle2 -= 10;
      if (angle2 < 0) {
        angle2 = 0;
      }
      myservo2.write(angle2);  //角度
      Serial.println("帆減少");
    }
  }

  delay(10);
}
```
## wifi1
```
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>

// ========= WiFi 設定 =========
const char* ssid = "";         // 修改為你的 WiFi 名稱
const char* password = "";    // 修改為你的 WiFi 密碼

// ========= 伺服設定 =========
Servo servo1, servo2;
const int servoPin1 = 13;  // 第 1 個伺服馬達的腳位
const int servoPin2 = 15;  // 第 2 個伺服馬達的腳位
int pos1 = 0; // 初始角度
int pos2 = 0;

// ========= 建立 Web Server =========
WebServer server(80);

// ========= 建立網頁內容 =========
String createHTML(int angle1, int angle2) {
  String html = R"rawliteral(
  <!DOCTYPE html>
  <html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="utf-8">
    <title>ESP32 伺服馬達控制</title>
    <style>
      html { font-family: sans-serif; text-align: center; }
      h1 { margin: 30px auto; }
      .slider { width: 80%; }
      .container { margin: 20px auto; }
    </style>
  </head>
  <body>
    <h1>ESP32 雙伺服馬達控制</h1>

    <div class="container">
      <h3>伺服1角度: <span id="angle1">)rawliteral" + String(angle1) + R"rawliteral(</span>°</h3>
      <input type="range" min="0" max="180" value=")rawliteral" + String(angle1) + R"rawliteral(" class="slider" id="servo1Slider" onchange="updateServo(1, this.value)">
    </div>

    <div class="container">
      <h3>伺服2角度: <span id="angle2">)rawliteral" + String(angle2) + R"rawliteral(</span>°</h3>
      <input type="range" min="0" max="180" value=")rawliteral" + String(angle2) + R"rawliteral(" class="slider" id="servo2Slider" onchange="updateServo(2, this.value)">
    </div>

    <script>
      function updateServo(id, pos) {
        document.getElementById("angle" + id).innerHTML = pos;
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/set?id=" + id + "&value=" + pos, true);
        xhr.send();
      }
    </script>
  </body>
  </html>
  )rawliteral";
  return html;
}

// ========= 處理首頁請求 =========
void handleRoot() {
  server.send(200, "text/html", createHTML(pos1, pos2));
}

// ========= 處理設定伺服角度請求 =========
void handleServoSet() {
  if (server.hasArg("id") && server.hasArg("value")) {
    int id = server.arg("id").toInt();
    int val = server.arg("value").toInt();

    if (id == 1) {
      pos1 = val;
      servo1.write(pos1);
      Serial.printf("伺服1 移動到 %d°\n", pos1);
    } else if (id == 2) {
      pos2 = val;
      servo2.write(pos2);
      Serial.printf("伺服2 移動到 %d°\n", pos2);
    }
    server.send(200, "text/plain", "OK");
  } else {
    server.send(400, "text/plain", "錯誤的參數");
  }
}

// ========= 初始化 =========
void setup() {
  Serial.begin(115200);

  servo1.setPeriodHertz(50);
  servo2.setPeriodHertz(50);
  servo1.attach(servoPin1, 500, 2400);
  servo2.attach(servoPin2, 500, 2400);

  servo1.write(pos1);
  servo2.write(pos2);

  WiFi.begin(ssid, password);
  Serial.print("連線中");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi 連線成功！");
  Serial.print("IP 位址: ");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);
  server.on("/set", handleServoSet);
  server.begin();
  Serial.println("HTTP 伺服器啟動完成");
}

// ========= 主迴圈 =========
void loop() {
  server.handleClient();
}

```
## wifi2
```
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>

// ========= WiFi 設定 =========
const char* ssid = "";           // 修改為你的 WiFi 名稱
const char* password = "";       // 修改為你的 WiFi 密碼

// ========= 伺服設定 =========
Servo servo1, servo2;
const int servoPin1 = 13;  // 第 1 個伺服馬達的腳位
const int servoPin2 = 15;  // 第 2 個伺服馬達的腳位
int pos1 = 0; // 初始角度
int pos2 = 0;

// ========= 建立 Web Server =========
WebServer server(80);

// ========= 建立網頁內容 =========
String createHTML(int angle1, int angle2, String ipAddress) {
  String html =  // 不要只用 R"rawliteral"
    "<!DOCTYPE html>\n"
    "<html>\n"
    "<head>\n"
    "  <meta name='viewport' content='width=device-width, initial-scale=1'>\n"
    "  <meta charset='utf-8'>\n"
    "  <title>ESP32 伺服馬達控制</title>\n"
    "  <style>\n"
    "    html { font-family: sans-serif; text-align: center; }\n"
    "    h1 { margin: 30px auto; }\n"
    "    .ipinfo { font-size: 1.2em; margin-bottom: 20px; color: #3366BB; }\n"
    "    .slider { width: 80%; }\n"
    "    .container { margin: 20px auto; }\n"
    "  </style>\n"
    "</head>\n"
    "<body>\n"
    "  <h1>ESP32 雙伺服馬達控制</h1>\n"
    "  <div class='ipinfo'>目前裝置 IP 位址：" + ipAddress + "</div>\n"

    "  <div class='container'>\n"
    "    <h3>伺服1角度: <span id='angle1'>" + String(angle1) + "</span>°</h3>\n"
    "    <input type='range' min='0' max='180' value='" + String(angle1) + "' class='slider' id='servo1Slider' onchange='updateServo(1, this.value)'>\n"
    "  </div>\n"

    "  <div class='container'>\n"
    "    <h3>伺服2角度: <span id='angle2'>" + String(angle2) + "</span>°</h3>\n"
    "    <input type='range' min='0' max='180' value='" + String(angle2) + "' class='slider' id='servo2Slider' onchange='updateServo(2, this.value)'>\n"
    "  </div>\n"

    "  <script>\n"
    "    function updateServo(id, pos) {\n"
    "      document.getElementById('angle' + id).innerHTML = pos;\n"
    "      var xhr = new XMLHttpRequest();\n"
    "      xhr.open('GET', '/set?id=' + id + '&value=' + pos, true);\n"
    "      xhr.send();\n"
    "    }\n"
    "  </script>\n"
    "</body>\n"
    "</html>\n";
  return html;
}

// ========= 處理首頁請求 =========
void handleRoot() {
  String ipAddress = WiFi.localIP().toString();
  server.send(200, "text/html", createHTML(pos1, pos2, ipAddress));
}

// ========= 處理設定伺服角度請求 =========
void handleServoSet() {
  if (server.hasArg("id") && server.hasArg("value")) {
    int id = server.arg("id").toInt();
    int val = server.arg("value").toInt();

    if (id == 1) {
      pos1 = val;
      servo1.write(pos1);
      Serial.printf("伺服1 移動到 %d°\n", pos1);
    } else if (id == 2) {
      pos2 = val;
      servo2.write(pos2);
      Serial.printf("伺服2 移動到 %d°\n", pos2);
    }
    server.send(200, "text/plain", "OK");
  } else {
    server.send(400, "text/plain", "錯誤的參數");
  }
}

// ========= 初始化 =========
void setup() {
  Serial.begin(115200);

  servo1.setPeriodHertz(50);
  servo2.setPeriodHertz(50);
  servo1.attach(servoPin1, 500, 2400);
  servo2.attach(servoPin2, 500, 2400);

  servo1.write(pos1);
  servo2.write(pos2);

  WiFi.begin(ssid, password);
  Serial.print("連線中");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi 連線成功！");
  Serial.print("IP 位址: ");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);
  server.on("/set", handleServoSet);
  server.begin();
  Serial.println("HTTP 伺服器啟動完成");
}

// ========= 主迴圈 =========
void loop() {
  server.handleClient();
}
```
## wifi button
```
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>

// ======= WiFi 設定 =======
const char* ssid = "";      // 修改為你的 WiFi 名稱
const char* password = "";  // 修改為你的 WiFi 密碼

// ======= 伺服設定 =======
Servo servoSail, servoRudder;
const int sailPin = 13;
const int rudderPin = 15;
int sailPos = 90;   //初始角度
int rudderPos = 90; //初始角度

// ======= Web Server =======
WebServer server(80);

// ======= 建立網頁內容（美化＋按鈕）=======
String createHTML(int sailAngle, int rudderAngle, String ipAddress) {
  String html = "<!DOCTYPE html><html><head>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<meta charset='utf-8'><title>ESP32 帆&舵伺服控制</title>";
  html += "<style>";
  html += "body{background:#f8f8ff; font-family:Tahoma,sans-serif; text-align:center;}";
  html += "h1{color:#0066cc;margin:18px;} .ipbox{color:#444;padding:8px;}";
  html += ".servo-box{display:inline-block; margin:16px; padding:18px; border-radius:18px; background:#e0ecff; box-shadow:0 3px 10px #bbc8e3;}";
  html += ".servo-title{font-size:1.3em; color:#23408c; margin-bottom:15px;} .angle{font-weight:bold; color:#cc4400;}";
  html += ".btn-grid{display:grid; grid-template-columns:repeat(5,1fr); gap:7px;}";
  html += ".ctrl-btn{border:none;padding:15px 3px; background:#2577cc;color:white; font-size:1em; border-radius:10px; font-weight:bold; cursor:pointer; box-shadow:0 1.5px 6px #8cb3e0;transition:transform .09s;background-image: linear-gradient(#3fb8ff, #2563b7);}";
  html += ".ctrl-btn:active{transform:scale(1.09);background-image:linear-gradient(#62ffa8,#2577cc);}";
  html += ".ctrl-btn.selected{background:#e69b43;color:#17413d;}";
  html += "</style></head><body>";
  html += "<h1>ESP32 雙伺服電控系統</h1>";
  html += "<div class='ipbox'>目前裝置 IP 位址：<b>" + ipAddress + "</b></div>";

  // 帆 (Servo1) 按鈕區加 id
  html += "<div class='servo-box'><div class='servo-title'>控制帆</div>";
  html += "<div>目前角度：<span class='angle' id='sailAngle'>" + String(sailAngle) + "</span>°</div><br>";
  html += "<div class='btn-grid' id='sailBtns'>";
  for (int v = 0; v <= 180; v += 45) {
    html += String("<button class='ctrl-btn");
    if (sailAngle == v) html += " selected";
    html += "' onclick='setServo(1," + String(v) + ")'>" + String(v) + "°</button>";
  }
  html += "</div></div>";

  // 舵 (Servo2) 按鈕區加 id
  html += "<div class='servo-box'><div class='servo-title'>控制舵</div>";
  html += "<div>目前角度：<span class='angle' id='rudderAngle'>" + String(rudderAngle) + "</span>°</div><br>";
  html += "<div class='btn-grid' id='rudderBtns'>";
  for (int v = 0; v <= 180; v += 45) {
    html += String("<button class='ctrl-btn");
    if (rudderAngle == v) html += " selected";
    html += "' onclick='setServo(2," + String(v) + ")'>" + String(v) + "°</button>";
  }
  html += "</div></div>";

  // JS 修正帆與舵分開高亮
  html += "<script>";
  html += "function setServo(id, value) {";
  html += "fetch('/set?id='+id+'&value='+value).then(()=>{";
  html += "if(id==1) {";
  html += "document.getElementById('sailAngle').innerHTML=value;";
  html += "let btns=document.querySelectorAll('#sailBtns .ctrl-btn');";
  html += "btns.forEach((b)=>{b.classList.remove('selected'); if(b.innerText==value+'°'){b.classList.add('selected');}});";
  html += "}";
  html += "if(id==2) {";
  html += "document.getElementById('rudderAngle').innerHTML=value;";
  html += "let btns=document.querySelectorAll('#rudderBtns .ctrl-btn');";
  html += "btns.forEach((b)=>{b.classList.remove('selected'); if(b.innerText==value+'°'){b.classList.add('selected');}});";
  html += "}";
  html += "});}";
  html += "</script>";
  html += "</body></html>";
  return html;
}

// ======= 頁面回應 =======
void handleRoot() {
  String ip = WiFi.localIP().toString();
  server.send(200, "text/html", createHTML(sailPos, rudderPos, ip));
}

void handleServoSet() {
  if(server.hasArg("id") && server.hasArg("value")) {
    int id = server.arg("id").toInt();
    int val = server.arg("value").toInt();
    if(id==1){
      sailPos = val;
      servoSail.write(sailPos);
      Serial.printf("帆(Servo1) 移動到 %d°\n", sailPos);
    }
    else if(id==2){
      rudderPos = val;
      servoRudder.write(rudderPos);
      Serial.printf("舵(Servo2) 移動到 %d°\n", rudderPos);
    }
    server.send(200, "text/plain", "OK");
  } else {
    server.send(400, "text/plain", "參數錯誤");
  }
}

// ======= 初始化 =======
void setup() {
  Serial.begin(115200);
  servoSail.setPeriodHertz(50);
  servoRudder.setPeriodHertz(50);
  servoSail.attach(sailPin, 500, 2400);
  servoRudder.attach(rudderPin, 500, 2400);
  servoSail.write(sailPos);
  servoRudder.write(rudderPos);
  WiFi.begin(ssid, password);
  Serial.print("連線中");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500); Serial.print(".");
  }
  Serial.println("\nWiFi 已連線");
  Serial.print("IP 位址: "); Serial.println(WiFi.localIP());

  server.on("/", handleRoot);
  server.on("/set", handleServoSet);
  server.begin();
  Serial.println("HTTP 伺服器已啟動");
}

// ======= 主迴圈 =======
void loop() {
  server.handleClient();
}
```
##  wifi button slider (目前使用版本)
```
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>

// ======= WiFi 設定 =======
const char* ssid = "";      // 修改為你的 WiFi 名稱
const char* password = "";  // 修改為你的 WiFi 密碼

// ======= 伺服設定 =======
Servo servoSail, servoRudder;
const int sailPin = 13;
const int rudderPin = 15;
int sailPos = 90;
int rudderPos = 90;

// ======= Web Server =======
WebServer server(80);

// ======= 響應式網頁內容，上下布局 =======
String createHTML(int sailAngle, int rudderAngle, String ipAddress) {
  String html = "<!DOCTYPE html><html><head>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<meta charset='utf-8'><title>ESP32 帆&舵伺服控制</title>";
  html += "<style>";
  html += "body{background:#f8f8ff; font-family:Tahoma,sans-serif; margin:0;}";
  html += "h1{color:#0066cc; margin:18px auto 8px; text-align:center;} .ipbox{color:#444; text-align:center;margin-bottom:12px;}";
  html += ".container{display:flex;flex-direction:column;max-width:500px;margin:30px auto;gap:30px;}";
  html += ".servo-box{padding:18px 12px; border-radius:16px; background:#e0ecff; box-shadow:0 2px 10px #bbc8e3; margin:0 auto;width:94%;}";
  html += ".servo-title{font-size:1.2em; color:#23408c; margin-bottom:10px;}";
  html += ".angle{font-weight:bold; color:#cc4400;}";
  html += ".btn-grid{display:grid; grid-template-columns:repeat(5,1fr); gap:8px; justify-items:center; margin-top:10px;}";
  html += ".ctrl-btn{border:none; padding:14px 4px; background:#2577cc;color:white; font-size:1em; border-radius:10px; font-weight:bold; cursor:pointer;";
  html += "box-shadow:0 1.5px 6px #8cb3e0;transition:transform .07s;background-image: linear-gradient(#3fb8ff, #2563b7);width:96%;}";
  html += ".ctrl-btn:active{transform:scale(1.09);background-image:linear-gradient(#62ffa8,#2577cc);}";
  html += ".ctrl-btn.selected{background:#e69b43;color:#17413d;}";
  html += ".slider-box{margin-top:16px; display:flex; flex-direction:column;align-items:center;}";
  html += ".slider{width:90%;max-width:350px;margin:16px 0;}";
  html += "@media(max-width:600px){.container{max-width:99vw}.servo-box{width:99%;}}";
  html += "</style></head><body>";
  html += "<h1>ESP32 雙伺服電控系統</h1>";
  html += "<div class='ipbox'>目前裝置 IP 位址：<b>" + ipAddress + "</b></div>";
  html += "<div class='container'>";
  // 帆 (Servo1)區-按鈕
  html += "<div class='servo-box'><div class='servo-title'>控制帆</div>";
  html += "<div>目前角度：<span class='angle' id='sailAngle'>" + String(sailAngle) + "</span>°</div>";
  html += "<div class='btn-grid' id='sailBtns'>";
  for (int v = 0; v <= 180; v += 45) {
    html += String("<button class='ctrl-btn");
    if (sailAngle == v) html += " selected";
    html += "' onclick='setServo(1," + String(v) + ")'>" + String(v) + "°</button>";
  }
  html += "</div></div>";
  // 舵 (Servo2)區-滑桿
  html += "<div class='servo-box'><div class='servo-title'>控制舵</div>";
  html += "<div>目前角度：<span class='angle' id='rudderAngle'>" + String(rudderAngle) + "</span>°</div>";
  html += "<div class='slider-box'>";
  html += "<input type='range' min='0' max='180' value='" + String(rudderAngle) + "' class='slider' id='rudderSlider' oninput='rudderVal.innerText=this.value' onchange='setServo(2,this.value)'>";
  html += "<div>選擇角度：<span class='angle' id='rudderVal'>" + String(rudderAngle) + "</span>°</div>";
  html += "</div></div>";
  html += "</div>";
  // JS 操作
  html += "<script>";
  html += "function setServo(id, value) {";
  html += "fetch('/set?id='+id+'&value='+value).then(()=>{";
  html += "if(id==1){";
  html += "document.getElementById('sailAngle').innerHTML=value;";
  html += "let btns=document.querySelectorAll('#sailBtns .ctrl-btn');";
  html += "btns.forEach((b)=>{b.classList.remove('selected');if(b.innerText==value+'°'){b.classList.add('selected');}});";
  html += "}else{";
  html += "document.getElementById('rudderAngle').innerHTML=value;";
  html += "document.getElementById('rudderVal').innerText=value;";
  html += "document.getElementById('rudderSlider').value=value;";
  html += "}";
  html += "});}";
  html += "</script>";
  html += "</body></html>";
  return html;
}

// ======= 頁面回應 =======
void handleRoot() {
  String ip = WiFi.localIP().toString();
  server.send(200, "text/html", createHTML(sailPos, rudderPos, ip));
}

void handleServoSet() {
  if (server.hasArg("id") && server.hasArg("value")) {
    int id = server.arg("id").toInt();
    int val = server.arg("value").toInt();
    if (id == 1) {
      sailPos = val;
      servoSail.write(sailPos);
      Serial.printf("帆(Servo1) 移動到 %d°\n", sailPos);
    } else if (id == 2) {
      rudderPos = val;
      servoRudder.write(rudderPos);
      Serial.printf("舵(Servo2) 移動到 %d°\n", rudderPos);
    }
    server.send(200, "text/plain", "OK");
  } else {
    server.send(400, "text/plain", "參數錯誤");
  }
}

// ======= 初始化 =======
void setup() {
  Serial.begin(115200);
  servoSail.setPeriodHertz(50);
  servoRudder.setPeriodHertz(50);
  servoSail.attach(sailPin, 500, 2400);
  servoRudder.attach(rudderPin, 500, 2400);
  servoSail.write(sailPos);
  servoRudder.write(rudderPos);

  WiFi.begin(ssid, password);
  Serial.print("連線中");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500); Serial.print(".");
  }
  Serial.println("\nWiFi 已連線");
  Serial.print("IP 位址: "); Serial.println(WiFi.localIP());

  server.on("/", handleRoot);
  server.on("/set", handleServoSet);
  server.begin();
  Serial.println("HTTP 伺服器已啟動");
}

// ======= 主迴圈 =======
void loop() {
  server.handleClient();
}
```