1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 | #include <TFT_eSPI.h> #include <Button2.h> #include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <Wire.h> #include <Adafruit_AHTX0.h> // AHT20 驅動庫 #include <time.h> // WiFi 設定 (請修改為你的網路) const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASSWORD"; // OpenWeatherMap API 設定 (如果不需要天氣功能,可以留空) const char* weatherApiKey = ""; // 請至 openweathermap.org 申請,或留空停用天氣功能 const char* city = "Hsinchu,TW"; // 硬體腳位定義 #define MQ2_PIN 12 // 煙霧感測器 (3.3V) #define MQ135_PIN 27 // 空氣品質感測器 (5V) // AHT20 I2C 腳位 (TTGO T-Display 預設) #define SDA_PIN 21 #define SCL_PIN 22 // RGB LED 設定 #define RED_PIN 15 #define GREEN_PIN 2 #define BLUE_PIN 13 #define PWM_FREQ 5000 #define PWM_RES 8 #define RED_CH 0 #define GREEN_CH 1 #define BLUE_CH 2 // 按鍵設定 #define BUTTON_A_PIN 0 #define BUTTON_B_PIN 35 // 自定義顏色 #define TFT_LIGHTGREEN 0x9772 #define TFT_ORANGE 0xFD20 // 全域物件 TFT_eSPI tft = TFT_eSPI(); Button2 buttonA = Button2(BUTTON_A_PIN); Button2 buttonB = Button2(BUTTON_B_PIN); Adafruit_AHTX0 aht; // AHT20 溫濕度感測器 // 顯示頁面枚舉 enum DisplayPage { PAGE_OVERVIEW = 0, // 總覽頁面 PAGE_AIR_DETAIL, // 空氣品質詳細 PAGE_CLIMATE, // 溫濕度詳細 PAGE_WEATHER, // 時間天氣 PAGE_SYSTEM, // 系統狀態 PAGE_COUNT // 總頁數 }; // 全域變數 DisplayPage currentPage = PAGE_OVERVIEW; unsigned long lastSensorUpdate = 0; unsigned long lastWeatherUpdate = 0; unsigned long lastTimeSync = 0; const unsigned long sensorInterval = 2000; // 感測器更新間隔 const unsigned long weatherInterval = 600000; // 天氣更新間隔 (10分鐘) const unsigned long timeSyncInterval = 3600000; // 時間同步間隔 (1小時) // 感測器數據 struct SensorData { float temperature = 0.0; float humidity = 0.0; int mq2_raw = 0; int mq135_raw = 0; float mq2_ppm = 0.0; float mq135_ppm = 0.0; int airQualityIndex = 0; String airQualityLevel = "良好"; bool ahtValid = false; } sensorData; // 天氣數據 struct WeatherData { String description = "載入中..."; float temp = 0.0; int humidity = 0; float pressure = 0.0; float windSpeed = 0.0; String icon = "01d"; bool valid = false; } weatherData; // WiFi 和系統狀態 bool wifiConnected = false; bool ntpSynced = false; bool weatherEnabled = false; String currentTime = "00:00:00"; String currentDate = "2025-01-01"; void setup() { Serial.begin(115200); Serial.println("=== 智慧空氣品質監控系統啟動 ==="); // 檢查天氣功能 weatherEnabled = (strlen(weatherApiKey) > 0); if (!weatherEnabled) { Serial.println("天氣功能已停用 (無 API Key)"); } // 初始化硬體 initDisplay(); initRGBLED(); initSensors(); initButtons(); // 顯示啟動畫面 showBootScreen(); // 連接 WiFi connectWiFi(); // 同步時間 if (wifiConnected) { syncNTPTime(); // 獲取初始天氣 (如果啟用) if (weatherEnabled) { updateWeather(); } } Serial.println("系統初始化完成"); } void loop() { // 處理按鍵 buttonA.loop(); buttonB.loop(); // 更新感測器數據 if (millis() - lastSensorUpdate >= sensorInterval) { updateSensorData(); updateDisplay(); updateLEDAlert(); lastSensorUpdate = millis(); } // 更新天氣資訊 if (millis() - lastWeatherUpdate >= weatherInterval && wifiConnected && weatherEnabled) { updateWeather(); lastWeatherUpdate = millis(); } // 定期同步時間 if (millis() - lastTimeSync >= timeSyncInterval && wifiConnected) { syncNTPTime(); lastTimeSync = millis(); } // 更新時間顯示 updateTimeDisplay(); delay(100); } void initDisplay() { tft.init(); tft.setRotation(1); // 橫向顯示 tft.fillScreen(TFT_BLACK); Serial.println("TFT 螢幕初始化完成"); } void initRGBLED() { ledcSetup(RED_CH, PWM_FREQ, PWM_RES); ledcSetup(GREEN_CH, PWM_FREQ, PWM_RES); ledcSetup(BLUE_CH, PWM_FREQ, PWM_RES); ledcAttachPin(RED_PIN, RED_CH); ledcAttachPin(GREEN_PIN, GREEN_CH); ledcAttachPin(BLUE_PIN, BLUE_CH); setRGBColor(0, 0, 255); // 開機藍色 Serial.println("RGB LED 初始化完成"); } void initSensors() { // 初始化 I2C Wire.begin(SDA_PIN, SCL_PIN); Serial.println("I2C 初始化完成"); // 初始化 AHT20 if (!aht.begin()) { Serial.println("❌ AHT20 初始化失敗!"); sensorData.ahtValid = false; } else { Serial.println("✅ AHT20 初始化成功"); sensorData.ahtValid = true; } // 氣體感測器預熱 Serial.println("氣體感測器預熱中..."); delay(2000); Serial.println("感測器初始化完成"); // 測試 AHT20 testAHT20(); } void testAHT20() { if (!sensorData.ahtValid) return; Serial.println("測試 AHT20 感測器..."); delay(1000); // AHT20 需要短暫啟動時間 sensors_event_t humidity, temp; aht.getEvent(&humidity, &temp); float testTemp = temp.temperature; float testHum = humidity.relative_humidity; if (isnan(testTemp) || isnan(testHum)) { Serial.println("❌ AHT20 感測器讀取異常!"); Serial.println("請檢查:"); Serial.println("1. VCC 接到 3.3V"); Serial.println("2. SDA 接到 GPIO 21"); Serial.println("3. SCL 接到 GPIO 22"); Serial.println("4. GND 連接"); sensorData.ahtValid = false; } else { Serial.printf("✅ AHT20 感測器正常!溫度: %.1f°C, 濕度: %.1f%%\n", testTemp, testHum); } } void initButtons() { buttonA.setPressedHandler([](Button2& btn) { currentPage = (DisplayPage)((currentPage + 1) % PAGE_COUNT); updateDisplay(); buttonFeedback(); Serial.printf("切換到頁面 %d\n", currentPage); }); buttonB.setPressedHandler([](Button2& btn) { // 強制更新所有數據 updateSensorData(); if (wifiConnected) { if (weatherEnabled) { updateWeather(); } syncNTPTime(); } updateDisplay(); buttonFeedback(); Serial.println("手動更新完成"); }); Serial.println("按鍵初始化完成"); } void showBootScreen() { tft.fillScreen(TFT_BLACK); // 標題 tft.setTextColor(TFT_CYAN); tft.setTextSize(2); tft.setCursor(20, 10); tft.println("智慧空氣監控"); // 圖標區域 (簡單的空氣品質圖標) drawAirQualityIcon(90, 35, TFT_GREEN); // 功能列表 tft.setTextColor(TFT_YELLOW); tft.setTextSize(1); tft.setCursor(10, 70); tft.println("• MQ-2 煙霧偵測 (GPIO12)"); tft.setCursor(10, 85); tft.println("• MQ-135 空氣品質 (GPIO27)"); tft.setCursor(10, 100); tft.println("• AHT20 溫濕度 (I2C)"); tft.setCursor(10, 115); if (weatherEnabled) { tft.println("• WiFi 天氣時間"); } else { tft.println("• WiFi 時間同步"); } delay(3000); } void connectWiFi() { tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE); tft.setTextSize(1); tft.setCursor(10, 40); tft.println("連接 WiFi..."); WiFi.begin(ssid, password); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); Serial.print("."); tft.print("."); attempts++; } if (WiFi.status() == WL_CONNECTED) { wifiConnected = true; Serial.println("\nWiFi 連接成功"); Serial.printf("IP: %s\n", WiFi.localIP().toString().c_str()); tft.setCursor(10, 60); tft.setTextColor(TFT_GREEN); tft.println("WiFi 連接成功!"); tft.setCursor(10, 75); tft.printf("IP: %s", WiFi.localIP().toString().c_str()); setRGBColor(0, 255, 0); // 成功綠色 } else { wifiConnected = false; Serial.println("\nWiFi 連接失敗"); tft.setCursor(10, 60); tft.setTextColor(TFT_RED); tft.println("WiFi 連接失敗"); tft.setCursor(10, 75); tft.println("將以離線模式運行"); setRGBColor(255, 255, 0); // 警告黃色 } delay(2000); } void syncNTPTime() { if (!wifiConnected) return; configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov"); Serial.println("同步 NTP 時間..."); struct tm timeinfo; if (getLocalTime(&timeinfo)) { ntpSynced = true; Serial.println("NTP 時間同步成功"); } else { ntpSynced = false; Serial.println("NTP 時間同步失敗"); } } void updateTimeDisplay() { if (!ntpSynced) return; struct tm timeinfo; if (getLocalTime(&timeinfo)) { char timeStr[10]; char dateStr[12]; strftime(timeStr, sizeof(timeStr), "%H:%M:%S", &timeinfo); strftime(dateStr, sizeof(dateStr), "%Y-%m-%d", &timeinfo); currentTime = String(timeStr); currentDate = String(dateStr); } } void updateWeather() { if (!wifiConnected || !weatherEnabled) return; HTTPClient http; String url = "http://api.openweathermap.org/data/2.5/weather?q=" + String(city) + "&appid=" + String(weatherApiKey) + "&units=metric&lang=zh_tw"; http.begin(url); int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, payload); weatherData.description = doc["weather"][0]["description"].as<String>(); weatherData.temp = doc["main"]["temp"]; weatherData.humidity = doc["main"]["humidity"]; weatherData.pressure = doc["main"]["pressure"]; weatherData.windSpeed = doc["wind"]["speed"]; weatherData.icon = doc["weather"][0]["icon"].as<String>(); weatherData.valid = true; Serial.println("天氣更新成功"); } else { Serial.printf("天氣更新失敗: %d\n", httpCode); weatherData.valid = false; } http.end(); } void updateSensorData() { // 讀取 AHT20 溫濕度 if (sensorData.ahtValid) { sensors_event_t humidity, temp; aht.getEvent(&humidity, &temp); float tempValue = temp.temperature; float humValue = humidity.relative_humidity; // 檢查數值有效性 if (!isnan(tempValue) && !isnan(humValue)) { sensorData.temperature = tempValue; sensorData.humidity = humValue; } else { Serial.println("AHT20 讀取失敗"); sensorData.ahtValid = false; } } // 讀取氣體感測器 sensorData.mq2_raw = analogRead(MQ2_PIN); sensorData.mq135_raw = analogRead(MQ135_PIN); // 轉換為 PPM sensorData.mq2_ppm = convertMQ2ToPPM(sensorData.mq2_raw); sensorData.mq135_ppm = convertMQ135ToPPM(sensorData.mq135_raw, sensorData.temperature, sensorData.humidity); // 計算空氣品質指數 calculateAirQualityIndex(); Serial.printf("AHT20(%s) - 溫度: %.1f°C, 濕度: %.1f%%, MQ2: %.1f ppm, MQ135: %.1f ppm\n", sensorData.ahtValid ? "OK" : "NG", sensorData.temperature, sensorData.humidity, sensorData.mq2_ppm, sensorData.mq135_ppm); } float convertMQ2ToPPM(int rawValue) { // MQ-2 轉換公式 (3.3V供電) float voltage = rawValue * (3.3 / 4095.0); if (voltage >= 3.3) voltage = 3.29; // 避免除零 float rs = (3.3 - voltage) / voltage * 10.0; // 假設負載電阻 10kΩ float ppm = 100 * pow(rs / 10.0, -1.4); // 簡化公式 return constrain(ppm, 0, 9999); // 限制範圍 } float convertMQ135ToPPM(int rawValue, float temp, float hum) { // MQ-135 轉換公式 (5V供電) 並進行溫濕度補償 float voltage = rawValue * (3.3 / 4095.0); // ADC 參考電壓仍是 3.3V if (voltage >= 3.3) voltage = 3.29; // 避免除零 // 感測器是 5V 供電,需要考慮分壓 float actualVoltage = voltage * (5.0 / 3.3); // 調整為實際感測器電壓 float rs = (5.0 - actualVoltage) / actualVoltage * 10.0; // 溫濕度補償 (只有在 AHT20 有效時才進行) float correctionFactor = 1.0; if (sensorData.ahtValid) { correctionFactor = 1.0 + 0.02 * (temp - 20.0) + 0.01 * (hum - 50.0); } float compensatedRs = rs / correctionFactor; float ppm = 50 * pow(compensatedRs / 10.0, -1.2); // 簡化公式 return constrain(ppm, 0, 9999); // 限制範圍 } void calculateAirQualityIndex() { // 基於 MQ135 數值計算 AQI if (sensorData.mq135_ppm <= 50) { sensorData.airQualityIndex = map(sensorData.mq135_ppm, 0, 50, 0, 50); sensorData.airQualityLevel = "優良"; } else if (sensorData.mq135_ppm <= 100) { sensorData.airQualityIndex = map(sensorData.mq135_ppm, 51, 100, 51, 100); sensorData.airQualityLevel = "良好"; } else if (sensorData.mq135_ppm <= 200) { sensorData.airQualityIndex = map(sensorData.mq135_ppm, 101, 200, 101, 150); sensorData.airQualityLevel = "輕度污染"; } else if (sensorData.mq135_ppm <= 300) { sensorData.airQualityIndex = map(sensorData.mq135_ppm, 201, 300, 151, 200); sensorData.airQualityLevel = "中度污染"; } else { sensorData.airQualityIndex = 250; sensorData.airQualityLevel = "重度污染"; } } void updateDisplay() { switch (currentPage) { case PAGE_OVERVIEW: drawOverviewPage(); break; case PAGE_AIR_DETAIL: drawAirDetailPage(); break; case PAGE_CLIMATE: drawClimatePage(); break; case PAGE_WEATHER: drawWeatherPage(); break; case PAGE_SYSTEM: drawSystemPage(); break; } } void drawOverviewPage() { tft.fillScreen(TFT_BLACK); // 頁面指示器 drawPageIndicator(0); // 標題 tft.setTextColor(TFT_CYAN); tft.setTextSize(1); tft.setCursor(85, 5); tft.println("空氣品質總覽"); // 空氣品質指數 (大字顯示) uint16_t aqiColor = getAQIColor(sensorData.airQualityIndex); tft.setTextColor(aqiColor); tft.setTextSize(3); tft.setCursor(90, 25); tft.printf("%d", sensorData.airQualityIndex); tft.setTextSize(1); tft.setCursor(80, 55); tft.setTextColor(aqiColor); tft.println(sensorData.airQualityLevel); // 空氣品質圖標 drawAirQualityIcon(20, 25, aqiColor); // 感測器數據 (小字) tft.setTextColor(TFT_WHITE); tft.setTextSize(1); // 溫濕度 (顯示狀態) tft.setCursor(10, 75); if (sensorData.ahtValid) { tft.printf("溫度: %.1f°C", sensorData.temperature); } else { tft.setTextColor(TFT_RED); tft.print("溫度: 錯誤"); tft.setTextColor(TFT_WHITE); } tft.setCursor(120, 75); if (sensorData.ahtValid) { tft.printf("濕度: %.0f%%", sensorData.humidity); } else { tft.setTextColor(TFT_RED); tft.print("濕度: 錯誤"); tft.setTextColor(TFT_WHITE); } // 氣體感測 tft.setCursor(10, 90); tft.printf("煙霧: %.0f ppm", sensorData.mq2_ppm); tft.setCursor(120, 90); tft.printf("CO₂: %.0f ppm", sensorData.mq135_ppm); // 時間 tft.setTextColor(TFT_YELLOW); tft.setCursor(10, 110); if (ntpSynced) { tft.printf("%s %s", currentTime.c_str(), currentDate.c_str()); } else { tft.printf("運行: %lu 秒", millis() / 1000); } // 操作提示 tft.setTextColor(TFT_GREEN); tft.setCursor(150, 120); tft.println("A:下頁 B:更新"); } void drawAirDetailPage() { tft.fillScreen(TFT_BLACK); drawPageIndicator(1); // 標題 tft.setTextColor(TFT_CYAN); tft.setTextSize(1); tft.setCursor(80, 5); tft.println("空氣品質詳細"); // MQ-2 煙霧感測器 tft.setTextColor(TFT_ORANGE); tft.setTextSize(1); tft.setCursor(10, 25); tft.println("MQ-2 煙霧偵測 (GPIO12):"); tft.setTextColor(TFT_WHITE); tft.setTextSize(2); tft.setCursor(10, 40); tft.printf("%.1f ppm", sensorData.mq2_ppm); // 煙霧警示條 drawWarningBar(150, 40, sensorData.mq2_ppm, 0, 500); // MQ-135 空氣品質感測器 tft.setTextColor(TFT_LIGHTGREEN); tft.setTextSize(1); tft.setCursor(10, 65); tft.println("MQ-135 空氣品質 (GPIO27):"); tft.setTextColor(TFT_WHITE); tft.setTextSize(2); tft.setCursor(10, 80); tft.printf("%.1f ppm", sensorData.mq135_ppm); // 空氣品質警示條 drawWarningBar(150, 80, sensorData.mq135_ppm, 0, 300); // AQI 指數 uint16_t aqiColor = getAQIColor(sensorData.airQualityIndex); tft.setTextColor(aqiColor); tft.setTextSize(1); tft.setCursor(10, 105); tft.printf("AQI: %d (%s)", sensorData.airQualityIndex, sensorData.airQualityLevel.c_str()); } void drawClimatePage() { tft.fillScreen(TFT_BLACK); drawPageIndicator(2); // 標題 tft.setTextColor(TFT_CYAN); tft.setTextSize(1); tft.setCursor(80, 5); tft.println("AHT20 溫濕度監控"); // 溫度顯示 tft.setTextColor(TFT_RED); tft.setTextSize(1); tft.setCursor(20, 25); tft.println("溫度:"); if (sensorData.ahtValid) { tft.setTextSize(3); tft.setCursor(20, 40); tft.printf("%.1f", sensorData.temperature); tft.setTextSize(2); tft.print("°C"); // 溫度圖標 drawTemperatureIcon(150, 35, TFT_RED); } else { tft.setTextColor(TFT_RED); tft.setTextSize(2); tft.setCursor(20, 40); tft.println("感測器錯誤"); } // 濕度顯示 tft.setTextColor(TFT_BLUE); tft.setTextSize(1); tft.setCursor(20, 75); tft.println("濕度:"); if (sensorData.ahtValid) { tft.setTextSize(3); tft.setCursor(20, 90); tft.printf("%.0f", sensorData.humidity); tft.setTextSize(2); tft.print("%"); // 濕度圖標 drawHumidityIcon(150, 85, TFT_BLUE); } else { tft.setTextColor(TFT_RED); tft.setTextSize(2); tft.setCursor(20, 90); tft.println("感測器錯誤"); } // 舒適度指示 (只有在 AHT20 有效時才顯示) if (sensorData.ahtValid) { String comfort = getComfortLevel(sensorData.temperature, sensorData.humidity); uint16_t comfortColor = getComfortColor(comfort); tft.setTextColor(comfortColor); tft.setTextSize(2); tft.setCursor(60, 115); tft.println(comfort); } else { tft.setTextColor(TFT_YELLOW); tft.setTextSize(1); tft.setCursor(20, 115); tft.println("請檢查 AHT20 I2C 連接"); } } void drawWeatherPage() { tft.fillScreen(TFT_BLACK); drawPageIndicator(3); // 標題 tft.setTextColor(TFT_CYAN); tft.setTextSize(1); tft.setCursor(85, 5); tft.println("天氣與時間"); if (!wifiConnected) { tft.setTextColor(TFT_RED); tft.setTextSize(2); tft.setCursor(50, 60); tft.println("無網路連線"); return; } // 時間顯示 if (ntpSynced) { tft.setTextColor(TFT_YELLOW); tft.setTextSize(2); tft.setCursor(40, 25); tft.println(currentTime); tft.setTextSize(1); tft.setCursor(70, 45); tft.println(currentDate); } else { tft.setTextColor(TFT_ORANGE); tft.setTextSize(1); tft.setCursor(40, 30); tft.println("時間同步中..."); } if (weatherEnabled && weatherData.valid) { // 天氣描述 tft.setTextColor(TFT_WHITE); tft.setTextSize(1); tft.setCursor(10, 65); tft.printf("天氣: %s", weatherData.description.c_str()); // 氣溫 tft.setCursor(10, 80); tft.printf("氣溫: %.1f°C", weatherData.temp); // 濕度和氣壓 tft.setCursor(10, 95); tft.printf("濕度: %d%% 氣壓: %.0f hPa", weatherData.humidity, weatherData.pressure); // 風速 tft.setCursor(10, 110); tft.printf("風速: %.1f m/s", weatherData.windSpeed); // 天氣圖標 drawWeatherIcon(180, 65, weatherData.icon); } else if (weatherEnabled) { tft.setTextColor(TFT_ORANGE); tft.setCursor(10, 70); tft.println("天氣資料載入中..."); } else { tft.setTextColor(TFT_YELLOW); tft.setCursor(10, 70); tft.println("天氣功能未啟用"); tft.setCursor(10, 85); tft.println("請設定 API Key"); } } void drawSystemPage() { tft.fillScreen(TFT_BLACK); drawPageIndicator(4); // 標題 tft.setTextColor(TFT_CYAN); tft.setTextSize(1); tft.setCursor(85, 5); tft.println("系統狀態"); // WiFi 狀態 tft.setTextColor(wifiConnected ? TFT_GREEN : TFT_RED); tft.setCursor(10, 25); tft.printf("WiFi: %s", wifiConnected ? "已連線" : "未連線"); if (wifiConnected) { tft.setTextColor(TFT_WHITE); tft.setCursor(10, 40); tft.printf("IP: %s", WiFi.localIP().toString().c_str()); tft.setCursor(10, 55); tft.printf("RSSI: %d dBm", WiFi.RSSI()); } // 時間同步狀態 tft.setTextColor(ntpSynced ? TFT_GREEN : TFT_RED); tft.setCursor(10, 70); tft.printf("時間同步: %s", ntpSynced ? "已同步" : "未同步"); // 感測器狀態 tft.setTextColor(TFT_WHITE); tft.setCursor(10, 85); tft.printf("AHT20: %s", sensorData.ahtValid ? "正常" : "異常"); // 天氣功能 tft.setCursor(10, 100); tft.printf("天氣API: %s", weatherEnabled ? "啟用" : "停用"); // 運行時間 tft.setCursor(10, 115); unsigned long uptime = millis() / 1000; tft.printf("運行時間: %lu 秒", uptime); } // 繪圖函式 void drawPageIndicator(int currentPageIndex) { // 頁面指示圓點 for (int i = 0; i < PAGE_COUNT; i++) { uint16_t color = (i == currentPageIndex) ? TFT_CYAN : TFT_DARKGREY; tft.fillCircle(10 + i * 15, 130, 3, color); } } void drawAirQualityIcon(int x, int y, uint16_t color) { // 簡單的空氣品質圖標 (雲朵形狀) tft.fillCircle(x, y + 10, 8, color); tft.fillCircle(x + 8, y + 10, 6, color); tft.fillCircle(x + 15, y + 10, 8, color); tft.fillRect(x - 8, y + 10, 23, 8, color); } void drawTemperatureIcon(int x, int y, uint16_t color) { // 溫度計圖標 tft.drawCircle(x, y + 15, 4, color); tft.drawRect(x - 2, y, 4, 15, color); tft.fillCircle(x, y + 15, 2, color); } void drawHumidityIcon(int x, int y, uint16_t color) { // 水滴圖標 tft.fillCircle(x, y + 10, 6, color); tft.fillTriangle(x, y, x - 6, y + 10, x + 6, y + 10, color); } void drawWeatherIcon(int x, int y, String iconCode) { // 根據天氣代碼繪製簡單圖標 if (iconCode.startsWith("01")) { // 晴天 tft.fillCircle(x, y, 8, TFT_YELLOW); } else if (iconCode.startsWith("02") || iconCode.startsWith("03")) { // 多雲 tft.fillCircle(x - 5, y, 6, TFT_WHITE); tft.fillCircle(x + 5, y, 6, TFT_LIGHTGREY); } else if (iconCode.startsWith("04")) { // 陰天 tft.fillCircle(x, y, 8, TFT_DARKGREY); } else if (iconCode.startsWith("09") || iconCode.startsWith("10")) { // 雨天 tft.fillCircle(x, y - 5, 6, TFT_DARKGREY); for (int i = 0; i < 3; i++) { tft.drawLine(x - 3 + i * 3, y + 3, x - 3 + i * 3, y + 10, TFT_BLUE); } } } void drawWarningBar(int x, int y, float value, float minVal, float maxVal) { // 警示條 int barWidth = 60; int barHeight = 8; // 背景 tft.drawRect(x, y, barWidth, barHeight, TFT_WHITE); // 填充條 float percentage = constrain(value / maxVal, 0, 1); int fillWidth = barWidth * percentage; uint16_t fillColor; if (percentage < 0.3) fillColor = TFT_GREEN; else if (percentage < 0.7) fillColor = TFT_YELLOW; else fillColor = TFT_RED; if (fillWidth > 2) { tft.fillRect(x + 1, y + 1, fillWidth - 2, barHeight - 2, fillColor); } } // 輔助函式 uint16_t getAQIColor(int aqi) { if (aqi <= 50) return TFT_GREEN; else if (aqi <= 100) return TFT_YELLOW; else if (aqi <= 150) return TFT_ORANGE; else if (aqi <= 200) return TFT_RED; else return TFT_PURPLE; } String getComfortLevel(float temp, float humidity) { if (temp >= 20 && temp <= 26 && humidity >= 40 && humidity <= 60) { return "舒適"; } else if (temp < 18) { return "偏冷"; } else if (temp > 28) { return "偏熱"; } else if (humidity < 30) { return "乾燥"; } else if (humidity > 70) { return "潮濕"; } else { return "一般"; } } uint16_t getComfortColor(String comfort) { if (comfort == "舒適") return TFT_GREEN; else if (comfort == "一般") return TFT_YELLOW; else return TFT_ORANGE; } void updateLEDAlert() { // 根據最嚴重的警示等級設定 LED 顏色 bool highSmoke = sensorData.mq2_ppm > 200; bool highAirPollution = sensorData.mq135_ppm > 150; bool extremeTemp = (sensorData.ahtValid && (sensorData.temperature < 10 || sensorData.temperature > 35)); bool sensorError = (!sensorData.ahtValid); if (sensorError) { // 感測器錯誤 - 紅色閃爍 setRGBColor(255, 0, 0); delay(50); setRGBColor(0, 0, 0); } else if (highSmoke || highAirPollution || extremeTemp) { // 緊急狀況 - 紅色 setRGBColor(255, 0, 0); } else if (sensorData.airQualityIndex > 100) { // 輕度污染 - 橙色 setRGBColor(255, 165, 0); } else if (sensorData.airQualityIndex > 50) { // 良好但需注意 - 黃色 setRGBColor(255, 255, 0); } else { // 空氣品質良好 - 綠色 setRGBColor(0, 255, 0); } } void setRGBColor(uint8_t r, uint8_t g, uint8_t b) { ledcWrite(RED_CH, 255 - r); ledcWrite(GREEN_CH, 255 - g); ledcWrite(BLUE_CH, 255 - b); } void buttonFeedback() { setRGBColor(255, 255, 255); delay(100); updateLEDAlert(); } |
Direct link: https://paste.plurk.com/show/CKb7EpxZVudun82zkqSt