---
# System prepended metadata

title: METL自製感測器

---

# METL自製感測器
METL204 Laboratory Real-Time Indoor Sensor Data



<iframe width="300" height="260" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/2942380/charts/1?bgcolor=%23ffffff&color=%23d62020&dynamic=true&results=60&type=line&update=15"></iframe><iframe width="300" height="260" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/2942380/charts/2?bgcolor=%23ffffff&color=%23d62020&dynamic=true&results=60&type=line&update=15"></iframe><iframe width="450" height="260" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/2942380/charts/3?bgcolor=%23ffffff&color=%23d62020&dynamic=true&results=60&type=line&update=15"></iframe><iframe width="450" height="260" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/2942380/charts/4?bgcolor=%23ffffff&color=%23d62020&dynamic=true&results=60&type=line&update=15"></iframe>
<iframe width="450" height="260" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/2942380/charts/5?bgcolor=%23ffffff&color=%23d62020&dynamic=true&results=60&type=line&update=15"></iframe>

## 使用感測器介紹

![image](https://hackmd.io/_uploads/Bk5Wx-Brxl.png)
1. SHT31 溫溼度感測器
Digital temperature and humidity sensor (SHT31,Sensiron AG, Staefa, Switzerland)

3. FS3000 風速計
Wind speed sensor (FS3000 Air Velocity Sensor Module, Renesas Electronics Corporation, Tokyo, Japan)

5. BH1750 光照感測器
Light sensor (BH1750 Digital Ambient Light Sensor, ROHM Semiconductor, Kyoto, Japan)

7. DFRobot_OxygenSensor 氧氣感測器
Oxygen sensor (Gravity: Electrochemical Oxygen / O2 Sensor, DFRobot, Shanghai, China)
9. S8_sensor 二氧化碳濃度感測器
Carbon dioxide sensor (SenseAir S8 CO₂ Sensor, SenseAir AB, Delsbo, Sweden)

```javascript=
#include <Wire.h>
#include "SSD1306Ascii.h"
#include "SSD1306AsciiWire.h"
#include <RTClib.h>
#include <BH1750.h>
#include "SHT31.h"
#include <s8_uart.h>
#include <SparkFun_FS3000_Arduino_Library.h>
#include <SD.h>
#include <SPI.h>
#include "DFRobot_OxygenSensor.h"

// define腳位
#define CO2_VCC 4
#define FAN_VCC 2
#define Sensor_VCC 33
#define Button 12
#define SD_CS 32
#define S8_RX_PIN 16
#define S8_TX_PIN 17
#define RST_PIN -1

// OLED
#define OLED_ADDRESS 0x3C
SSD1306AsciiWire oled;

// 感測資料庫
RTC_DS3231 rtc;
BH1750 lightMeter;
SHT31 sht;
FS3000 fs3000;
HardwareSerial S8_serial(2);
S8_UART s8_sensor(S8_serial);

// define氧氣感測器(後來加入的)
#define Oxygen_IICAddress ADDRESS_3
#define OXYGEN_SAMPLE_COUNT 10
DFRobot_OxygenSensor oxygenSensor;

// SD 卡叫他datafile
File dataFile;

// 時間與模式變數
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 2000;
bool systemPoweredOn = false;
bool passiveMode = false;
unsigned long lastAutoRun = 0;
const unsigned long autoInterval = 300000  ; // 5min測量一次
unsigned long autoStateTime = 0;
int autoState = 0;
bool autoRunning = false;
bool firstLoop = true;
void setup() {
  Serial.begin(115200);
  delay(2000); // 等一下拉

  pinMode(Sensor_VCC, OUTPUT);
  pinMode(FAN_VCC, OUTPUT);
  pinMode(CO2_VCC, OUTPUT);
  pinMode(Button, INPUT_PULLUP);

  digitalWrite(Sensor_VCC, LOW);
  digitalWrite(FAN_VCC, LOW);
  digitalWrite(CO2_VCC, LOW);
  Serial.println("系統啟動，設備關閉...");

  digitalWrite(Sensor_VCC, HIGH);
  Serial.println("Sensor VCC已開啟，準備開始初始化RTC...");

  // --- 加上這兩行！一定要有！！ ---
  Wire.begin();  // 初始化I2C
  if (!rtc.begin()) {
    Serial.println("RTC not found! 停止執行");
    while (1); // RTC沒找到直接卡住
  }
  // ---------------------------------

  Serial.println("RTC初始化完成，開始等待分鐘為5的倍數...");

  // --- 正確的等待5分鐘倍數流程 ---
  //等到5min倍數再進行運作
  while (true) {
    DateTime now = rtc.now();
    Serial.printf("目前時間: %02d:%02d:%02d\n", now.hour(), now.minute(), now.second());

    if (now.minute() % 5 == 0) {
      Serial.printf("現在時間 %02d:%02d，符合5分鐘倍數，開始運行!\n", now.hour(), now.minute());
      break;
    }
    delay(1000); // 每秒檢查一次
  }
}

void initializeDevices() {
  Serial.println("初始化所有設備...");
  Wire.begin();
  Wire.setClock(400000L);

  if (!rtc.begin()) {
    Serial.println("RTC not found!");
  }
  if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  if (!lightMeter.begin()) {
    Serial.println("BH1750 not detected!");
  }

  if (!sht.begin()) {
    Serial.println("SHT31 not detected!");
  }

  if (!fs3000.begin()) {
    Serial.println("FS3000 not detected!");
  }
  fs3000.setRange(AIRFLOW_RANGE_7_MPS);

  if (!oxygenSensor.begin(Oxygen_IICAddress)) {
    Serial.println("O2 Sensor not detected (I2C)");
  }

  S8_serial.begin(9600, SERIAL_8N1, S8_RX_PIN, S8_TX_PIN);

  if (!SD.begin(SD_CS)) {
    Serial.println("SD card initialization failed!");
  } else {
    Serial.println("SD Initialization done.");
  }

#if RST_PIN >= 0
  oled.begin(&Adafruit128x64, OLED_ADDRESS, RST_PIN); ㄎ
#else
  oled.begin(&Adafruit128x64, OLED_ADDRESS);
#endif

  oled.setFont(Adafruit5x7);
  oled.clear();
  oled.println(F("System Init"));
  delay(1000);
  oled.clear();
  Serial.println("所有設備初始化完成!");
}

void restartCO2Sensor() {
  Serial.println("重新啟動 CO2 感測器 UART...");
  S8_serial.end();
  delay(500);
  S8_serial.begin(9600, SERIAL_8N1, S8_RX_PIN, S8_TX_PIN);
  delay(2000);
}

void readAndDisplayData(bool saveToSD = false) {
  DateTime now = rtc.now();
  sht.read();
  float lux = lightMeter.readLightLevel();
  float temperature = sht.getTemperature();
  float humidity = sht.getHumidity();
  float airflow = fs3000.readMetersPerSecond();
  float co2 = s8_sensor.get_co2();
  float o2 = oxygenSensor.getOxygenData(OXYGEN_SAMPLE_COUNT);

  if (co2 <= 0) {
    Serial.println("CO2 數據讀取錯誤，嘗試重新初始化...");
    restartCO2Sensor();
    co2 = s8_sensor.get_co2();
  }

  Serial.printf("Time: %02d:%02d:%02d | Lux: %.2f lx | Temp: %.1f C | Hum: %.0f%% | Wind: %.2f m/s | CO2: %.0f ppm | O2: %.2f %%vol\n",
                now.hour(), now.minute(), now.second(),
                lux, temperature, humidity, airflow, co2, o2);

  if (saveToSD) {
    logData(now, temperature, humidity, lux, airflow, co2, o2);
  }

  oled.clear();
  oled.setCursor(0, 0);
  oled.printf("Time: %02d:%02d:%02d\n", now.hour(), now.minute(), now.second());
  oled.printf("Lux: %.2f lx\n", lux);
  oled.printf("Temp: %.1f C\n", temperature);
  oled.printf("Hum: %.0f %%\n", humidity);
  oled.printf("Wind: %.2f m/s\n", airflow);
  oled.printf("CO2: %.0f ppm\n", co2);
  oled.printf("O2: %.2f %%\n", o2);
}

void logData(DateTime now, float temperature, float humidity, float lux, float airflow, float co2, float o2) {
  dataFile = SD.open("/data.txt", FILE_APPEND);
  if (dataFile) {
    dataFile.printf("%04d/%02d/%02d %02d:%02d:%02d, %.1f, %.1f, %.2f, %.2f, %.0f, %.2f\n",
                    now.year(), now.month(), now.day(),
                    now.hour(), now.minute(), now.second(),
                    temperature, humidity, lux, airflow, co2, o2);
    dataFile.flush();
    dataFile.close();
    Serial.println("Logged Data");
  } else {
    Serial.println("Error opening data file");
  }
}

void loop() {
  if (firstLoop) {
    lastAutoRun = millis();  // 這邊才正確設定！
    firstLoop = false;
  }
  // 被動模式：按鈕按下
  if (digitalRead(Button) == LOW) {
    if (!passiveMode) {
      Serial.println("進入被動模式");
      passiveMode = true;
      systemPoweredOn = true;
      digitalWrite(Sensor_VCC, HIGH);
      digitalWrite(FAN_VCC, HIGH);
      digitalWrite(CO2_VCC, HIGH);
      delay(500);
      initializeDevices();
      restartCO2Sensor();
    }

    if (millis() - lastUpdate >= updateInterval) {
      lastUpdate = millis();
      readAndDisplayData(false);  // 顯示不儲存
    }

  } else {
    if (passiveMode) {
      Serial.println("離開被動模式，關閉設備");
      passiveMode = false;
      systemPoweredOn = false;
      digitalWrite(Sensor_VCC, LOW);
      digitalWrite(FAN_VCC, LOW);
      digitalWrite(CO2_VCC, LOW);
    }
  }

  // 主動模式：每小時執行一次
  if (millis() - lastAutoRun >= autoInterval && !autoRunning && !passiveMode) {
    autoRunning = true;
    autoState = 1;
    autoStateTime = millis();
    lastAutoRun = millis();
    Serial.println("啟動主動模式流程");
  }

  // 主動模式狀態流程
  if (autoRunning) {
    unsigned long now = millis();

    switch (autoState) {
      case 1:
        Serial.println("主動模式：開啟設備");
        digitalWrite(Sensor_VCC, HIGH);
        digitalWrite(FAN_VCC, HIGH);
        digitalWrite(CO2_VCC, HIGH);
        delay(500);
        initializeDevices();
        restartCO2Sensor();
        autoStateTime = now;
        autoState = 2;
        break;

      case 2:
        if (now - autoStateTime >= 10000) {
          Serial.println("主動模式：關閉風扇");
          digitalWrite(FAN_VCC, LOW);
          autoStateTime = now;
          autoState = 3;
        }
        break;

      case 3:
        if (now - autoStateTime >= 5000) {
          Serial.println("主動模式：00紀錄資料");
          oled.clear();
          oled.setCursor(0, 0);
          oled.println(F("Logging data..."));
          delay(1000);
          readAndDisplayData(true);
          autoStateTime = now;
          autoState = 4;
        }
        break;

      case 4:
        if (now - autoStateTime >= 1000) {
          Serial.println("主動模式：關閉電源");
          digitalWrite(Sensor_VCC, LOW);
          digitalWrite(FAN_VCC, LOW);
          digitalWrite(CO2_VCC, LOW);
          autoRunning = false;
          autoState = 0;
        }
        break;
    }
  }

  delay(100);
} 
```
## 實際工作電路
本研究使用固緯電子的可程式單通道電子負載，測量實際消耗電流。Electronic Load(PEL-3041 Programmable DC Electronic Load,  GW Instek,  New Taipei, Taiwan)
![O1CN01FunLzu1lZ6KJXAwGP_!!2-item_pic.png_](https://hackmd.io/_uploads/HyMLldjBxe.jpg)

![image](https://hackmd.io/_uploads/ryu8RDiSle.png)

## TCA9548A協定multi-I2C Address
具備連接wifi、Dashboard串接顯示
```javascripts=
#include <Wire.h>
#include "SSD1306Ascii.h"
#include "SSD1306AsciiWire.h"
#include <RTClib.h>
#include <BH1750.h>
#include "SHT31.h"
#include <s8_uart.h>
#include <SparkFun_FS3000_Arduino_Library.h>
#include <SD.h>
#include <SPI.h>
#include "DFRobot_OxygenSensor.h"
#include <WiFi.h>
#include <HTTPClient.h>

// ======================= WiFi 與 ThingSpeak =======================
const char* ssid = "metl204";
const char* password = "33669821";
WiFiClient client;
unsigned long myChannelNumber = 2942378;
const char* myWriteAPIKey = "Q8NPUWL5ACGHKG3N";

// ======================= 硬體腳位 =======================
#define CO2_VCC     4
#define FAN_VCC     2
#define Sensor_VCC 33
#define Button     12
#define SD_CS      32
#define S8_RX_PIN  16
#define S8_TX_PIN  17
#define RST_PIN   -1

// ======================= TCA9548A 設定 =======================
#define TCA_ADDR       0x70
#define TCA_CH_COMMON  0   // OLED、BH1750、SHT31、O2、RTC 等
#define TCA_CH_FS1     6   // 第一顆 FS3000
#define TCA_CH_FS2     7   // 第二顆 FS3000
static inline void tcaSelect(uint8_t ch) {
  Wire.beginTransmission(TCA_ADDR);
  Wire.write(1 << ch);
  Wire.endTransmission();
  delayMicroseconds(500);
}

// ======================= OLED =======================
#define OLED_ADDRESS 0x3C
SSD1306AsciiWire oled;

// ======================= 感測器物件 =======================
RTC_DS3231 rtc;
BH1750 lightMeter;
SHT31 sht;
FS3000 fs3000_1;  // FS3000 #1 on TCA_CH_FS1
FS3000 fs3000_2;  // FS3000 #2 on TCA_CH_FS2
HardwareSerial S8_serial(2);
S8_UART s8_sensor(S8_serial);
#define Oxygen_IICAddress ADDRESS_3
#define OXYGEN_SAMPLE_COUNT 10
DFRobot_OxygenSensor oxygenSensor;

// ======================= SD =======================
File dataFile;
bool sd_ok = false;
const char* LOG_PATH = "/data.txt";

// ======================= 狀態變數 =======================
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 20000;   // 20s
bool systemPoweredOn = false;
bool passiveMode = false;
unsigned long lastAutoRun = 0;
const unsigned long autoInterval = 300000;    // 5min
unsigned long autoStateTime = 0;
int autoState = 0;
bool autoRunning = false;
bool firstLoop = true;

// ======================= 函式宣告 =======================
void initializeDevices();
void restartCO2Sensor();
void readAndDisplayData(bool saveToSD=false);
void logData(DateTime now, float temperature, float humidity, float lux,
             float airflow1, float airflow2, float co2, float o2);
bool ensureSDMounted();
void uploadToThingSpeak(float temperature, float humidity, float lux,
                        float airflow1, float airflow2, float co2, float o2);

// ======================= WiFi/ThingSpeak 上傳 =======================
void uploadToThingSpeak(float temperature, float humidity, float lux,
                        float airflow1, float airflow2, float co2, float o2) {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    String url = "http://api.thingspeak.com/update?api_key=" + String(myWriteAPIKey) +
                 "&field1=" + String(temperature, 1) +
                 "&field2=" + String(humidity, 1) +
                 "&field3=" + String(lux, 2) +
                 "&field4=" + String(airflow1, 2) +
                 "&field5=" + String(co2, 0) +
                 "&field6=" + String(o2, 2) +
                 "&field7=" + String(airflow2, 2);
    http.begin(url);
    int httpCode = http.GET();
    if (httpCode > 0) {
      Serial.printf("ThingSpeak 回應：%d\n", httpCode);
    } else {
      Serial.println("上傳至 ThingSpeak 失敗");
    }
    http.end();
  } else {
    Serial.println("WiFi 未連線，無法上傳至 ThingSpeak");
  }
}

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

  Serial.println("連接 WiFi...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi 連線成功!");

  pinMode(Sensor_VCC, OUTPUT);
  pinMode(FAN_VCC, OUTPUT);
  pinMode(CO2_VCC, OUTPUT);
  pinMode(Button, INPUT_PULLUP);

  digitalWrite(Sensor_VCC, LOW);
  digitalWrite(FAN_VCC, LOW);
  digitalWrite(CO2_VCC, LOW);

  digitalWrite(Sensor_VCC, HIGH);

  Wire.begin();

  // RTC 初始化在公共通道
  tcaSelect(TCA_CH_COMMON);
  if (!rtc.begin()) {
    Serial.println("RTC not found! 停止執行");
    while (1);
  }
  if (rtc.lostPower()) {
    Serial.println("RTC lost power, set compile time.");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
  Serial.println("RTC初始化完成，開始等待分鐘為5的倍數...");

  while (true) {
    tcaSelect(TCA_CH_COMMON);
    DateTime now = rtc.now();
    Serial.printf("目前時間: %02d:%02d:%02d\n", now.hour(), now.minute(), now.second());
    if (now.minute() % 5 == 0) {
      Serial.printf("現在時間 %02d:%02d，符合5分鐘倍數，開始運行!\n", now.hour(), now.minute());
      break;
    }
    delay(1000);
  }
}

// ======================= 初始化所有設備（含 TCA/SD） =======================
void initializeDevices() {
  Serial.println("初始化所有設備...");
  Wire.begin();
  Wire.setClock(400000L);

  // 公共通道裝置
  tcaSelect(TCA_CH_COMMON);

  if (!rtc.begin()) {
    Serial.println("RTC not found!");
  }
  if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
    Serial.println("BH1750 not detected!");
  }

  if (!sht.begin()) {
    Serial.println("SHT31 not detected!");
  }

  if (!oxygenSensor.begin(Oxygen_IICAddress)) {
    Serial.println("O2 Sensor not detected (I2C)");
  }

  // FS3000 #1
  tcaSelect(TCA_CH_FS1);
  if (!fs3000_1.begin()) {
    Serial.println("FS3000 #1 not detected!");
  } else {
    fs3000_1.setRange(AIRFLOW_RANGE_7_MPS);
  }

  // FS3000 #2
  tcaSelect(TCA_CH_FS2);
  if (!fs3000_2.begin()) {
    Serial.println("FS3000 #2 not detected!");
  } else {
    fs3000_2.setRange(AIRFLOW_RANGE_7_MPS);
  }

  // UART CO2
  S8_serial.begin(9600, SERIAL_8N1, S8_RX_PIN, S8_TX_PIN);

  // SD：掛載 + 建檔(含表頭)
  SPI.begin();                  // 預設 VSPI 腳位
  sd_ok = SD.begin(SD_CS);
  if (!sd_ok) {
    Serial.println("SD card initialization failed!（檢查接線/CS/供電）");
  } else {
    Serial.println("SD Initialization done.");
    if (!SD.exists(LOG_PATH)) {
      File f = SD.open(LOG_PATH, FILE_WRITE);
      if (f) {
        f.println("DateTime, Temp(C), RH(%), Lux, Wind1(m/s), Wind2(m/s), CO2(ppm), O2(%)");
        f.close();
        Serial.println("Created data.txt with header.");
      } else {
        Serial.println("Failed to create data.txt");
      }
    }
  }

  // OLED
  tcaSelect(TCA_CH_COMMON);
#if RST_PIN >= 0
  oled.begin(&Adafruit128x64, OLED_ADDRESS, RST_PIN);
#else
  oled.begin(&Adafruit128x64, OLED_ADDRESS);
#endif
  oled.setFont(Adafruit5x7);
  oled.clear();
  oled.println(F("System Init"));
  delay(800);
  oled.clear();

  Serial.println("所有設備初始化完成!");
}

void restartCO2Sensor() {
  Serial.println("重新啟動 CO2 感測器 UART...");
  S8_serial.end();
  delay(500);
  S8_serial.begin(9600, SERIAL_8N1, S8_RX_PIN, S8_TX_PIN);
  delay(2000);
}

// ======================= SD 確保掛載 =======================
bool ensureSDMounted() {
  if (sd_ok) return true;
  Serial.println("Re-trying SD.begin()...");
  SPI.begin();
  sd_ok = SD.begin(SD_CS);
  if (!sd_ok) Serial.println("SD still not ready.");
  return sd_ok;
}

// ======================= 讀取與顯示（含兩顆 FS3000） =======================
void readAndDisplayData(bool saveToSD) {
  // 公共通道：時間、溫濕度、光照、O2
  tcaSelect(TCA_CH_COMMON);
  DateTime now = rtc.now();

  sht.read();
  float lux = lightMeter.readLightLevel();
  float temperature = sht.getTemperature();
  float humidity = sht.getHumidity();

  // FS3000 #1
  tcaSelect(TCA_CH_FS1);
  float airflow1 = fs3000_1.readMetersPerSecond();

  // FS3000 #2
  tcaSelect(TCA_CH_FS2);
  float airflow2 = fs3000_2.readMetersPerSecond();

  // CO2 (UART)
  float co2 = s8_sensor.get_co2();
  if (co2 <= 0) {
    Serial.println("CO2 數據讀取錯誤，嘗試重新初始化...");
    restartCO2Sensor();
    co2 = s8_sensor.get_co2();
  }

  // O2
  tcaSelect(TCA_CH_COMMON);
  float o2 = oxygenSensor.getOxygenData(OXYGEN_SAMPLE_COUNT);

  Serial.printf("Time %02d:%02d:%02d | Lux %.2f lx | T %.1f C | RH %.0f %% | W1 %.2f m/s | W2 %.2f m/s | CO2 %.0f ppm | O2 %.2f %%\n",
                now.hour(), now.minute(), now.second(),
                lux, temperature, humidity, airflow1, airflow2, co2, o2);

  if (saveToSD) {
    logData(now, temperature, humidity, lux, airflow1, airflow2, co2, o2);
    uploadToThingSpeak(temperature, humidity, lux, airflow1, airflow2, co2, o2);
  }

  // OLED 顯示
  tcaSelect(TCA_CH_COMMON);
  oled.clear();
  oled.setCursor(0, 0);
  oled.printf("T:%02d:%02d:%02d\n", now.hour(), now.minute(), now.second());
  oled.printf("Lux: %.2f lx\n", lux);
  oled.printf("T/H: %.1fC %.0f%%\n", temperature, humidity);
  oled.printf("W1: %.2f  W2: %.2f\n", airflow1, airflow2);
  oled.printf("CO2: %.0f O2: %.2f\n", co2, o2);
}

// ======================= SD 紀錄 =======================



void logData(DateTime now, float temperature, float humidity, float lux,
             float airflow1, float airflow2, float co2, float o2) {
  if (!ensureSDMounted()) {
    Serial.println("Skip logging: SD not mounted.");
    return;
  }

  if (!SD.exists(LOG_PATH)) {
    File f0 = SD.open(LOG_PATH, FILE_WRITE);
    if (f0) {
      f0.println("DateTime, Temp(C), RH(%), Lux, Wind1(m/s), Wind2(m/s), CO2(ppm), O2(%)");
      f0.close();
    } else {
      Serial.println("Cannot create log file.");
      return;
    }
  }

  File f = SD.open(LOG_PATH, FILE_APPEND);
  if (f) {
    f.printf("%04d/%02d/%02d %02d:%02d:%02d, %.1f, %.1f, %.2f, %.2f, %.2f, %.0f, %.2f\r\n",
             now.year(), now.month(), now.day(),
             now.hour(), now.minute(), now.second(),
             temperature, humidity, lux, airflow1, airflow2, co2, o2);
    f.flush();
    f.close();
    Serial.println("Logged Data -> /data.txt");
  } else {
    Serial.println("Error opening data file (append).");
    File f2 = SD.open(LOG_PATH, FILE_WRITE);
    if (f2) {
      f2.printf("%04d/%02d/%02d %02d:%02d:%02d, %.1f, %.1f, %.2f, %.2f, %.2f, %.0f, %.2f\r\n",
                now.year(), now.month(), now.day(),
                now.hour(), now.minute(), now.second(),
                temperature, humidity, lux, airflow1, airflow2, co2, o2);
      f2.close();
      Serial.println("Append failed; WRITE fallback succeeded.");
    } else {
      Serial.println("WRITE fallback also failed.");
    }
  }
}

// ======================= 主迴圈 =======================
void loop() {
  if (firstLoop) {
    lastAutoRun = millis();
    firstLoop = false;
  }

  // 被動模式：按鍵按下即時顯示（不存檔）
  if (digitalRead(Button) == LOW) {
    if (!passiveMode) {
      Serial.println("進入被動模式");
      passiveMode = true;
      systemPoweredOn = true;
      digitalWrite(Sensor_VCC, HIGH);
      digitalWrite(FAN_VCC, HIGH);
      digitalWrite(CO2_VCC, HIGH);
      delay(500);
      initializeDevices();
      restartCO2Sensor();
    }

    if (millis() - lastUpdate >= updateInterval) {
      lastUpdate = millis();
      readAndDisplayData(false);
    }

  } else {
    if (passiveMode) {
      Serial.println("離開被動模式，關閉設備");
      passiveMode = false;
      systemPoweredOn = false;
      digitalWrite(Sensor_VCC, LOW);
      digitalWrite(FAN_VCC, LOW);
      digitalWrite(CO2_VCC, LOW);
    }
  }

  // 主動模式：每 5 分鐘自動量測與上傳
  if (millis() - lastAutoRun >= autoInterval && !autoRunning && !passiveMode) {
    autoRunning = true;
    autoState = 1;
    autoStateTime = millis();
    lastAutoRun = millis();
    Serial.println("啟動主動模式流程");
  }

  if (autoRunning) {
    unsigned long nowms = millis();

    switch (autoState) {
      case 1:
        Serial.println("主動模式：開啟設備");
        digitalWrite(Sensor_VCC, HIGH);
        digitalWrite(FAN_VCC, HIGH);
        digitalWrite(CO2_VCC, HIGH);
        delay(500);
        initializeDevices();
        restartCO2Sensor();
        autoStateTime = nowms;
        autoState = 2;
        break;

      case 2:
        if (nowms - autoStateTime >= 20000) {
          Serial.println("主動模式：關閉風扇");
          digitalWrite(FAN_VCC, LOW);
          autoStateTime = nowms;
          autoState = 3;
        }
        break;

      case 3:
        if (nowms - autoStateTime >= 5000) {
          Serial.println("主動模式：紀錄資料");
          tcaSelect(TCA_CH_COMMON);
          oled.clear();
          oled.setCursor(0, 0);
          oled.println(F("Logging data..."));
          delay(1000);
          readAndDisplayData(true); // 會觸發上傳
          autoStateTime = nowms;
          autoState = 4;
        }
        break;

      case 4:
        if (nowms - autoStateTime >= 1000) {
          Serial.println("主動模式：關閉電源");
          digitalWrite(Sensor_VCC, LOW);
          digitalWrite(FAN_VCC, LOW);
          digitalWrite(CO2_VCC, LOW);
          autoRunning = false;
          autoState = 0;
        }
        break;
    }
  }

  delay(100);
}
```

