Search
Duplicate
moon
sun

아두이노 IoT기초_센서값저장(with 구글시트,GAS)

목차(클릭하세요)
💡
아두이노가 측정한 온습도를 인터넷 너머 구글 시트에 자동으로 쌓는 실습 일반적으로 엑셀에 저장하는 방법, 프로그램도 있지만, 보다 진일보한 형태로 구글 appas script를 활용하는 방식
[참고자료] 엑셀에 저장하는 방법은 상당히 많이 존재함

0. 아두이노 우노 R4에서 측정값을 구글 시트로 저장하기 위한 사전지식

0-1. 2.4GH만 대역만 지원

[주의점] WiFi - 5GHz 사용 불가)

0-2.관련 라이브러리

•
WiFi는 사용을 위한 "WiFiS3.h" 라이브러리가 이미 존재함(기본 라이브러리)

0-3. 유의사항

⚠️ 실습에서 비밀번호 3종 세트가 사용됨➡️ 혼동 주의
•
#주의사항: 구글 계정 비밀번호는 코드 어디에도 입력하지 않음
•
아두이노 코드의 PASSWORD는 구글 계정 비밀번호가 아니라 와이파이 비밀번호임
•
CLASS_KEY는 사용자가 임의로 정하는 수업용 키임
◦
영문과 숫자를 섞어 각자 다르게 정하기
값
복사할 위치
입력할 부분
시트 ID
시트 주소의 /d/ 다음
Apps Script의 SHEET_ID
배포 ID
배포된 구글 웹 앱 주소의 /s/와 /exec 사이
아두이노의 DEPLOY_ID
CLASS_KEY (수업용 키)
각자 정한 영문·숫자 조합(각자 임의대로)
두 코드의 CLASS_KEY

0-4. 전체 흐름

•
DHT11 센서가 온도와 습도를 측정함
•
아두이노 UNO R4 WiFi가 측정값을 수신해 학교 와이파이를 경유하여 구글 서버로 전송함
•
구글 앱스 스크립트로 구현한 웹 앱이 이 요청을 수신함
•
앱스 스크립트가 구글 시트에 1행을 추가함

Ⅰ. 구글 시트·앱스 스크립트 수신부 구성

구글 시트는 데이터를 보관하는 저장소이고, 앱스 스크립트는 요청을 수신해 그 저장소에 값을 기록하는 프로그램임. 아두이노 작업 전에 이 수신부부터 구성함.

1-1. 구글 시트 생성과 시트 ID 확인

•
본인 구글 학교 계정으로 로그인하기
•
구글 드라이브에서 새 구글 스프레드시트 1개 생성하기
•
파일 이름은 날짜와 이름을 포함해 지정하기
◦
예시: 26년 9월온습도센서값기록
•
주소창에서 시트 ID 복사해 두기
◦
주소 형식은 https://docs.google.com/spreadsheets/d/시트_ID/edit임
◦
/d/ 다음부터 그다음 / 앞까지가 시트 ID임
[예시]https://docs.google.com/spreadsheets/d/1Rexckjdcmkldcmlk여기가시트IDq-QyiJn5TiYhm9oAWPTs/edit?gid=1461417854#gid=1461417854

1-2. 앱스 스크립트 추가와 setupSheet() 실행

•
시트 메뉴에서 확장 프로그램을 누르고 Apps Script 열기
•
새 프로젝트가 출력되면 기존 코드를 전부 삭제하고 아래 코드 붙여 넣기
const SHEET_ID = "YOUR_SPREADSHEET_ID"; const CLASS_KEY = "YOUR_OWN_LETTERS_AND_NUMBERS"; // 최초 한 번 편집기에서 실행하여 권한을 승인하고 기록 탭을 준비함 function setupSheet() { const book = SpreadsheetApp.openById(SHEET_ID); let sheet = book.getSheetByName("기록"); if (!sheet) { sheet = book.insertSheet("기록"); } if (sheet.getLastRow() === 0) { sheet.appendRow(["서버 수신 시각(KST)", "온도(℃)", "습도(%)"]); } } // 주소로 받은 값을 검사하고, 한국 시간의 수신 시각과 함께 기록함 function doGet(e) { const p = (e && e.parameter) || {}; if (p.key !== CLASS_KEY) { return ContentService.createTextOutput("DENIED: check key"); } const integer = /^\d{1,3}$/; if (!integer.test(p.temp || "") || !integer.test(p.humi || "")) { return ContentService.createTextOutput("INVALID: use integer values"); } const temp = Number(p.temp); const humi = Number(p.humi); if (temp < 0 || temp > 50 || humi < 0 || humi > 100) { return ContentService.createTextOutput("INVALID: out of range"); } const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName("기록"); if (!sheet) { return ContentService.createTextOutput("SETUP_REQUIRED: run setupSheet"); } // 시간대를 명시하여 계정 설정과 관계없이 한국 시간으로 기록함 const receivedAt = Utilities.formatDate( new Date(), "Asia/Seoul", "yyyy-MM-dd HH:mm:ss" ); sheet.appendRow([receivedAt, temp, humi]); SpreadsheetApp.flush(); return ContentService.createTextOutput("SAVED: " + temp + "," + humi); }
JavaScript
복사
•
맨 위의 SHEET_ID와 CLASS_KEY를 본인 값으로 수정하고 저장하기
◦
이 2줄이 각자 수정해야 하는 부분임
•
편집기 위쪽 함수 목록에서 setupSheet()를 선택하고 실행 누르기
•
본인 계정과 프로젝트를 확인하는 창이 출력되면 시트 접근 권한 승인하기
•
시트로 돌아가 결과 확인하기
•
제목 행에 3칸이 입력되었는지 확인하기
•
기록이라는 이름의 탭이 새로 생성되었는지 확인하기

1-3. 웹 앱 배포와 배포 ID 확인

•
앱스 스크립트에서 배포를 누르고 새 배포를 선택한 다음 종류를 웹 앱으로 지정하기
•
실행 사용자는 나로 설정하기
•
액세스 권한은 모든 사용자로 설정하기
◦
중간에 영어로 출력되는 실행 여부 확인 창도 계속 승인하기
•
배포가 끝나면 /exec로 끝나는 웹 앱 URL 복사하기
◦
주소 형식은 https://script.google.com/macros/s/배포_ID/exec임
◦
s 다음부터 exec 앞까지가 배포 ID임

1-4. 브라우저 주소창을 이용한 시험값 전송

•
아두이노 없이 브라우저만으로 구글 측 수신부가 정상 실행되는지 먼저 확인할 수 있음
•
복사해 둔 웹 앱 URL 뒤에 아래 내용을 추가해 다른 브라우저 창이나 탭의 주소창에 입력하기
?key=나의키&temp=25&humi=60
Plain Text
복사
•
나의키 자리에는 앞에서 정한 CLASS_KEY 값 입력하기
◦
키를 ysjtttysj로 지정했다면 ?key=ysjtttysj&temp=25&humi=60이 됨
•
정상 결과 확인하기
◦
브라우저 화면에 SAVED: 25,60이 출력됨
◦
시트에 한국 시간과 함께 25와 60이 1행 기록됨
🎉 여기까지 확인되면 이 실습의 50% 이상이 완료된 상태임. 구글 측 수신부가 정상 동작한다는 뜻이므로, 남은 작업은 아두이노에서 동일한 주소를 호출하도록 구현하는 것임.

Ⅱ. 아두이노 수동 입력 전송

센서를 곧바로 연결하지 않는 이유가 있음. 센서까지 한꺼번에 연결하면 값이 수신되지 않을 때 통신 문제인지 센서 문제인지 구분할 수 없음. 따라서 먼저 사람이 직접 숫자를 입력해 통신 경로부터 검증하는 과정을 거침

2-1. 보드 설정과 값 4개 수정

•
기본 보드 설정을 마치고 아래 코드 붙여 넣기
#include <WiFiS3.h> #include <WiFiSSLClient.h> #include <stdio.h> // 구글 계정 비밀번호가 아니라 Wi-Fi 비밀번호를 입력함 const char SSID[] = "YOUR_WIFI_NAME"; const char PASSWORD[] = "YOUR_WIFI_PASSWORD"; const char DEPLOY_ID[] = "YOUR_DEPLOYMENT_ID"; const char CLASS_KEY[] = "YOUR_OWN_LETTERS_AND_NUMBERS"; WiFiSSLClient client; void setup() { Serial.begin(9600); Serial.setTimeout(1000); client.setTimeout(15000); // 시리얼 모니터를 기다리되, 열지 않아도 무한정 멈추지 않음 unsigned long started = millis(); while (!Serial && millis() - started < 10000) { delay(10); } Serial.println("WiFi connecting..."); WiFi.begin(SSID, PASSWORD); delay(10000); if (WiFi.status() == WL_CONNECTED) { Serial.print("WiFi connected. IP: "); Serial.println(WiFi.localIP()); } else { Serial.println("WiFi failed. Check settings, then press RESET."); } Serial.println("Enter integers as temperature,humidity. Example: 25,60"); } void loop() { if (!Serial.available()) { return; } String input = Serial.readStringUntil('\n'); input.trim(); if (input.length() == 0) { return; } int temp; int humi; char extra; // 숫자가 아닌 입력과 남는 문자를 거부함 쉼표 앞 공백은 넣지 않음 if (input.length() > 12 || sscanf(input.c_str(), "%3d,%3d%c", &temp, &humi, &extra) != 2 || temp < 0 || temp > 50 || humi < 0 || humi > 100) { Serial.println("Invalid input. Use integers: 25,60 (T: 0-50, H: 0-100)."); return; } if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi disconnected. Press RESET, then enter again."); return; } Serial.print("Sending: "); Serial.println(input); if (!client.connect("script.google.com", 443)) { Serial.println("HTTPS connection failed. Check WiFi and firmware."); client.stop(); return; } // 두 값은 숫자이고 키는 영문·숫자이므로 주소 인코딩을 별도로 하지 않음 String path = String("/macros/s/") + DEPLOY_ID + "/exec"; path += String("?key=") + CLASS_KEY; path += String("&temp=") + temp; path += String("&humi=") + humi; client.print(String("GET ") + path + " HTTP/1.1\r\n"); client.println("Host: script.google.com"); client.println("Connection: close"); client.println(); // 첫 응답만 표시함 302도 저장 성공을 증명하지 않음 String status = client.readStringUntil('\n'); status.trim(); Serial.print("HTTP: "); Serial.println(status.length() > 0 ? status : "No status received"); client.stop(); Serial.println("Check your sheet for the new row before sending again."); }
C++
복사
•
SSID, PASSWORD, DEPLOY_ID, CLASS_KEY 4개 수정하기
◦
SSID: 학교 와이파이이므로 OOOO입력하기
◦
PASSWORD: OOOO
◦
DEPLOY_ID: 1-3에서 확인한 배포 ID (웹 앱 주소에서 s 다음부터 exec 앞까지)
◦
CLASS_KEY: 각자 정한 임의 번호를 그대로 입력하기
•
수정을 마쳤으면 컴파일 후 업로드하기

2-2. 시리얼 모니터 입력과 결과 확인

•
시리얼 모니터를 열고 9600bps와 새 줄로 설정하기
•
RESET 버튼을 누른 뒤 연결 메시지가 출력될 때까지 대기하기
◦
WiFi connected와 IP가 출력되면 준비가 완료된 상태임
•
시리얼 모니터 창에 ‘26, 67’처럼 온도와 습도를 쉼표로 연결해 숫자로 입력하기
◦
쉼표 앞에 공백을 넣지 않기
•
아래 3가지 시험 수행하기
입력
기대 결과
관찰 기록
26,61
해당 값 한 행 저장
□ 성공 □ 실패
27,62
다음 행에 해당 값 저장
□ 성공 □ 실패
abc,60
Invalid input, 저장 없음
□ 성공 □ 실패
•
3번째 시험이 통과해야 코드가 유효성 검사로 잘못된 값을 차단하고 있음을 확인한 것임
[참고]doGet()의 값 검증 순서
•
수신 측 코드는 값을 무조건 저장하지 않음.
◦
키가 일치하는지 확인하고, 숫자 형식이 맞는지 정규식으로 확인하고, 온도 0에서 50, 습도 0에서 100 범위 안인지 확인함.
•
수신측에서 3개 검사를 모두 통과해야 시트에 행이 추가되고 SAVED 응답이 반환됨.
◦
중간에 차단되면 각각 DENIED, INVALID, 범위 초과에 해당하는 다른 문자열이 반환됨

Ⅲ. 센서 자동 기록 구성

사람이 수동으로 하던 입력을 DHT11 센서로 바꾸는 단계임.
✅구글 시트 측 앱스 스크립트는 Ⅰ단계에서 검증한 코드를 그대로 재사용하며, 기록 탭도 그대로 사용함.

3-1. DHT11 온습도 센서연결

•
전원을 분리한 상태에서 연결하기
◦
S → D4
◦
+ → 5V
◦
− → GND
•
접촉이 헐거우면 값이 읽히지 않으므로 꽂은 뒤에 한 번씩 확인하기

3-2. 자동 전송 코드 업로드

•
아래 코드를 붙여 넣고 Ⅱ단계에서 검증한 설정 4개를 그대로 복사해 입력하기
#include <DHT11.h> #include <WiFiS3.h> #include <WiFiSSLClient.h> // 2단계에서 성공한 설정 네 개를 그대로 복사함 const char SSID[] = "YOUR_WIFI_NAME"; const char PASSWORD[] = "YOUR_WIFI_PASSWORD"; const char DEPLOY_ID[] = "YOUR_DEPLOYMENT_ID"; const char CLASS_KEY[] = "YOUR_OWN_LETTERS_AND_NUMBERS"; const unsigned long INTERVAL = 5000; unsigned long lastAttempt = 0; DHT11 dht11(4); WiFiSSLClient client; void setup() { Serial.begin(9600); client.setTimeout(15000); // 모니터를 열지 않아도 자동 측정이 시작되도록 대기 시간을 제한함 unsigned long started = millis(); while (!Serial && millis() - started < 10000) { delay(10); } Serial.println("WiFi connecting..."); WiFi.begin(SSID, PASSWORD); delay(10000); if (WiFi.status() == WL_CONNECTED) { Serial.print("WiFi connected. IP: "); Serial.println(WiFi.localIP()); } else { Serial.println("WiFi failed. Check settings, then press RESET."); } lastAttempt = millis(); Serial.println("DHT11 automatic logging started. Interval: 5 seconds."); } void loop() { // 전송 시작을 기준으로 5초를 계산함 통신 중에는 요청이 겹치지 않음 if (millis() - lastAttempt < INTERVAL) { return; } lastAttempt = millis(); if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi disconnected. Press RESET to reconnect."); return; } int temp = 0; int humi = 0; int err = dht11.readTemperatureHumidity(temp, humi); // 읽기에 실패한 값은 시트에 전송하지 않음 if (err != 0) { Serial.print("Sensor error: "); Serial.println(err); return; } Serial.print("Temperature: "); Serial.print(temp); Serial.print(" Humidity: "); Serial.println(humi); if (!client.connect("script.google.com", 443)) { Serial.println("HTTPS connection failed."); client.stop(); return; } // 1단계와 같은 주소 형식으로 실제 측정값을 전달함 String path = String("/macros/s/") + DEPLOY_ID + "/exec"; path += String("?key=") + CLASS_KEY; path += String("&temp=") + temp; path += String("&humi=") + humi; client.print(String("GET ") + path + " HTTP/1.1\r\n"); client.println("Host: script.google.com"); client.println("Connection: close"); client.println(); // HTTP 상태만 표시하며, 저장 성공은 시트의 새 행으로 확인함 String status = client.readStringUntil('\n'); status.trim(); Serial.print("HTTP: "); Serial.println(status.length() > 0 ? status : "No status received"); client.stop(); }
C++
복사
•
코드의 동작 조건 확인하기
◦
전송 간격은 5초로 설정되어 있음
◦
읽기에 실패한 값은 시트로 전송하지 않고 건너뜀
◦
통신에 실패한 값도 자동으로 재전송하지 않음
•
HTTP 200이나 302가 출력됐는데 시트에 행이 없는 경우가 생길 수 있음
◦
통신에 실패한 값은 재전송하지 않으므로 해당 시각의 데이터는 소실될 수 있다는 이야기임
•
기록 간격이 5초보다 길어지는 것도 코드 문제가 아니라 통신 소요 시간이 추가되기 때문

3-3. 성공 기준 확인

•
시리얼 모니터에 측정값이 출력되는지 확인하기
•
시트에 실제 측정값이 3회 이상 저장되면 성공임
•
HTTP 상태만으로 성공을 판단하지 않기
◦
시리얼 모니터에 출력되는 HTTP 상태는 참고 사항임
◦
200이나 302가 출력되어도 시트에 행이 생성되지 않았으면 저장된 것이 아님
◦
성공 여부는 언제나 시트의 새 행으로 판단함
단계
입력과 성공 기준
Ⅱ단계
시리얼 모니터에 26,61을 직접 입력 → 시트에 같은 값 한 행 저장
Ⅲ단계
DHT11 측정 → 5초 간격 전송 시도 → 실제 측정값 3회 이상 저장

부록. 증상별 점검 항목

🔧 오류 메시지를 그대로 읽고 해당하는 줄을 찾기. 구글 측 증상과 아두이노 측 증상으로 구분해 정리함.

부록-1. 구글 측 증상과 조치

•
먼저 ?key=나의키&temp=25&humi=60 시험 주소를 시크릿 창에서 열어 구글 측만 단독으로 확인하기
증상
확인할 사항과 조치
setupSheet 오류
SHEET_ID가 시트 주소의 값인지 확인함. 해당 계정이 시트를 열 수 있는지 확인함.
로그인 화면 표시
웹 앱의 모든 사용자 접근 설정을 확인함. /dev 대신 /exec 사용 여부를 확인함.
DENIED
Apps Script·아두이노·시험 주소의 CLASS_KEY가 같은지 확인함. 대소문자도 구분함.
INVALID
온도와 습도가 정수인지 확인함. 온도 0~50, 습도 0~100 범위를 확인함.
SETUP_REQUIRED
setupSheet를 실행하고 기록 탭이 있는지 확인함.
수정 전 결과가 계속 표시
저장 후 배포 관리에서 새 버전으로 갱신했는지 확인함.
HTTP 302 또는 200, 그런데 기록 없음
이 상태만으로 성공을 판단하지 않음. 동일 주소·키로 브라우저 시험 후 Apps Script 실행 기록의 실패 원인을 확인함.
HTTP 403 또는 404
403은 접근 권한·네트워크 제한 가능성을, 404는 배포 ID·배포 상태를 우선 확인함. 코드만으로 원인을 단정하지 않음.
응답 없음 또는 중복 행
시트부터 확인함. 저장 후 응답만 못 받았을 수 있음. 새로고침·재전송 횟수를 확인함.

부록-2. 아두이노 측 증상과 조치

증상
확인할 사항과 조치
업로드 또는 포트 오류
UNO R4 WiFi 선택, USB 데이터 케이블, 포트 번호를 확인함. 포트를 사용하는 다른 프로그램을 닫음.
WiFiS3.h 파일 없음
Arduino UNO R4 Boards 설치와 보드 선택을 확인함. ESP32용 WiFi.h로 임의 교체하지 않음.
DHT11.h 파일 없음 또는 함수 없음
기초 코드에서 사용한 라이브러리와 비교함. 이름·제작자·버전 및 readTemperatureHumidity 지원 여부를 확인함.
WiFi failed 또는 disconnected
SSID·비밀번호·2.4GHz 접속 가능 여부를 확인함. 설정 수정 후 업로드하거나 RESET으로 다시 연결함.
HTTPS connection failed
Wi-Fi 연결 다음에 인터넷 접속·학교망 제한이므로 수업용으로 보안이 적게 셋팅된 Wifi연결 필요
Sensor error 숫자 표시
전원 분리 후 S→D4, +→5V, −→GND와 접촉 상태를 확인함. 기초 센서 코드로 따로 시험함. 오류 번호의 의미는 설치한 라이브러리 문서로 확인함.
시리얼 입력 반응 없음
9600bps와 새 줄을 확인함. 통신 응답을 기다린 뒤 다시 입력함.
기록 간격이 5초보다 김
현재 코드는 통신 중 다음 요청을 실행하지 않음. 학교망 지연·서버 처리시간을 함께 확인함. INTERVAL만 줄여 해결하려 하지 않음.