# 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)

#### 點選`Download the latest release`,電腦就會自動下載。
---
### 1.2 新增開發版管理員
安裝完IDE後,到 `file>Preferences`,在 `Addition boards manager URLs` 輸入 `https://espressif.github.io/arduino-esp32/package_esp32_index.json`

---
### 1.3 下載開發版管理員
到 `Tools>Board>Board manager`,搜尋`esp32`。

下載(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」(如紅色箭頭所示)。


---
### MacOS
**路徑:** `工具` > `開發板` > `esp32` > `ESP32C3 Dev Module`
1. 點選上方選單列的 「工具」。
2. 移動滑鼠到 「開發板」 這一欄(截圖中顯示為 : "ESP32C3 Dev Module" 開發板)。
3. 在展開的選單中,找到 「esp32」 這個分類(請注意,這裡是小寫的 esp32)。
4. 在最右側的列表中,點選 「ESP32C3 Dev Module」。

---
## 第三步 貼上程式碼
```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中

使用傳輸線(type-c線,平常充手機的即可)連接ESP32與電腦。
便點擊左上方的`上傳` (`➡️圖案的按鈕`) 將程式碼燒錄製至`esp32 supermini`。
---
## 第五步 開啟手機的WiFi功能
### 找到你的ESP32名稱的網路,輸入預設密碼:`12345678`


---
### 點選Configure WiFi

### 如果沒有正常跳轉頁面(常發於Android手機或是你已連接過這塊ESP32)
在瀏覽器輸入 IP位址:`192.168.4.1`,便可以到達這個頁面。

---

### ⚠️ 如果是要連接手機熱點,要注意下面幾點:
1. 如果是跳轉頁面後才開啟熱點,請按網頁下方的 `Refresh`
2. 使用iPhone手機的人,請幫我在熱點頁面開啟 `最大相容性` 或是 `Maximum Compatibility`

---
### 第六步 頁面跳轉

到這個頁面後,就會跳轉回原本連接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
