2026年8月2日 星期日

[水井村USR] 從 No Data 到成功讀取四種感測器:Atlas Scientific Aquaponics Kit V1.8 × ESP32 除錯紀錄



在建置水井村智慧養殖系統時,我們選用 Atlas Scientific Aquaponics Kit V1.8, 搭配 Adafruit HUZZAH32 ESP32 Feather,希望讀取水溫、pH、溶氧與導電度, 並為後續上傳 Django REST API 做準備。原本以為只要修改 Wi-Fi 與 API 即可完成整合, 沒想到真正花最多時間的,是硬體腳位、Enable 控制與 I²C 位址的確認。

一、系統架構

Atlas Aquaponics Kit V1.8
        │
        ├── RTD(水溫)
        ├── pH
        ├── Dissolved Oxygen(DO)
        └── Conductivity(EC)
                 │
                 ▼
      Adafruit HUZZAH32 ESP32 Feather
                 │
              Arduino IDE
                 │
         Django REST API(下一階段)

二、第一個問題:全部都是 No Data

最初程式執行時,四個感測器全部無法取得有效資料:

RTD:無有效資料
pH:無有效資料
DO:無有效資料
EC:無有效資料

因此,除錯並沒有先從 API 開始,而是先確認 ESP32 是否正常、I²C 是否正確、 Atlas 模組是否有供電,以及每個感測器是否真的出現在匯流排上。

三、第二個問題:I²C 腳位誤判

一開始依照 PCB 絲印誤判為:

SDA = GPIO14
SCL = GPIO32

重新對照 Adafruit HUZZAH32 Feather 的實際腳位後,修正為:

SDA = GPIO23
SCL = GPIO22

修正之後,ESP32 才開始穩定掃描到 Atlas EZO 模組。

四、第三個問題:Enable 腳位

依照 Atlas Aquaponics V1.8 PCB 與實際測試,確認 Enable 腳位如下:

功能GPIO
pH ENGPIO13
DO ENGPIO12
EC ENGPIO27
RTD ENGPIO33
CO₂ ENGPIO15
Humidity ENGPIO32

程式初始化如下:

digitalWrite(EN_PH, LOW);
digitalWrite(EN_DO, LOW);
digitalWrite(EN_EC, LOW);

digitalWrite(EN_RTD, HIGH);
digitalWrite(EN_CO2, HIGH);
digitalWrite(EN_HUM, HIGH);

五、第四個問題:只有 EC 一直 No Data

修正腳位與 Enable 後,RTD、pH 與 DO 已能讀取,但 EC 仍顯示:

EC: No Data

甚至在單獨測試 EC 時,出現:

Wire.endTransmission = 4

一開始看起來像是 EC 模組故障、插槽異常、Enable 設定錯誤, 甚至懷疑探棒、供電或主機板本身有問題。

六、第五個問題:EC LED 與插槽交換測試

觀察 EC 模組時,發現剛通電時藍燈約亮一秒,隨後熄滅。 我們也進行了模組與插槽交換測試,但現象一致,因此逐漸排除 Conductivity 插槽本身故障的可能。

除錯過程中最重要的原則,是不要只看 LED 或假設模組一定使用原廠預設位址, 而是要實際掃描 I²C,並逐一確認每一個位址對應的模組。

七、最後找到真正原因:EC 位址被修改

I²C 掃描結果如下:

找到 0x61 ( 97) -> EZO DO
找到 0x63 ( 99) -> EZO pH
找到 0x66 (102) -> EZO RTD
找到 0x69 (105) -> 其他 I2C 裝置

原本一直把 0x69 當成未知裝置, 後來才發現它其實就是 EC 模組,只是 I²C 位址已被修改。

原先以為 EC 位址是:

constexpr uint8_t ADDR_EC = 100;  // 0x64

實際上應改成:

constexpr uint8_t ADDR_EC = 105;  // 0x69,本機 EC 已改址

掃描名稱也同步修正:

case 0x69:
  return "EZO EC(已改址)";

八、成功讀取四種感測器

最後測試結果如下:
水溫 RTD:30.01 °C
pH:6.80
溶氧 DO:31.92 mg/L
導電度 EC:441 µS/cm

目前確認的 I²C 位址如下:

感測器十進位位址十六進位位址
DO970x61
pH990x63
RTD1020x66
EC1050x69

九、本次最大的收穫

不要假設所有模組都維持原廠預設值。

設備在出廠、維修、校正或前一個專案中,都可能修改 I²C 位址、輸出格式或 LED 設定。 最可靠的方法是先掃描全部位址,再逐一確認每個裝置的實際身分。

這次除錯流程可以整理成:

  1. 確認開發板型號與 PSRAM 設定。
  2. 確認正確的 SDA、SCL 腳位。
  3. 確認各感測器 Enable 腳位與有效電位。
  4. 掃描完整 I²C 位址。
  5. 逐顆單獨讀取。
  6. 不要假設模組位址仍為原廠預設值。

十、下一步:上傳 Django REST API

完成硬體驗證後,下一階段將把四項水質資料上傳至水井 USR Django API。

{
  "token": "abc123",
  "farm_name": "湖虎戰隊",
  "pond_code": "1",
  "water_temperature": 30.01,
  "salinity": 0.22,
  "ph": 6.80,
  "dissolved_oxygen": 31.92,
  "water_source": "直流變頻水車運轉中",
  "recorded_at": "2026-08-02T14:30:00+08:00"
}

後續可延伸:

  • Django Dashboard 即時儀表板
  • 歷史趨勢分析
  • MQTT 與 LoRa
  • 異常警示
  • AI 水質判讀與養殖決策建議

結語

這次除錯的價值,不只是讓程式成功讀到資料,而是更深入理解 Atlas Scientific EZO 模組、 ESP32、Enable 控制與 I²C 通訊的實際運作。對智慧養殖系統而言, 穩定的硬體資料來源是所有雲端分析、AI 判讀與自動控制的基礎。

本文由作者主導內容規劃,並使用生成式 AI 協助文字整理、程式說明與版面設計; 所有內容均依實際硬體測試結果進行修訂與確認。

附件:程式碼

  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
/*
  Atlas Scientific Aquaponics V1.8
  修正版:硬體驗證版

  實體板:
  - Adafruit HUZZAH32 ESP32 Feather
  - Atlas Aquaponics V1.8

  依 PCB 絲印與 HUZZAH32 實體排針重新確認:
  pH EN   -> GPIO13
  DO EN   -> GPIO12
  EC EN   -> GPIO27
  RTD EN  -> GPIO33
  CO2 EN  -> GPIO15
  HUM EN  -> GPIO32

  I2C 使用 Feather 板上標示的 SDA / SCL 腳:
  SDA -> GPIO23
  SCL -> GPIO22

  Arduino IDE:
  Board: Adafruit ESP32 Feather
  PSRAM: Disabled
  Serial Monitor: 115200
*/

#include <Wire.h>
#include <Ezo_i2c.h>
#include <Ezo_i2c_util.h>

constexpr uint8_t I2C_SDA = 23;
constexpr uint8_t I2C_SCL = 22;

constexpr uint8_t EN_PH  = 13;
constexpr uint8_t EN_DO  = 12;
constexpr uint8_t EN_EC  = 27;
constexpr uint8_t EN_RTD = 33;
constexpr uint8_t EN_CO2 = 15;
constexpr uint8_t EN_HUM = 32;

constexpr uint8_t ADDR_DO  = 97;   // 0x61
constexpr uint8_t ADDR_PH  = 99;   // 0x63
constexpr uint8_t ADDR_EC  = 105;  // 0x64
constexpr uint8_t ADDR_RTD = 102;  // 0x66;

Ezo_board PH  = Ezo_board(ADDR_PH,  "PH");
Ezo_board DO  = Ezo_board(ADDR_DO,  "DO");
Ezo_board EC  = Ezo_board(ADDR_EC,  "EC");
Ezo_board RTD = Ezo_board(ADDR_RTD, "RTD");

constexpr unsigned long READ_DELAY_MS = 1100;
constexpr unsigned long LOOP_DELAY_MS = 5000;

void enableCircuits() {
  pinMode(EN_PH, OUTPUT);
  pinMode(EN_DO, OUTPUT);
  pinMode(EN_EC, OUTPUT);
  pinMode(EN_RTD, OUTPUT);
  pinMode(EN_CO2, OUTPUT);
  pinMode(EN_HUM, OUTPUT);

  // 沿用 Atlas 官方範例的極性邏輯
  digitalWrite(EN_PH, LOW);
  digitalWrite(EN_DO, LOW);
  digitalWrite(EN_EC, LOW);

  digitalWrite(EN_RTD, HIGH);
  digitalWrite(EN_CO2, HIGH);
  digitalWrite(EN_HUM, HIGH);

  delay(2000);
}

const char* deviceName(uint8_t address) {
  switch (address) {
    case 0x61: return "EZO DO";
    case 0x63: return "EZO pH";
    case 0x64: return "EZO EC";
    case 0x66: return "EZO RTD";
    case 0x67: return "EZO Pump";
    case 0x6F: return "Humidity";
    case 0x71: return "CO2";
    default:   return "其他 I2C 裝置";
  }
}

uint8_t scanI2C() {
  uint8_t count = 0;

  Serial.println();
  Serial.println("========== I2C 掃描 ==========");

  for (uint8_t address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    uint8_t error = Wire.endTransmission();

    if (error == 0) {
      Serial.printf(
        "找到 0x%02X (%3u) -> %s\n",
        address,
        address,
        deviceName(address)
      );
      count++;
    }
  }

  Serial.printf("共找到 %u 個 I2C 裝置\n", count);
  Serial.println("==============================");

  return count;
}

bool receiveValue(Ezo_board& sensor, float& value) {
  receive_and_print_reading(sensor);

  if (sensor.get_error() != Ezo_board::SUCCESS) {
    return false;
  }

  value = sensor.get_last_received_reading();
  return true;
}

void readSensors() {
  Serial.println();
  Serial.println("========== 感測器讀值 ==========");

  RTD.send_read_cmd();
  delay(READ_DELAY_MS);

  float temperature = NAN;
  bool rtdOK = receiveValue(RTD, temperature);

  if (rtdOK && temperature > -1000.0f) {
    Serial.printf("水溫 RTD:%.2f C\n", temperature);

    PH.send_cmd_with_num("T,", temperature);
    DO.send_cmd_with_num("T,", temperature);
    EC.send_cmd_with_num("T,", temperature);
  } else {
    Serial.println("水溫 RTD:無有效資料");

    PH.send_cmd_with_num("T,", 25.0);
    DO.send_cmd_with_num("T,", 25.0);
    EC.send_cmd_with_num("T,", 25.0);
  }

  delay(350);

  PH.send_read_cmd();
  DO.send_read_cmd();
  EC.send_read_cmd();

  delay(READ_DELAY_MS);

  float phValue = NAN;
  float doValue = NAN;
  float ecValue = NAN;

  bool phOK = receiveValue(PH, phValue);
  bool doOK = receiveValue(DO, doValue);
  bool ecOK = receiveValue(EC, ecValue);

  Serial.println();
  Serial.println("---------- 結果 ----------");

  if (phOK) {
    Serial.printf("pH:%.2f\n", phValue);
  } else {
    Serial.println("pH:無有效資料");
  }

  if (doOK) {
    Serial.printf("溶氧 DO:%.2f mg/L\n", doValue);
  } else {
    Serial.println("溶氧 DO:無有效資料");
  }

  if (ecOK) {
    Serial.printf("導電度 EC:%.0f uS/cm\n", ecValue);
  } else {
    Serial.println("導電度 EC:無有效資料");
  }

  Serial.println("==========================");
}

void setup() {
  Serial.begin(115200);
  delay(1500);

  Serial.println();
  Serial.println("Atlas Aquaponics V1.8 修正版硬體驗證啟動");
  Serial.printf("SDA=GPIO%u, SCL=GPIO%u\n", I2C_SDA, I2C_SCL);

  enableCircuits();

  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(100000);
  Wire.setTimeOut(1000);

  delay(500);

  scanI2C();
  readSensors();
}

void loop() {
  delay(LOOP_DELAY_MS);
  scanI2C();
  readSensors();
}

沒有留言:

張貼留言