---
# System prepended metadata

title: 2026陽明交大電機系一日營硬體維護手冊
tags: [一日營]

---

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