14주차_카카오톡으로 토양 센서 값 전송하기 - sookite22/SmartDivice_24 GitHub Wiki
- 토양 센서
- OAuth
- REST API
- 실습
- 토양 센서 값 읽기
- 토양 센서 값을 카카오톡으로 전송
- 후기
토양에 있는 두 개의 탐침이 전류를 통과시키고 저항 변화가 측정된다. 토양 수분이 높으면 저항이 낮아지고 센서값은 크게 나타난다. 그 반대의 경우도 마찬가지이다.
토양 센서는 토양의 여러 가지 특성을 측정하는 데 사용된다. 가장 일반적으로 사용되는 토양 센서 중 하나는 토양 수분 센서 이다. 이는 토양의 수분 함량을 측정하여 농업, 원예, 환경 모니터링 등에 활용된다.
- 본 실습에서는 저항 방식 의 토양 센서를 사용한다.
저항 방식 센서는 토양의 전기 저항을 측정한다. 물은 전기가 잘 통하는 성질을 가지므로 토양에 포함된 수분량이 많을수록 전기 저항이 낮아진다. 두 개의 전극(프로브)를 토양에 삽입하면, 센서의 두 전극 사이에 전류를 흐르게 하여 저항을 측정할 수 있다.
토양 내의 수분 함량이 높을수록 토양의 전기 전도성이 증가하고, 저항이 감소한다. 반대로 토양이 건조할수록 저항이 증가한다. 센서는 이러한 저항 변화를 측정한다.
간단하고 저렴한 방식이지만 토양의 염분 농도에 영향을 받기 쉽다는 단점도 존재한다.
- 토양 습도 센서의 경우, 센서에 공급된 전압과 토양의 저항을 통해 전류를 계산할 수 있다. 반대로 전류를 측정하여 토양의 저항을 계산할 수도 있다.
- 전압 공급: 센서에 일정한 전압이 공급된다.
- 저항 측정: 프로브를 통해 토양의 저항을 측정한다. 이때, 토양의 수분 함량에 따라 저항 값이 달라진다.
- 전류 측정: 저항 값을 알고 있다면 공급된 전압을 통해 전류를 계산할 수 있다.
- 수분 함량 계산: 측정된 저항 값을 바탕으로 토양의 수분 함량을 계산한다. 저항 값이 낮을수록 수분 함량이 높다는 것을 의미한다.
- 앞선 주차들의 실습에 사용된 센서 설명에도 있듯이, 토양 습도 센서 또한 옴의 법칙이 적용된다.
옴의 법칙은 전압(V), 전류(I), 저항(R) 사이의 관계를 설명하는 물리 법칙이다.
V = I * R
- 전압(Voltage) 은 전기 회로에서 전력 을 나타낸다. 단위 - 볼트(V)
- 전류(Current) 는 전기 회로를 통해 흐르는 전자의 흐름 을 나타낸다. 단위 - 암페어(Ampere, A)
- 저항(Resistance) 은 전기 회로에서 전류의 흐름을 제한하는 속성 을 나타낸다. 단위 - 옴(Ohm, Ω)
예를 들어, 센서에 5V 전압을 인가하고, 전류가 2mA (0.002A)로 측정되었다고 가정해보자. 옴의 법칙을 이용하여 저항을 계산하면 2500Ω가 나온다. 이 저항값은 토양의 습도 상태를 나타내며, 저항값이 특정 범위에 있으면 습도가 높거나 낮다고 판단할 수 있다.
사용자나 응용 프로그램이 타 서비스에 자신의 정보를 공유하는 데 사용할 수 있는 표준화된 인증 프로토콜 이다. 일반적으로 웹 애플리케이션에서 사용자 인증이나 데이터 공유를 위한 권한을 부여할 때 사용된다. 이를 통해 사용자는 자신의 정보나 리소스를 안전하게 공유할 수 있게 된다.
특히 개발자들이 다른 웹 서비스와 상호 작용할 수 있도록 해주고, 데이터를 보호하고 액세스 권한을 제한하는 기능을 제공한다.
- OAuth 1.0a: 인증의 신뢰성이나 보안을 높이기 위해 사용되는 더 오래된 프로토콜이다. API 사용 시 인증에 서명이 필요하다.
- OAuth 2.0: 더 간단하고 확장성이 높은 인증 프로토콜이다. 다양한 애플리케이션 및 사용 사례에 맞게 다양한 인증 방법을 제공한다.
- Consumer: 액세스 권한을 요청하는 애플리케이션이나 서비스이다.
- Resource Owner: 사용자나 시스템의 소유자이다. 액세스 권한을 제공할 수 있다.
- Resource Server: 데이터나 리소스를 보관하고 제공하는 서버이다.
- Authorization Server: 사용자의 권한을 승인하는 서버이다.
- 리소스 소유자 인증: 리소스 소유자가 인증을 받는다. 예를 들어, 웹 브라우저에서의 로그인이 있다.
- 권한 요청: 소비자가 리소스 서버와 통신하기 전에 권한을 요청한다.
- 인가 코드 발급: 리소스 서버는 리소스 소유자에게 권한을 부여하고 인가 코드를 발급한다.
- 액세스 토큰 발급: 인가 코드가 발급되면 소비자가 액세스 토큰을 요청할 수 있다. 이 토큰은 리소스 서버와 소비자 사이의 인증을 위한 것이다.
- 액세스 토큰 사용: 소비자는 해당 액세스 토큰을 사용하여 리소스 서버에 요청을 보낼 수 있다.
네트워크 상에서 자원을 처리하는 방식 을 정의한 아키텍처 스타일 이다.
HTTP 프로토콜을 기반으로 클라이언트와 서버 간 통신을 통해 자원(데이터)을 처리하고, 이를 통해 웹 서비스나 애플리케이션을 설계할 때 자주 사용된다.
-
자원(Resource): 자원은 네트워크 상에서 액세스할 수 있는 모든 것을 의미한다. 사용자, 파일, 이미지 등이 있다. 각 자원은 고유한 URI(Uniform Resource Identifier) 로 식별된다.
-
표현(Representation): 클라이언트가 자원에 접근할 때, 서버는 자원의 현재 상태를 클라이언트에 전달한다. 이는 일반적으로 JSON, XML, HTML 등으로 전송된다.
-
HTTP 메서드: REST API는 HTTP 메서드를 사용하여 자원에 대한 작업을 정의한다.
- GET: 자원 조회
- POST: 새로운 자원 생성
- PUT: 기존 자원 수정
- DELETE: 자원 삭제
- PATCH: 자원의 일부 수정
-
상태 없는(stateless) 각 요청은 클라이언트의 상태를 서버에 저장하지 않는다. 모든 요청은 독립적이며, 필요한 모든 정보는 요청에 포함되어야 한다. 예를 들어, 사용자가 로그인을 한 후 다른 작업을 할 때마다 로그인 정보를 포함해야 한다.
-
캐시 가능(Cachable): 응답은 명시적으로 캐시할 수 있어야 한다. 이를 통해 성능 향상이 가능하다.
-
계층화(Layered System) 클라이언트는 서버와 직접 통신하지 않을 수 있으며, 중간 서버를 통해 통신할 수도 있다. 중간 서버는 로드 밸런싱, 보안, 캐싱 등의 역할을 수행할 수 있다.
-
인터페이스 일관성(Uniform Interface) RESTful API는 일관된 인터페이스를 제공한다. 자원과 상호작용하는 방법이 일관되게 정의되어 있어야 한다.
- 확장성: 클라이언트와 서버가 독립적으로 동작할 수 있다.
- 유연성: 다양한 데이터 형식을 지원하며, HTTP 표준을 사용하여 다양한 플랫폼에서 사용 가능하다.
- 유지보수성: 일관된 인터페이스를 제공하므로 유지보수가 용이하다.
- 복잡성: 상태를 유지하지 않아 클라이언트가 요청마다 모든 정보를 제공해야 할 때가 많이 발생한다.
- 보안: HTTP를 사용하므로 기본적으로 보안에 취약하며 HTTPS를 사용하여 보안을 강화해야 한다.
const int soilSensorPin = 34;
void setup(){
Serial.begin(115200);
}
void loop(){
Serial.print("토양 습도 센서 값: ");
Serial.println(analogRead(soilSensorPin));
delay(1000);
}
- 시리얼 모니터를 통해 결과값을 확인할 수 있다.
센서값 실시간 확인을 위하여 별도의 앱을 설치하지 않아도 기존에 사용하던 카카오톡을 활용할 수 있다. 이를 위한 카카오톡 앱 설정을 해보자.
카카오톡 개발자 사이트 https://developers.kakao.com/ 에 로그인한다.
- 내 애플리케이션을 추가하고 REST API키 복사해 둔다.
- "플랫폼" 탭에서 Web 플랫폼 등록을 한다. 사이트 도메인은 "http://localhost"로 저장하면 된다.
-
"카카오톡 로그인" 탭에서 카카오톡 로그인을 활성화하고, Redirect URL에 “https://www.example.com/oauth”를 저장한다.
-
카카오톡 로그인 탭 중 "동의항목"을 선택하고, 카카오톡 메시지 전송” 항목을 다음과 같이 설정한다.
- 토큰 은 사용자의 카카오 로그인 인증 및 인가 정보를 담은 권한 증명으로, 카카오 API 호출에 사용된다.
- 카카오 로그인은 OAuth 2.0 표준 규격에 따라 액세스 토큰(Access token), 리프레시 토큰(Refresh token) 두 종류의 토큰을 발급한다.
-
액세스 토큰: 사용자 인증, 카카오 API 호출 권한을 부여한다.
-
리프레시 토큰: 액세스 토큰 재발급에 사용된다. 유효한 리프레시 토큰이 있다면 사용자가 매번 카카오 계정 정보를 입력하거나 카카오톡으로 로그인하는 인증 절차를 거치지 않아도 액세스 토큰을 재발급 받을 수 있다.
-
ID 토큰: 카카오 로그인 사용자의 인증 정보를 제공한다.
-
이러한 토큰들은 만료 시간이 존재하므로 주의하자.
브라우저 주소창에 다음과 같이 입력한다.
- {CLIENT_ID}는 카카오 개발자 사이트에서 발급 받은 앱의 REST API 키, {REDIRECT_URI}는 설정한 Redirect URI를 의미한다.
브라우저에서 위 URL을 입력하고 엔터를 누르면, 카카오 로그인 화면으로 리디렉션된다. 사용자 로그인 및 권한 동의를 완료하면, 설정한 Redirect URI로 리디렉션되고 URL에 code 파라미터로 인가 코드 가 포함된다. code= 이후의 값을 복사해 둔다.
- 인가 코드는 한 번 사용하면 만료되므로 필요 시 재발급을 받도록 하자.
최종적으로 Access 토큰을 발급 받기 위하여 HTTP POST 요청을 카카오 토큰 엔드포인트에 보내야 한다.
명령 프롬포트에 아래와 같은 명령어를 입력한다.
curl -v -X POST "https://kauth.kakao.com/oauth/token"
-H "Content-Type: application/x-wwwform-urlencoded"
-d "grant_type=authorization_code"
-d "client_id=${CLIENT_ID}"
--dataurlencode "redirect_uri=${REDIRECT_URI}"
-d "code=${AUTHORIZE_CODE}"
다음과 같은 명령문으로도 가능하다.
curl -X POST "https://kauth.kakao.com/oauth/token"
-d "grant_type=authorization_code"
-d "client_id={CLIENT_ID}"
-d "redirect_uri={REDIRECT_URI}"
-d "code={AUTHORIZE_CODE}"
다음과 같은 파이썬 코드를 실행한다.
import requests
import json
url = 'https://kauth.kakao.com/oauth/token'
client_id = 'REST_API_KEY'
redirect_uri = 'https://www.example.com/oauth'
code = 'AUTHORIZATION_CODE'
data = {
'grant_type':'authorization_code',
'client_id':client_id,
'redirect_uri':redirect_uri,
'code': code,
}
response = requests.post(url, data=data)
tokens = response.json()
print(tokens)
- {AUTHORIZE_CODE}는 위에서 발급 받은 인가 코드이다.
- 위 요청이 성공하면 JSON 형태로 Access 토큰과 Refresh 토큰이 반환된다.
먼저 'ArduinoJson' 라이브러리를 설치한다.
#include <ArduinoJson.h>
void setup() {
// Initialize serial port
Serial.begin(115200);
while (!Serial) continue;
delay(3000);
// JSON 문서 할당
//
// 괄호 안의 200은 메모리 풀의 용량(바이트)입니다.
// JSON 문서와 일치하도록 이 값을 변경하는 것을 잊지 마십시오.
// https://arduinojson.org/v6/assistant 를 사용하여 용량을 계산합니다.
StaticJsonDocument<96> doc;
// StaticJsonDocument<N>은 스택에 메모리를 할당합니다.
// 힙에 할당하는 DynamicJsonDocument로 대체할 수 있습니다.
// DynamicJsonDocument doc(200);
// JSON 입력 문자열.
char json[] = R"rawliteral({
"token_type":"bearer",
"access_token":"c281d73b097",
"expires_in":43199,
"refresh_token":"0a0c90af08f",
"refresh_token_expires_in":5184000,
"scope":"account_email profile"
})rawliteral";
Serial.println(json);
// Deserialize the JSON document
DeserializationError error = deserializeJson(doc, json);
// Test if parsing succeeds.
if (error) {
Serial.print(F("deserializeJson() failed: "));
Serial.println(error.f_str());
return;
}
// Fetch values.
const char* access_token = doc["access_token"];
const char* refresh_token = doc["refresh_token"];
long expires_in = doc["expires_in"];
// Print values.
Serial.print("Access token : ");
Serial.println(access_token);
Serial.print("Refresh token : ");
Serial.println(refresh_token);
Serial.print("Expire time : ");
Serial.println(expires_in, 6);
}
void loop() {
// not used in this example
}
#include <WiFi.h>
#include <HTTPClient.h>
const char *ssid = "Your_SSID"; // 사용하는 WiFi 네트워크 이름 (SSID)
const char *password = "Your_Password"; // 사용하는 WiFi 네트워크 비밀번호
const String rest_api_key = "REST API KEY";
String access_token = "Access token";
String refresh_token = "Refresh token";
#define MsgSendInterval 3600 // 60 * 60 초, 즉 한시간 간격으로 전송
long timeout = 3600; //시간을 초로 나타냄
int sensorValue = 0;
int sensorPin = 34; // 토양 습도 센서 핀
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
Serial.println("Connecting");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
Serial.print("\nConnected to WiFi : ");
Serial.println(WiFi.localIP());
}
void loop() {
if (timeout++ > MsgSendInterval) // 1시간(60 * 60)에 1번씩 전송
{
if (isAccessTokenExpired() == true) { //access token 만료 여부 확인
if (update_access_token() == false) { // access token 재발급
Serial.println("Access token update failed");
}
}
sensorValue = analogRead(sensorPin);//토양 센서값 읽기
send_message();
timeout = 0;
}
delay(1000);
}
// str문자열에서 start_string와 end_string사이의 문자열을 추출하는 함수
String extract_string(String str, String start_string, String end_string) {
int index1 = str.indexOf(start_string) + start_string.length();
int index2 = str.indexOf(end_string, index1);
String value = str.substring(index1, index2);
return value;
}
bool isAccessTokenExpired() {
HTTPClient http;
bool returnVal = true;
/*
curl -v -X GET "https://kapi.kakao.com/v1/user/access_token_info" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
*/
if (!http.begin("https://kapi.kakao.com/v1/user/access_token_info")) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Authorization", "Bearer " + access_token);
int httpCode = http.GET();
// httpCode will be negative on error
if (httpCode > 0) {
// file found at server
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(payload);
String expireT = extract_string(payload, "\"expires_in\":", ",");
Serial.println(expireT.toInt());
if (expireT.toInt() > 0) {
returnVal = false;
} else {
returnVal = true;
}
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
return returnVal;
}
void send_message() {
HTTPClient http;
String url = "https://kapi.kakao.com/v2/api/talk/memo/default/send";
if (!http.begin(url)) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Authorization", "Bearer " + access_token);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int http_code;
/*
template_object={
"object_type": "text",
"text": "텍스트 영역입니다. 최대 200자 표시 가능합니다.",
"link": {
"web_url": "https://developers.kakao.com",
"mobile_web_url": "https://developers.kakao.com"
},
"button_title": "바로 확인"
}
*/
String data = String("template_object={") +
String("\"object_type\": \"text\",") +
String("\"text\": \"") + String("토양 센서 값 :") +
String(sensorValue) + //토양 센서 값
String("\",\"link\": {}}"); //link가 없으면 오류메세지 받음
Serial.println(data);
http_code = http.POST(data);
Serial.print("HTTP Response code: ");
Serial.println(http_code);
String response;
if (http_code > 0) {
response = http.getString();
Serial.println(response);
}
http.end();
}
/*
curl -v -X POST "https://kauth.kakao.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=${REST_API_KEY}" \
-d "refresh_token=${USER_REFRESH_TOKEN}"
*/
bool update_access_token() {
HTTPClient http;
bool retVal = false;
String url = "https://kauth.kakao.com/oauth/token";
String new_refresh_token = "";
if (!http.begin(url)) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int http_code;
String data = "grant_type=refresh_token&client_id=" + rest_api_key + "&refresh_token=" + refresh_token;
Serial.println(data);
http_code = http.POST(data);
Serial.print("HTTP Response code: ");
Serial.println(http_code);
String response;
if (http_code > 0) {
response = http.getString();
Serial.println(response);
access_token = extract_string(response, "{\"access_token\":\"", "\"");
new_refresh_token = extract_string(response, "\"refresh_token\":\"", "\"");
//만료 1개월전부터 갱신되므로 data가 없을 수도 있음
if (new_refresh_token != "") {
refresh_token = new_refresh_token;
}
retVal = true;
} else {
retVal = false;
}
http.end();
return retVal;
}
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
// WiFi 설정
const char *ssid = "AndroidHotspotSY"; // 사용하는 WiFi 네트워크 이름 (SSID)
const char *password = "s2178hr@"; // 사용하는 WiFi 네트워크 비밀번호
const String rest_api_key = ""; // REST API 값을 여가에 입력
String access_token = ""; // access_token 값을 여기에 입력
String refresh_token = ""; // refresh_token 값을 여기에 입력
#define MsgSendInterval 3600 // 60 * 60 초, 즉 한 시간 간격으로 전송
long timeout = 10; // 시간을 초로 나타냄 // 쉬운 확인을 위해 10초 간격으로 변경
int sensorValue = 0;
int sensorPin = 34; // 토양 습도 센서 핀
void setup() {
Serial.begin(115200);
// WiFi 연결
WiFi.begin(ssid, password);
Serial.println("Connecting");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
Serial.print("\nConnected to WiFi : ");
Serial.println(WiFi.localIP());
}
void loop() {
if (timeout++ > MsgSendInterval) // 1시간(60 * 60)에 1번씩 전송
{
if (isAccessTokenExpired() == true) { // access token 만료 여부 확인
if (update_access_token() == false) { // access token 재발급
Serial.println("Access token update failed");
}
}
sensorValue = analogRead(sensorPin); // 토양 센서값 읽기
send_message();
timeout = 0;
}
delay(1000);
}
// str문자열에서 start_string와 end_string 사이의 문자열을 추출하는 함수
String extract_string(String str, String start_string, String end_string) {
int index1 = str.indexOf(start_string) + start_string.length();
int index2 = str.indexOf(end_string, index1);
String value = str.substring(index1, index2);
return value;
}
bool isAccessTokenExpired() {
HTTPClient http;
bool returnVal = true;
if (!http.begin("https://kapi.kakao.com/v1/user/access_token_info")) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Authorization", "Bearer " + access_token);
int httpCode = http.GET();
// httpCode will be negative on error
if (httpCode > 0) {
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(payload);
String expireT = extract_string(payload, "\"expires_in\":", ",");
Serial.println(expireT.toInt());
if (expireT.toInt() > 0) {
returnVal = false;
} else {
returnVal = true;
}
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
return returnVal;
}
void send_message() {
HTTPClient http;
String url = "https://kapi.kakao.com/v2/api/talk/memo/default/send";
if (!http.begin(url)) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Authorization", "Bearer " + access_token);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int http_code;
String data = String("template_object={") +
String("\"object_type\": \"text\",") +
String("\"text\": \"") + String("토양 센서 값 :") +
String(sensorValue) + // 토양 센서 값
String("\",\"link\": {}}"); // link가 없으면 오류메세지 받음
Serial.println(data);
http_code = http.POST(data);
Serial.print("HTTP Response code: ");
Serial.println(http_code);
String response;
if (http_code > 0) {
response = http.getString();
Serial.println(response);
}
http.end();
}
bool update_access_token() {
HTTPClient http;
bool retVal = false;
String url = "https://kauth.kakao.com/oauth/token";
String new_refresh_token = "";
if (!http.begin(url)) {
Serial.println("\nfailed to begin http\n");
}
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
int http_code;
String data = "grant_type=refresh_token&client_id=" + rest_api_key + "&refresh_token=" + refresh_token;
Serial.println(data);
http_code = http.POST(data);
Serial.print("HTTP Response code: ");
Serial.println(http_code);
String response;
if (http_code > 0) {
response = http.getString();
Serial.println(response);
access_token = extract_string(response, "{\"access_token\":\"", "\"");
new_refresh_token = extract_string(response, "\"refresh_token\":\"", "\"");
if (new_refresh_token != "") {
refresh_token = new_refresh_token;
}
retVal = true;
} else {
retVal = false;
}
http.end();
return retVal;
}
- 토양 센서 값이 일정한 시간 간격으로 카카오톡 메세지로 전송되는 것을 확인할 수 있다. 쉬운 확인을 위하여 10초 간격으로 알림이 오게끔 변경하였다.
토양 센서 모듈로 값을 읽어올 뿐만 아니라, 평소 많은 사람들이 실제로 사용하는 카카오톡 앱을 이용하여 값을 확인할 수 있다는 것이 매우 흥미롭다. 현재 카카오 맵 API를 이용하는 웹을 개발 중인데, 이때보다 본 실습에서 OAuth를 통해 토큰을 발급 받는 것에 다소 어려움이 있었다. 그러나 이런 경험을 통해서 성공적으로 실습을 마무리할 수 있었고, 현재 개발 중인 웹에도 값을 카카오톡으로 읽어올 수 있도록 하는 방법을 적용해 볼 예정이다. 매우 도움이 되는 실습이었고 재미있었다.