# 2026 陽明交大電機系一日營硬體維修手冊 由於這次專案實作的功能需要用到中央氣象局的API抓取各地的天氣資訊,因此當大家想使用天氣功能時需要連接網路,以下的步驟將會教會大家如何使用電腦連結ESP32並連結無線網路。 ## 第一步 下載 Arduino IDE 開發環境 ### 1.1 到Arduino官網下載適合自己電腦的IDE [Arduino IDE下載連結](https://support.arduino.cc/hc/en-us/articles/360019833020-Download-and-install-Arduino-IDE) ![screenshot-1770178728804](https://hackmd.io/_uploads/rJ1HKSevZl.png) #### 點選`Download the latest release`,電腦就會自動下載。 --- ### 1.2 新增開發版管理員 安裝完IDE後,到 `file>Preferences`,在 `Addition boards manager URLs` 輸入 `https://espressif.github.io/arduino-esp32/package_esp32_index.json` ![image](https://hackmd.io/_uploads/ry9z5rlP-g.png) --- ### 1.3 下載開發版管理員 到 `Tools>Board>Board manager`,搜尋`esp32`。 ![image](https://hackmd.io/_uploads/SyAhL5xDbe.png) 下載(install) `esp32`,版本選`3.3.6`。 --- ### 1.4 下載程式庫 到 `Tools>Manage Library`,下載以下四個程式庫。 #### 1. `ArduinoJson` 作者:`Benoit Blanchon` 版本:⚠️ 請選 `6.x.x` (絕對不要選 `7.0` 以上) #### 2. `Adafruit SSD1306` 作者:`Adafruit` 版本:最新版本 #### 3. `Adafruit GFX Library` 作者:`Adafruit` 版本:最新版本 #### 4. `WiFiManager` 作者:`tzapu` 版本:最新版本 --- ### 1.5 下載驅動程式 CH340 ### 1.5.1:下載CH340 [如何安裝CH340晶片程式 【2025最新】Windows/Mac/Linux 完整教學](https://www.taiwaniot.com.tw/%E6%8A%80%E8%A1%93%E6%96%87%E4%BB%B6/%E5%A6%82%E4%BD%95%E5%AE%89%E8%A3%9Dch340%E6%99%B6%E7%89%87%E7%A8%8B%E5%BC%8F/?srsltid=AfmBOopWusSHENUYC_jSxe06bNowrjO0zgh1mVG4J9YgCBUyPfS6vVYl) ### 1.5.2:安裝 執行剛下載的 CH341SER.EXE。 會跳出一個藍色介面的視窗,直接點擊最大顆的 `INSTALL` 按鈕。 等待幾秒鐘,出現 `Driver install success!(驅動安裝成功)` 的視窗即完成。 ### 1.5.3 :檢查 使用傳輸線(type-c線,平常充手機的即可)連接ESP32與電腦。 打開 Arduino IDE。 去 `工具 (Tools) > 序列埠 (Port)` 看一下。 如果有看到 COM3、COM4 或其他數字 (通常不是 COM1),就代表成功了! --- ## 第二步 開發板選擇 打開下載的Arduino IDE開發環境。 ### Windows **路徑:** `工具` > `開發板` > `ESP32 Arduino` > `ESP32C3 Dev Module` 1. 點選上方選單的 「工具」 (Tools)。 2. 移動滑鼠到 「開發板」 (Board) 這一欄(注意:這一欄的名稱後面可能會顯示目前已選的板子,例如圖中的 "ESP32C3 Dev Module")。 4. 在跳出的子選單中,找到並指向 「ESP32 Arduino」。 5. 在最後展開的列表中,往下滑找到並點選 「ESP32C3 Dev Module」(如紅色箭頭所示)。 ![截圖 2026-01-25 上午10.29.14](https://hackmd.io/_uploads/rJ8-xZ7LZx.png) ![截圖 2026-01-25 上午10.29.29](https://hackmd.io/_uploads/BkLMlZQ8-e.png) --- ### MacOS **路徑:** `工具` > `開發板` > `esp32` > `ESP32C3 Dev Module` 1. 點選上方選單列的 「工具」。 2. 移動滑鼠到 「開發板」 這一欄(截圖中顯示為 : "ESP32C3 Dev Module" 開發板)。 3. 在展開的選單中,找到 「esp32」 這個分類(請注意,這裡是小寫的 esp32)。 4. 在最右側的列表中,點選 「ESP32C3 Dev Module」。 ![截圖 2026-01-25 上午10.36.32](https://hackmd.io/_uploads/rkp2ZZXIWg.png) --- ## 第三步 貼上程式碼 ```arduino= #include <WiFi.h> #include <Wire.h> #include <WiFiClientSecure.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include "time.h" #include <WiFiManager.h> // 硬體腳位 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_SDA 6 #define OLED_SCL 7 #define BTN_SWITCH 9 #define BTN_HOUR 8 #define BTN_MINUTE 10 #define BTN_ALARM 5 // 最右邊那顆 #define BUZZER_PIN 4 // 使用者設定 () const char* weatherURL = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-C0032-001?Authorization=CWA-E10D7DE6-EDBF-4B49-AA0E-9DB66A749DF9&locationName=%E8%87%BA%E4%B8%AD%E5%B8%82"; const char* CityName = "Taichung"; // 時區設定 const long gmtOffset_sec = 8 * 3600; const int daylightOffset_sec = 0; const char* ntpServer = "time.stdtime.gov.tw"; // 系統變數 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); WiFiManager wm; bool showTime = true; unsigned long lastWeatherUpdate = 0; bool wifiConnected = false; bool alarmEnabled = false; int alarmHour = 7; int alarmMinute = 0; bool buzzing = false; // 按鈕計時變數 unsigned long alarmBtnPressStart = 0; bool isResetting = false; String wxDesc = "Loading.."; String tempStr = "--"; String popStr = "--%"; const int pwmChannel = 0; const int pwmResolution = 8; // 聲音 void playTone(int freq) { #if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3 ledcAttach(BUZZER_PIN, freq, 8); ledcWrite(BUZZER_PIN, 128); #else ledcSetup(pwmChannel, freq, pwmResolution); ledcAttachPin(BUZZER_PIN, pwmChannel); ledcWrite(pwmChannel, 128); #endif } void stopTone() { #if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3 ledcDetach(BUZZER_PIN); digitalWrite(BUZZER_PIN, LOW); #else ledcWrite(pwmChannel, 0); ledcDetachPin(BUZZER_PIN); digitalWrite(BUZZER_PIN, LOW); #endif } // 天氣處理 String normalizeWxASCII(const String& wx) { if (wx.indexOf("雷") >= 0) return "T-Storm"; if (wx.indexOf("雪") >= 0) return "Snow"; if (wx.indexOf("霧") >= 0 || wx.indexOf("霾") >= 0) return "Fog"; if (wx.indexOf("雨") >= 0) return "Rain"; if (wx.indexOf("陰") >= 0) return "Cloudy"; if (wx.indexOf("多雲") >= 0) return "Partly Cloudy"; if (wx.indexOf("晴") >= 0) return "Sunny"; return wx; } void fetchWeather() { if (WiFi.status() != WL_CONNECTED) return; WiFiClientSecure client; client.setInsecure(); HTTPClient http; http.setTimeout(10000); String url = String(weatherURL) + "&elementName=Wx,PoP,MinT,MaxT"; if (!http.begin(client, url)) return; int code = http.GET(); if (code == 200) { String payload = http.getString(); DynamicJsonDocument doc(24576); DeserializationError error = deserializeJson(doc, payload); if (!error) { if (doc["success"] == "true") { JsonObject locObj = doc["records"]["location"][0]; JsonArray elements = locObj["weatherElement"]; String myWx = "--"; String myPoP = "--"; String myMinT = "--"; String myMaxT = "--"; for (JsonObject v : elements) { String name = v["elementName"].as<String>(); if (name == "Wx") myWx = v["time"][0]["parameter"]["parameterName"].as<String>(); if (name == "PoP") myPoP = v["time"][0]["parameter"]["parameterName"].as<String>(); if (name == "MinT") myMinT = v["time"][0]["parameter"]["parameterName"].as<String>(); if (name == "MaxT") myMaxT = v["time"][0]["parameter"]["parameterName"].as<String>(); } wxDesc = normalizeWxASCII(myWx); tempStr = myMinT + "~" + myMaxT; popStr = myPoP + "%"; } } } http.end(); } // 畫面顯示 void drawTimeScreen() { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); struct tm t; if (getLocalTime(&t)) { char dateStr[20]; strftime(dateStr, sizeof(dateStr), "%b %d %a", &t); char timeStr[10]; sprintf(timeStr, "%02d:%02d", t.tm_hour, t.tm_min); char secStr[5]; sprintf(secStr, ":%02d", t.tm_sec); display.setTextSize(1); display.setCursor(0, 0); display.print(dateStr); display.setTextSize(2); display.setCursor(18, 23); display.print(timeStr); display.print(secStr); display.setTextSize(1); display.drawLine(0, 53, 128, 53, SSD1306_WHITE); display.setCursor(0, 55); if (alarmEnabled) display.printf("Alarm: %02d:%02d [ON]", alarmHour, alarmMinute); else display.print("Alarm: OFF"); } else { display.setCursor(10, 25); display.setTextSize(1); display.print("Syncing Time..."); } display.display(); } void drawWeatherScreen() { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1); display.setCursor(0, 0); display.printf("Loc: %s", CityName); display.setTextSize(2); display.setCursor(30, 30); display.printf("%sC", tempStr.c_str()); display.setTextSize(1); display.setCursor(0, 13); display.printf("Rain:%s", popStr.c_str()); display.setTextSize(1); display.setCursor(0, 55); display.print(wxDesc); display.display(); } // ========================================== // 按鈕邏輯 // ========================================== void handleButtons() { if (digitalRead(BTN_ALARM) == LOW) { if (alarmBtnPressStart == 0) { alarmBtnPressStart = millis(); } // 長按 3 秒 -> Restarting... -> 重開機 if (millis() - alarmBtnPressStart > 3000 && !isResetting) { isResetting = true; stopTone(); display.clearDisplay(); display.setTextSize(2); display.setCursor(0, 20); display.println("Restarting..."); display.display(); delay(2000); ESP.restart(); } } else { if (alarmBtnPressStart > 0) { if (!isResetting) { unsigned long duration = millis() - alarmBtnPressStart; if (duration < 1000) { if (buzzing) { stopTone(); buzzing = false; alarmEnabled = false; } else { alarmEnabled = !alarmEnabled; } } } alarmBtnPressStart = 0; } isResetting = false; } if (digitalRead(BTN_SWITCH) == LOW) { if (buzzing) { stopTone(); buzzing = false; alarmEnabled = false; delay(500); } else { showTime = !showTime; delay(250); } } if (!buzzing) { if (digitalRead(BTN_HOUR) == LOW) { alarmHour = (alarmHour + 1) % 24; delay(200); } if (digitalRead(BTN_MINUTE) == LOW) { alarmMinute = (alarmMinute + 1) % 60; delay(200); } } } void checkAlarm() { if (!alarmEnabled || buzzing) return; struct tm t; if (!getLocalTime(&t)) return; if (t.tm_hour == alarmHour && t.tm_min == alarmMinute && t.tm_sec == 0) { buzzing = true; } } void playAlarmSound() { if (buzzing) { unsigned long m = millis(); if (m % 1000 < 100) playTone(2000); else if (m % 1000 < 200) stopTone(); else if (m % 1000 < 300) playTone(2000); else stopTone(); } } // SETUP void setup() { delay(2000); Serial.begin(115200); Wire.begin(OLED_SDA, OLED_SCL); if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F("SSD1306 failed")); for(;;); } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); pinMode(BTN_SWITCH, INPUT_PULLUP); pinMode(BTN_HOUR, INPUT_PULLUP); pinMode(BTN_MINUTE, INPUT_PULLUP); pinMode(BTN_ALARM, INPUT_PULLUP); pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); // 開機救援:按住右鍵上電 -> 清除 WiFi if (digitalRead(BTN_ALARM) == LOW) { display.clearDisplay(); display.setCursor(0, 20); display.setTextSize(2); display.println("Resetting"); display.println("WiFi..."); display.display(); WiFiManager wm_reset; wm_reset.resetSettings(); delay(2000); ESP.restart(); } wm.setAPCallback([](WiFiManager *myWiFiManager) { display.clearDisplay(); display.setCursor(0, 0); display.setTextSize(1); display.println("Connect WiFi to:"); display.setCursor(10,20); display.setTextSize(2); display.println("ESP32_593"); display.setTextSize(1); display.setCursor(0, 47); display.println("Pwd: 12345678"); display.println("IP: 192.168.4.1"); display.display(); }); // 如果 10 秒沒連上,就會放棄並開啟開始畫面 wm.setConnectTimeout(10); wm.setConfigPortalTimeout(180); display.clearDisplay(); display.setCursor(0, 20); display.println("Connecting..."); display.display(); // 開始連線 (10秒限制) bool res = wm.autoConnect("ESP32_593", "12345678"); if(!res) { display.clearDisplay(); display.setCursor(0, 20); display.println("WiFi Failed"); display.println("Restarting..."); display.display(); delay(3000); ESP.restart(); } wifiConnected = true; display.clearDisplay(); display.setCursor(7, 27); display.setTextSize(2); display.println("Connected!"); display.display(); delay(1000); // 對時 display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 20); display.println("Syncing Time..."); display.display(); configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); struct tm t; int retry = 0; while (!getLocalTime(&t) && retry < 40) { Serial.print("."); delay(500); retry++; } if (retry >= 40) { display.clearDisplay(); display.setCursor(0, 20); display.println("Time Sync Failed"); display.display(); delay(2000); } else { Serial.println("\nTime synced!"); } fetchWeather(); lastWeatherUpdate = millis(); } void loop() { if (millis() - lastWeatherUpdate >= 600000UL) { fetchWeather(); lastWeatherUpdate = millis(); } handleButtons(); checkAlarm(); playAlarmSound(); if (showTime) drawTimeScreen(); else drawWeatherScreen(); delay(50); } ``` --- ## 第四步 燒錄程式至ESP32 C3 supermini中 ![截圖 2026-01-26 上午9.53.03](https://hackmd.io/_uploads/HkpbYBVIbx.png) 使用傳輸線(type-c線,平常充手機的即可)連接ESP32與電腦。 便點擊左上方的`上傳` (`➡️圖案的按鈕`) 將程式碼燒錄製至`esp32 supermini`。 --- ## 第五步 開啟手機的WiFi功能 ### 找到你的ESP32名稱的網路,輸入預設密碼:`12345678` ![image](https://hackmd.io/_uploads/HJH1MiBLWl.png) ![image](https://hackmd.io/_uploads/By4ZziB8Zl.png) --- ### 點選Configure WiFi ![IMG_3682](https://hackmd.io/_uploads/SypsMjS8-g.jpg) ### 如果沒有正常跳轉頁面(常發於Android手機或是你已連接過這塊ESP32) 在瀏覽器輸入 IP位址:`192.168.4.1`,便可以到達這個頁面。 ![image](https://hackmd.io/_uploads/rJQFIjBL-l.png) --- ![IMG_3683](https://hackmd.io/_uploads/r1KGNoBLbe.jpg) ### ⚠️ 如果是要連接手機熱點,要注意下面幾點: 1. 如果是跳轉頁面後才開啟熱點,請按網頁下方的 `Refresh` 2. 使用iPhone手機的人,請幫我在熱點頁面開啟 `最大相容性` 或是 `Maximum Compatibility` ![IMG_3684](https://hackmd.io/_uploads/SJ6cHoSUZe.jpg) --- ### 第六步 頁面跳轉 ![IMG_3686](https://hackmd.io/_uploads/rJ27jjrIbl.jpg) 到這個頁面後,就會跳轉回原本連接WiFi的頁面。此時就會正常連接了。 --- ## 可以自行變動的地方 1. ESP32的名稱以及網路密碼 ```arduino=322 wm.setAPCallback([](WiFiManager *myWiFiManager) { display.clearDisplay(); display.setCursor(0, 0); display.setTextSize(1); display.println("Connect WiFi to:"); display.setCursor(10,20); display.setTextSize(2); display.println("ESP32_173"); display.setTextSize(1); display.setCursor(0, 47); display.println("Pwd: 12345678"); display.println("IP: 192.168.4.1"); display.display(); }); // 如果 10 秒沒連上,就會放棄並開啟開始畫面 wm.setConnectTimeout(10); wm.setConfigPortalTimeout(180); display.clearDisplay(); display.setCursor(0, 20); display.println("Connecting..."); display.display(); // 開始連線 (10秒限制) bool res = wm.autoConnect("ESP32_173", "12345678"); ``` ### 程式中`ESP32_173`可以改成你想要的名稱,`12345678`改成你想要的密碼 --- 2. 鬧鐘顯示的城市 修改程式碼最上方的 `CityName` 與 `weatherURL` 時,請參考下表。 **使用方式**:將表格最後一欄的「亂碼」複製,貼在 `weatherURL` 網址最後面的 `locationName=` 之後。 | 區域 | 縣市名稱 | 程式碼:CityName (英文) | 程式碼:URL 編碼 (locationName) | | :--- | :--- | :--- | :--- | | **北部** | 臺北市 | `"Taipei City"` | `%E8%87%BA%E5%8C%97%E5%B8%82` | | | 新北市 | `"New Taipei"` | `%E6%96%B0%E5%8C%97%E5%B8%82` | | | 基隆市 | `"Keelung"` | `%E5%9F%BA%E9%9A%86%E5%B8%82` | | | 桃園市 | `"Taoyuan"` | `%E6%A1%83%E5%9C%92%E5%B8%82` | | | 新竹市 | `"Hsinchu City"` | `%E6%96%B0%E7%AB%B9%E5%B8%82` | | | 新竹縣 | `"Hsinchu County"` | `%E6%96%B0%E7%AB%B9%E7%B8%A3` | | | 宜蘭縣 | `"Yilan"` | `%E5%AE%9C%E8%98%AD%E7%B8%A3` | | **中部** | 臺中市 | `"Taichung"` | `%E8%87%BA%E4%B8%AD%E5%B8%82` | | | 苗栗縣 | `"Miaoli"` | `%E8%8B%97%E6%A0%97%E7%B8%A3` | | | 彰化縣 | `"Changhua"` | `%E5%BD%B0%E5%8C%96%E7%B8%A3` | | | 南投縣 | `"Nantou"` | `%E5%8D%97%E6%8A%95%E7%B8%A3` | | | 雲林縣 | `"Yunlin"` | `%E9%9B%B2%E6%9E%97%E7%B8%A3` | | **南部** | 臺南市 | `"Tainan"` | `%E8%87%BA%E5%8D%97%E5%B8%82` | | | 高雄市 | `"Kaohsiung"` | `%E9%AB%98%E9%9B%84%E5%B8%82` | | | 嘉義市 | `"Chiayi City"` | `%E5%98%89%E7%BE%A9%E5%B8%82` | | | 嘉義縣 | `"Chiayi County"` | `%E5%98%89%E7%BE%A9%E7%B8%A3` | | | 屏東縣 | `"Pingtung"` | `%E5%B1%8F%E6%9D%B1%E7%B8%A3` | | **東部** | 花蓮縣 | `"Hualien"` | `%E8%8A%B1%E8%93%AE%E7%B8%A3` | | | 臺東縣 | `"Taitung"` | `%E8%87%BA%E6%9D%B1%E7%B8%A3` | | **離島** | 澎湖縣 | `"Penghu"` | `%E6%BE%8E%E6%B9%96%E7%B8%A3` | | | 金門縣 | `"Kinmen"` | `%E9%87%91%E9%96%80%E7%B8%A3` | | | 連江縣 | `"Matsu"` | `%E9%80%A3%E6%B1%9F%E7%B8%A3` | # PCB Layout ![截圖 2026-02-04 下午6.23.33](https://hackmd.io/_uploads/SJzEA5gDbx.png)