# 帆船專題 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(); } ```