만들기
아두이노로 만든 센서를 내 정원에 연결하기
Arduino UNO R4 WiFi에 DHT22와 SEN0308을 붙여 측정값을 Open API로 전송하는 센서 노드를 만드는 방법입니다.
직접 만든 장치로도 내정원의 사계절에 센서 데이터를 보낼 수 있습니다. 이 글에서는 Arduino UNO R4 WiFi에 온습도 센서와 토양수분 센서를 붙여 5분마다 측정값을 전송하는 센서 노드를 만듭니다.
완성하면 앱의 센서 관리 화면에 센서 세 개가 자동으로 나타나고, 구역과 식물에 연결해 Ecowitt 센서와 똑같이 쓸 수 있습니다.
공유기 정보와 토양수분 보정값은 코드를 고치지 않고 휴대폰 브라우저에서 입력합니다. 보드가 설정용 Wi-Fi를 열어주는 방식이라 한 번 만들어 두면 다른 화분으로 옮길 때도 다시 업로드할 필요가 없습니다.
이 글에서 쓰는 코드 전체는 GitHub에 공개되어 있습니다 — four-seasons-of-mygarden/sensors의
arduino-r4-wifi-soilmoisture폴더입니다.
준비물
| 부품 | 설명 |
|---|---|
| Arduino UNO R4 WiFi | Wi-Fi와 12×8 LED 매트릭스가 내장된 보드입니다. |
| DHT22 | 온도와 습도를 함께 측정합니다. |
| DFRobot SEN0308 | 방수 처리된 정전용량식 토양수분 센서입니다. |
| 누름 버튼 | 설정 화면을 다시 열 때 씁니다. 없어도 동작합니다. |
| 점퍼선, USB-C 케이블 | 데이터 전송이 되는 케이블이어야 합니다. |
UNO R4 WiFi를 고른 이유는 이 API가 요구하는 세 가지를 보드 하나로 해결하기 때문입니다. WiFiSSLClient가 기본 CA 번들을 내장하고 있어 인증서 검증을 켠 채로 HTTPS를 쓸 수 있고, WiFi.getTime()으로 네트워크 시각을 바로 받아올 수 있으며, Arduino_CloudUtils의 SHA256으로 서명을 만들 수 있습니다.
토양수분 센서로 SEN0308을 쓴 것은 탐침 전체가 코팅되어 있어 저항식 센서처럼 부식되지 않기 때문입니다. 화분에 오래 꽂아 두는 용도에는 이 차이가 큽니다.
1. 회로 연결
DHT22
| DHT22 | UNO R4 WiFi |
|---|---|
| VCC | 5V |
| DATA | D2 |
| GND | GND |
센서 단품을 쓴다면 VCC와 DATA 사이에 10kΩ 풀업 저항을 넣습니다. 저항이 포함된 모듈형 제품은 필요 없습니다.
SEN0308
| SEN0308 선 색상 | UNO R4 WiFi |
|---|---|
| 빨강 (VCC) | 5V |
| 노랑 (Signal) | A0 |
| 검정 (GND) | GND |
센서를 흙에 꽂을 때 기판에 표시된 경고선을 넘기지 마세요. 방수 처리는 탐침까지이고 그 위쪽은 보호되지 않습니다.
설정 버튼
평소에는 열려 있고 누르면 연결되는 누름 버튼을 D3과 GND 사이에 연결합니다. 내부 풀업을 쓰므로 저항은 필요 없습니다. 보드의 RESET 버튼과는 별개입니다.
핀 번호는 모두 include/config.h에서 바꿀 수 있습니다.
2. 프로젝트 준비
VS Code에 PlatformIO IDE 확장을 설치하고 sensors 저장소를 내려받아 arduino-r4-wifi-soilmoisture 폴더를 엽니다. 필요한 라이브러리는 platformio.ini에 이미 정의되어 있어 처음 빌드할 때 자동으로 받습니다.
lib_deps =
arduino-libraries/ArduinoHttpClient @ ^0.6.1
arduino-libraries/Arduino_CloudUtils @ ^1.2.1
bblanchon/ArduinoJson @ ^7.4.2
adafruit/DHT sensor library @ ^1.4.6
ricmoo/QRCode@0.0.1
3. API 키 발급과 설정
앱에서 키를 먼저 발급받습니다.
MY → 설정 → 센서 관리 → 오른쪽 위 + 버튼 → OpenAPI
Key 이름을 입력하고 저장하면 Access Key와 Secret Key가 발급됩니다. Secret Key는 이때 한 번만 제공되니 CSV 파일을 꼭 저장해 두세요. 자세한 절차는 나만의 센서 데이터를 Open API로 전송하기에 정리되어 있습니다.
include/config.example.h를 include/config.h로 복사하고 값을 채웁니다.
constexpr char SETUP_AP_PASSWORD[] = "설정용_AP_비밀번호"; // 8~63자
constexpr char DEVICE_NAME[] = "베란다 화분";
constexpr char API_KEY[] = "발급받은_Access_Key";
constexpr char API_SECRET[] = "발급받은_Secret_Key";
config.h는 .gitignore에 들어 있습니다. 실제 키가 저장소에 올라가지 않도록 한 것이니, 다른 곳에 복사할 때도 이 파일은 빼고 옮기세요.
Wi-Fi 정보와 토양수분 보정값은 여기 넣지 않습니다. 다음 단계에서 웹페이지로 입력하면 EEPROM에 저장됩니다.
4. 업로드하고 Wi-Fi 설정하기
보드를 USB로 연결하고 PlatformIO의 Upload를 실행합니다.
pio run --target upload
pio device monitor
업로드가 끝나면 LED 매트릭스에 AP가 표시됩니다. 설정용 Wi-Fi가 열렸다는 뜻입니다.
- 휴대폰에서
SoilSensor-XXXXXXWi-Fi에 연결합니다. 비밀번호는config.h에 넣은 값입니다. - 브라우저에서
http://192.168.4.1을 엽니다. - 공유기 SSID와 비밀번호를 입력하고 저장합니다.
휴대폰이 “인터넷에 연결되어 있지 않음”이라고 표시해도 연결을 유지해야 합니다. 설정 AP에는 인터넷이 없는 것이 정상입니다.
저장하면 AP가 닫히고 보드가 입력한 공유기에 연결됩니다. 시리얼 모니터(115200 baud)에서 연결 결과와 IP를 확인할 수 있습니다.
이후에는 같은 Wi-Fi에서 http://soilsensor-xxxxxx.local/로 설정 페이지를 다시 열 수 있습니다. xxxxxx는 MAC 주소 마지막 6자리이고, AP 이름의 숫자와 같습니다. 설정 페이지의 QR 스티커 인쇄로 주소를 뽑아 기기에 붙여 두면 다음에 찾기 쉽습니다.
설정 화면을 다시 열어야 하는데 .local 주소가 안 열린다면, D3 버튼을 3초 이상 눌러 AP를 다시 띄울 수 있습니다.
5. 토양수분 보정하기
같은 ADC 값이라도 흙과 화분에 따라 의미가 다릅니다. 실제로 쓸 화분에서 기준점 두 개를 잡아야 합니다.
설정 페이지에는 현재 ADC 값과 계산된 습도가 1초마다 표시됩니다. 센서를 원하는 상태에 놓고 현재값 사용 버튼을 누르면 그 값이 입력칸에 들어갑니다.
| 기준점 | 잡는 방법 |
|---|---|
| 관수 기준점 | 물을 줘야겠다고 느끼는 상태의 ADC 값과, 그때 부여할 % (예: 600, 30%) |
| 습윤 기준값 | 충분히 젖은 흙에 같은 깊이로 꽂았을 때의 ADC 값 (100%) |
보정식은 다음과 같습니다.
습도(%) = P + (100 - P) × (A - 현재 ADC) / (A - W)
A는 관수 시점 ADC, P는 그때의 %, W는 충분히 젖은 상태의 ADC입니다. A=600, P=30, W=200이면 ADC 600은 30%, 400은 65%, 200은 100%가 됩니다.
화분을 바싹 말릴 필요 없이 지금 물 줘야 하는 상태를 기준으로 삼을 수 있다는 점이 편합니다. 관수 시점보다 더 마르면 30% 아래로 내려갑니다.
센서를 다른 화분으로 옮기거나 꽂는 깊이를 크게 바꿨다면 다시 보정하세요. 보정값은 센서가 아니라 센서와 화분과 흙의 조합에 붙는 값입니다. 자세한 배경은 토양수분 센서 보정하기에 있습니다.
6. 전송이 동작하는 방식
Wi-Fi가 처음 연결되고 30초 뒤 첫 전송이 일어나고, 이후 5분 간격으로 반복됩니다. 5분보다 짧은 주기로 보내면 서버에서 받지 않기 때문에 5분으로 맞춰 둔 것입니다.
보내기 전에 값을 다듬습니다
센서는 5초마다 읽지만 읽은 값을 그대로 보내지는 않습니다. 최근 표본(최대 60개)을 정렬해 위아래 10%씩 잘라낸 절사평균을 구한 뒤, alpha = 0.3인 EMA를 적용합니다. DHT22가 가끔 튀는 값을 내놓아도 그래프가 출렁이지 않습니다.
SEN0308은 한 번 읽을 때 16회 연속 측정해 평균을 씁니다.
전송에 실패하면 표본과 직전 EMA를 그대로 두고, 성공했을 때만 EMA를 확정하고 표본 구간을 새로 시작합니다.
서명 만들기
요청마다 아래 문자열을 Secret Key로 HMAC-SHA256 서명하고 Base64로 인코딩합니다.
POST /api/sensor/v1/data
1757232000000
발급받은_Access_Key
src/sensor_api.cpp의 구현입니다.
String generateSignature(const String& timestamp) {
String message;
message += SERVER_HTTP_METHOD; // "POST"
message += ' ';
message += SERVER_PATH; // "/api/sensor/v1/data"
message += '\n';
message += timestamp;
message += '\n';
message += API_KEY;
uint8_t digest[HMAC_SHA256_SIZE];
hmacSha256(reinterpret_cast<const uint8_t*>(API_SECRET), strlen(API_SECRET),
reinterpret_cast<const uint8_t*>(message.c_str()), message.length(), digest);
return base64Encode(digest, sizeof(digest));
}
Arduino_SHA256.h는 SHA256만 제공하므로 HMAC은 직접 구현했습니다. 키를 64바이트 블록에 맞춘 뒤 0x36과 0x5C로 각각 XOR해 안쪽·바깥쪽 해시를 두 번 계산하는 표준 방식입니다.
시각 다루기
WiFi.getTime()으로 epoch 초를 받고, 1700000000(2023년 11월)보다 작으면 아직 시각이 맞지 않은 것으로 보고 전송을 건너뜁니다. 서명은 서버 시각과 5분 이내여야 통과하므로 시각이 틀리면 무조건 거부됩니다.
밀리초 타임스탬프를 만드는 부분에 작은 요령이 들어 있습니다.
timestamp = String(epochSeconds);
timestamp += "000";
UNO R4의 축소된 printf가 64비트 정수 서식을 제대로 지원하지 않아서, 곱셈 대신 문자열 뒤에 000을 붙였습니다.
본문의 date는 gmtime과 strftime으로 만든 UTC 문자열입니다. 한국 시간을 넣으면 9시간 어긋난 시점에 기록되니 주의하세요.
전송 형식
const SensorReading readings[] = {
{"temp", "01", String(filteredTemperature, 2)},
{"humidity", "02", String(filteredHumidity, 2)},
{"soil_moisture", "07", String(filteredSoilMoisture, 2)},
};
const bool sent = sendSensorReadings(readings, 3);
| field_nm | category | 의미 |
|---|---|---|
temp |
01 |
온도 (°C) |
humidity |
02 |
습도 (%) |
soil_moisture |
07 |
토양습도 (%) |
토양수분을 보정된 %(07)로 보내는 점이 중요합니다. 원시 ADC 값을 그대로 넘기지 않고 보드에서 이미 환산한 값을 보내기 때문에, 앱에서 바로 의미 있는 수치로 보입니다.
7. 동작 확인하기
시리얼 모니터에서 전송 과정을 볼 수 있습니다.
[API] payload: {"device_name":"베란다 화분","date":"2026-09-07 08:00:00",...}
[API] HTTP status: 200
[API] response: 데이터 수신 성공
LED 매트릭스로도 확인할 수 있습니다. 공유기에 연결되면 다섯 화면이 2초 간격으로 돌아갑니다.
| 화면 | 표시 |
|---|---|
| 온도 | 24C |
| 공기습도 | H58 |
| 토양습도 | S42 |
| Wi-Fi | 안테나 모양, 끊기면 사선 |
| 전송 결과 | 화살표 옆에 체크(성공) 또는 X(실패) |
값을 아직 못 읽었으면 ---, H--, S--로 표시됩니다.
200이 확인되면 앱의 센서 관리 화면에서 발급한 키 아래에 temp, humidity, soil_moisture 세 센서가 나타납니다. 각각 열어서 설치한 구역을 지정하고 식물과 연결하면 식물 상세 화면에서 측정값을 볼 수 있습니다.
자주 막히는 곳
401이 나옵니다
401을 받으면 코드가 시리얼에 X-Timestamp와 X-Signature를 찍어 줍니다. 먼저 시각을 의심하세요. Could not obtain valid network time이 보이면 WiFi.getTime()이 값을 못 받은 상태입니다.
키를 붙여 넣을 때 앞뒤 공백이나 CSV의 따옴표가 섞여 들어가는 경우도 흔합니다.
설정 페이지가 안 열립니다
.local 주소는 mDNS를 씁니다. 공유기가 mDNS를 막거나 게스트 Wi-Fi에서 기기 간 통신을 차단하면 접속되지 않습니다. 시리얼 모니터의 [Settings] open http://... 출력에서 IP를 직접 확인해 접속하거나, D3 버튼을 3초 눌러 AP 모드로 여세요.
저장한 Wi-Fi 정보가 사라집니다
Upload를 실행하면 EEPROM의 Wi-Fi 정보와 보정값이 초기화됩니다. 코드를 고치지 않아도 업로드하면 새 식별자가 들어가 초기화되도록 되어 있습니다. Wi-Fi 정보를 잘못 저장했을 때 되돌리는 수단이기도 합니다.
RESET 버튼이나 전원 재연결로는 지워지지 않습니다.
토양수분이 항상 0% 또는 100%입니다
보정 기준점이 실제 값 범위 밖에 있을 가능성이 높습니다. 설정 페이지에 표시되는 현재 ADC 값을 보면서 현재값 사용 버튼으로 다시 잡으세요. A > W이고 0 ≤ P < 100이어야 합니다.
더 해볼 것
이 프로젝트는 전송 실패 시 데이터를 임시로 저장하는 기능이 아직 없습니다. Wi-Fi가 끊긴 동안의 값은 버려지고 다음 성공한 전송부터 다시 기록됩니다. EEPROM이나 SD 카드에 쌓아 두었다가 재전송하는 것이 다음 개선 항목입니다.
sensors 저장소에는 이 구성 말고 빛을 재는 구성도 들어 있습니다. 만드는 방법은 아두이노로 광 센서를 만들어 내 정원에 연결하기에 정리해 두었습니다.