10주차 ‐ Firebase를 활용한 ESP32 IoT 프로젝트 - gitjs523/SmartDevice2025 GitHub Wiki

1. Firebase 개요

(1) Firebase 소개

  • Google이 제공하는 모바일 및 웹 애플리케이션 개발 플랫폼
  • 서버 없이 빠르게 앱 개발
  • 실시간으로 기능 확장 가능
  • 주요 특징:
    • 장기간 데이터 보존 및 분석 가능
    • 여러 디바이스 간 실시간 데이터 공유 가능
    • 데이터 분석하여 추세 파악 및 장치 성능 개선
    • 데이터 보안 및 백업으로 안전하고 안정적인 서비스 제공
    • BaaS(Backend-as-a-Service): 서버 구축 없이 데이터 저장, 인증, 호스팅 등 백엔드 기능 제공
    • 실시간 데이터 처리: 데이터 변경을 클라이언트에 실시간으로 반영
    • 크로스 플랫폼 지원: Android, iOS, 웹 등 다양한 플랫폼 지원
    • 구글 생태계와의 통합: Google Cloud, Analytics, AdMob 등과 손쉽게 연동
    • Firebase Hosting을 통해 전 세계 어디서나 접근 가능

(2) Firebase와 IoT(사물인터넷)

  • Firebase는 IoT 데이터 수집 및 시각화에 매우 적합함
    • 적용 예시: ESP32의 센서 데이터 실시간 저장 및 웹 차트로 시각화
  • 복잡한 서버 인프라 없이도 실시간 모니터링 앱 구축 가능

2. Firebase를 활용한 실습

사전 준비: Firebase 프로젝트 설정

  1. Firebase 홈페이지 접속하여 계정 로그인
  2. ‘프로젝트 만들기’ 클릭
  3. 프로젝트 이름 설정 ("이 프로젝트에서 Google 애널리틱스 사용 설정" 하지 않음)
  4. 빌드 > Realtime Database 섹션 이동 후, "데이터베이스 만들기"를 클릭
  5. 데이터베이스 서버의 위치를 설정하고 테스트 모드로 사용 설정
  6. 데이터베이스의 URL을 확인 후 Database의 읽기, 쓰기 규칙을 설정

image

  1. 빌드 → Authentication 섹션으로 이동, 시작하기를 클릭
  2. 익명(Anonymous) 방식을 선택하고 사용 설정 On
  3. 프로젝트 설정으로 이동. 탭에서 웹 API키를 확인 후 복사
  4. 아두이노에서 라이브러리 “Firebase ESP32 Client (by Mobizt)”를 검색하고 설치

(1) ESP 코드 구현 (LED 기본 제어) 실습

  • Firebase 프로젝트를 만들어 사전 준비한 것을 토대로 실습 시작
  • 제공 받은 코드를 이용한 기본적인 LED 제어 실습
  • 프로젝트의 'Realtime Database'에서 'ledState' 값에 0을 넣으면 파란색 내장 LED가 꺼지고, 1을 넣으면 LED가 켜진다.

사용한 코드

#include <WiFi.h>
#include <FirebaseESP32.h>

//Provide the token generation process info.
#include "addons/TokenHelper.h"
//Provide the RTDB payload printing info and other helper functions.
#include "addons/RTDBHelper.h"

// Firebase 설정
#define FIREBASE_HOST "your_firebase_url"  // url 주소를 입력
#define FIREBASE_AUTH "your_firebase_api"   // API 키 값을 입력
#define WIFI_SSID "your_wifi_ssid"
#define WIFI_PASSWORD "your_wifi_password"

// 내장 LED 핀 설정
#define LED_PIN 2

// Firebase 객체 생성
FirebaseData firebaseData;

FirebaseAuth auth;
FirebaseConfig config;
bool signupOK = false;
unsigned long sendDataPrevMillis = 0;

void setup_wifi() {
  delay(10);
  // Wi-Fi 네트워크에 연결 시작
  Serial.println();
  Serial.print("연결 중인 Wi-Fi: ");
  Serial.println(WIFI_SSID);

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("Wi-Fi 연결됨");
  Serial.println("IP 주소: ");
  Serial.println(WiFi.localIP());
}

void setup() {
  // 시리얼 통신 초기화
  Serial.begin(115200);

  // WiFi 연결
  setup_wifi();

  // API 키를 할당합니다.(필수)
  config.api_key = FIREBASE_AUTH;

  //RTDB URL을 할당합니다.(필수)
  config.database_url = FIREBASE_HOST;

  /* Sign up */
  if (Firebase.signUp(&config, &auth, "", "")){
    Serial.println("ok");
    signupOK = true;
  }
  else{
    Serial.printf("%s\n", config.signer.signupError.message.c_str());
  }

  // 긴 실행 시간이 필요한 토큰 생성 작업을 위한 콜백 함수를 할당합니다.
  config.token_status_callback = tokenStatusCallback; 

  Firebase.begin(&config, &auth);
  Firebase.reconnectWiFi(true);
  // 내장 LED 핀 설정
  pinMode(LED_PIN, OUTPUT);

  // "/ledState" 경로를 0으로 초기화
  if (Firebase.ready() && signupOK)
  {
    if (Firebase.setInt(firebaseData, "/ledState", 0)){
      Serial.println("PASSED");
      Serial.println("PATH: " + firebaseData.dataPath());
      Serial.println("TYPE: " + firebaseData.dataType());
    }
    else {
      Serial.println("FAILED");
      Serial.println("REASON: " + firebaseData.errorReason());
    }
  }
}
void loop() {
  // Firebase.ready() 함수는 인증 작업을 처리하기 위해 반복적으로 호출되어야 합니다.
  if (Firebase.ready() && signupOK && \
  (millis() - sendDataPrevMillis > 2000 || sendDataPrevMillis == 0))
  {
    sendDataPrevMillis = millis();

    // Firebase에서 LED 상태 가져오기
    int ledState = 0;
    if (Firebase.getInt(firebaseData, "/ledState")) {
      if (firebaseData.dataType() == "int") {
        ledState = firebaseData.intData();
        Serial.println(ledState);
        if (ledState == 1) {
          digitalWrite(LED_PIN, HIGH);
        } else {
          digitalWrite(LED_PIN, LOW);
        }
      }
    }
    else {
        Serial.println(firebaseData.errorReason());
    }
  }
}

실행결과

20250525_182335.mp4

(2) 빛 감지 센서 값 수집 및 로깅 (조도 센서로 빛 감지) 실습

  • 조도 센서를 이용한 실습
  • 조도 센서가 감지한 값이 시리얼 모니터에 출력됨.

사용한 코드

int sensorPin = 34;
int value = 0;

void setup() {
  Serial.begin(115200);
}

void loop() {
  value = analogRead(sensorPin);

  Serial.println(value);

  delay(500);
}

실행 결과

20250528_013906.mp4

image

(3) 빛 감지 센서 값 수집 및 로깅(타임스탬프를 기반으로하여 데이터 베이스에 데이터로깅)

  • Firebase에서 'ESP32-LIGHT-SENSOR'라는 이름의 새로운 프로젝트를 만들기
  • 프로젝트 만든 후 실습1 때와 다른 새로운 url 경로와 api key 값 이용하여 실습

사용한 코드

#include <WiFi.h>
#include <FirebaseESP32.h>

// 토큰 생성 프로세스 정보 제공
#include "addons/TokenHelper.h"
// RTDB 페이로드 출력 정보 및 기타 도움 함수 제공
#include "addons/RTDBHelper.h"

// Firebase 설정
#define FIREBASE_HOST "your_firebase_host"  
#define FIREBASE_AUTH "your_firebase_auth"  
#define WIFI_SSID "your_wifi_ssid"
#define WIFI_PASSWORD "your_wifi_password"


// Firebase 객체 정의
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
bool signupOK = false;

// 데이터베이스 주 경로
String databasePath = "/room1";
String sensorPath = "/lightsensor";
String timePath = "/timestamp";

/// 부모 노드 (현재 시간 정보로 매 루프마다 업데이트)
String parentPath;

int timestamp;
FirebaseJson json;

const char* ntpServer = "pool.ntp.org";

// 빛 센서 핀
int sensorPin = 34;

// 타이머 변수 (새로운 측정 값을 20초마다 전송)
unsigned long sendDataPrevMillis = 0;
unsigned long timerDelay = 60000;  //1 minutes

// WiFi 초기화
void setup_wifi() {
  delay(10);
  // Wi-Fi 네트워크에 연결 시작
  Serial.println();
  Serial.print("연결 중인 Wi-Fi: ");
  Serial.println(WIFI_SSID);

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("Wi-Fi 연결됨");
  Serial.println("IP 주소: ");
  Serial.println(WiFi.localIP());
}
// 현재 epoch 시간을 가져오는 함수
unsigned long getTime() {
  time_t now;
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    //Serial.println("Failed to obtain time");
    return (0);
  }
  time(&now);
  return now;
}

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

  // WiFi 연결
  setup_wifi();
  configTime(0, 0, ntpServer);

  // API 키 할당 (필수)
  config.api_key = FIREBASE_AUTH;

  // RTDB URL 할당 (필수)
  config.database_url = FIREBASE_HOST;

  Firebase.reconnectWiFi(true);
  fbdo.setResponseSize(4096);

  /* 회원 가입 */
  if (Firebase.signUp(&config, &auth, "", "")) {
    Serial.println("ok");
    signupOK = true;
  } else {
    Serial.printf("%s\n", config.signer.signupError.message.c_str());
  }

  // 장기 실행 토큰 생성 작업에 대한 콜백 함수 할당
  config.token_status_callback = tokenStatusCallback;

  // 토큰 생성 최대 재시도 횟수 할당
  config.max_token_generation_retry = 5;

  // Firebase 인증 및 설정과 함께 라이브러리 초기화
  Firebase.begin(&config, &auth);
}

void loop() {

  // Send new readings to database
  if (Firebase.ready() && signupOK
      && (millis() - sendDataPrevMillis > timerDelay
          || sendDataPrevMillis == 0)) {
    sendDataPrevMillis = millis();

    // 현재 타임스탬프 가져오기
    timestamp = getTime();
    Serial.print("time: ");
    Serial.println(timestamp);

    parentPath = databasePath + "/" + String(timestamp);

    json.set(sensorPath.c_str(), String(analogRead(sensorPin)));
    json.set(timePath, String(timestamp));
    Serial.println("Set json... ");
    if (Firebase.setJSON(fbdo, parentPath.c_str(), json))
      Serial.println("ok");
    else
      Serial.println(fbdo.errorReason());
  }
}

실행 결과

  • "unsigned long timerDelay = 60000; //1 minutes" 본문 코드 중 이 코드로 인해 새로운 노드가 생기는 주기가 1분인 것으로 보인다.
  • 특정 시간마다 room1 폴더에 새로운 노드가 생긴다.

image

image

(4) 웹 앱 구현 및 데이터 시각화

  • 시간대별로 저장된 빛 센서의 변화량을 실시간으로 표시해주는 웹 앱을 만드는 실습
  • 코드는 제공 받은 index.html 파일을 실행하여 나온 웹페이지에서 'CTRL + U' 키를 누르면 페이지의 원본이 나오는데 거기서 코드를 확인할 수 있다.

실습 과정

  1. 앱 설정 진입해서 앱 이름을 정하고 앱 등록(Register app) 버튼 클릭한다.
  2. "<script> 태그 사용" 선택 후 아래에 있는 코드 script 카피한다.
  3. 제공 받은 index.html 파일을 마이크로소프트 엣지나 구글 크롬 등으로 실행하여 나오는 페이지에서 'Ctrl + U'키로 웹페이지 원본 코드를 확보한다.
  • 그래프가 그려져 있지 않은 원본 페이지

image

image

  1. 메모장이나 기타 편집기에서 html 코드를 작성한다. 이때 웹페이지 원본 코드에 과정 '2번'에서 앱 등록 시 복사한 스크립트 코드의 정보를 집어넣는다.
  2. 작성한 코드를 index.html로 저장한다.
  3. 저장한 index.html을 실행하면 그래프가 그려져 있다.

웹페이지 코드(복사한 스크립트 코드에 있는 데이터베이스 url, api key, 프로젝트 Id 등의 값을 이 원본 코드에 대입해야 한다.)

<html>
<body>
<!--StartFragment-->
<!DOCTYPE html>
--
  | <html lang="en">
  | <head>
  | 
  | <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
  | <meta http-equiv="Pragma" content="no-cache" />
  | <meta http-equiv="Expires" content="0" />
  | <title>ESP 데이터 기록 Firebase 앱</title>
  | <style>
  | #chart_div {
  | width: 1200px;
  | height: 500px;
  | }
  |  
  | #gauge_div {
  | height: 300px;
  | }
  |  
  | div {
  | display: table;
  | margin-right: auto;
  | margin-left: auto;
  | }
  | </style>
  | <!-- Firebase SDK 포함 -->
  | <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
  |  
  | <!-- 필요한 Firebase 기능만 포함 -->
  | <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-auth.js"></script>
  | <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-database.js"></script>
  |  
  | <script>
  | // 웹 앱의 Firebase 구성
  | const firebaseConfig = {
  | apiKey: "API 키",
  | authDomain: "esp32-light-sensor-49d0d.firebaseapp.com",
  | databaseURL: "https://esp32-light-sensor-49d0d-default-rtdb.firebaseio.com",
  | projectId: "esp32-light-sensor-49d0d",
  | storageBucket: "esp32-light-sensor-49d0d.appspot.com",
  | messagingSenderId: "473819592135",
  | appId: "1:473819592135:web:f839002c4c51d1daa4d5e2"
  | };
  |  
  | // Firebase 초기화
  | firebase.initializeApp(firebaseConfig);
  |  
  | // 인증과 데이터베이스 참조 생성
  | const auth = firebase.auth();
  | const db = firebase.database();
  | </script>
  | </head>
  | <body>
  | <!-- 차트를 위한 컨테이너 -->
  | <div>
  | <div id="chart_div"></div>
  | </div>
  | <div>
  | <div id="gauge_div"></div>
  | </div>
  | <script type="text/javascript"
  | src="https://www.gstatic.com/charts/loader.js"></script>
  | <script>
  | // 현재 차트 패키지 로드
  | google.charts.load('current', {
  | packages: ['corechart', 'line', 'gauge'],
  | });
  | // API가 로드되었을 때 콜백 함수 설정
  | google.charts.setOnLoadCallback(drawChart);
  |  
  | function drawChart() {
  | // 기본 값으로 데이터 객체 생성
  | let data = google.visualization.arrayToDataTable([
  | ['Time', 'Light Sensor'],
  | ["00:00", 0],
  | ]);
  |  
  | let gauge_data = google.visualization.arrayToDataTable([
  | ['Light Sensor'],
  | [0],
  | ]);
  |  
  | // 제목, 색상 등이 포함된 옵션 객체 생성
  | let options = {
  | max: 2048, //4096,
  | hAxis: {
  | //textPosition: 'none',
  | },
  | vAxis: {
  |  
  | },
  | };
  |  
  | // 차트 그리기
  | let chart = new google.visualization.LineChart(
  | document.getElementById('chart_div')
  | );
  | chart.draw(data, options);
  |  
  | let gauge_chart = new google.visualization.Gauge(
  | document.getElementById('gauge_div')
  | );
  | gauge_chart.draw(gauge_data, options);
  |  
  | // 데이터베이스 경로
  | var dbPath = 'room1';
  |  
  | // 데이터베이스 참조
  | var dbRef = firebase.database().ref(dbPath);
  |  
  | // 표시할 최대 데이터 행 수
  | let maxDatas = 50;
  |  
  | // 최신 측정값 가져와서 차트에 표시 (표시되는 측정값 수는 chartRange 값에 해당)
  | dbRef.orderByKey().limitToLast(maxDatas).on('child_added', snapshot =>{
  | var jsonData = snapshot.toJSON(); // 예: {lightsensor: 2502, timestamp:1641317355}
  |  
  | // 값 저장
  | var lightsensor = Number(jsonData.lightsensor);
  | var timestamp = epochToDateTime(jsonData.timestamp);
  |  
  | // 차트에 값 표시
  | if (data.getNumberOfRows() > maxDatas) {
  | data.removeRows(0, data.getNumberOfRows() - maxDatas);
  | }
  | data.addRow([timestamp, lightsensor]);
  | chart.draw(data, options);
  |  
  | gauge_data.setValue(0, 0, lightsensor);
  | gauge_chart.draw(gauge_data, options);
  | });
  | }
  | // 에포크 Time을 JavaScript Date 객체로 변환
  | function epochToJsDate(epochTime){
  | return new Date(epochTime*1000);
  | }
  |  
  | // Time을 사람이 읽을 수 있는 형식 (HH:MM)으로 변환
  | function epochToDateTime(epochTime) {
  | var epochDate = new Date(epochToJsDate(epochTime));
  | var dateTime =
  | ("00" + epochDate.getHours()).slice(-2) +
  | ":" +
  | ("00" + epochDate.getMinutes()).slice(-2);
  | return dateTime;
  | }
  | </script>
  | </body>
  | </html>

<!--EndFragment-->
</body>
</html>

실행 결과

  • 수집 된 데이터가 그래프로 성공적으로 그려졌다.

image

(5) Firebase Hosting을 통한 웹 배포

  • 웹사이트를 호스팅하여 어디서든 접속 및 조회할 수 있도록 하는 실습

실습 과정

  1. 'Firebase CLI'를 다운로드 한다.(C 드라이브의 영문 폴더에 다운로드)
  2. 다운 받은 후 실행하면 창이 뜨고 로그인 메시지가 발생한다. 구글 계정으로 로그인 한다.

image

image

  • 로그인 성공한 모습

image

  1. 로그인이 완료되면 pdf 파일에 나온 명령어를 실행한다.
  • 명령어에 대한 설명

image

  • 명령어 실행

image

  1. 'firebase projects:list' 명령어 실행하여 프로젝트 리스트를 확인한다.

image

  1. 'firebase init' 명령어를 실행하여 프로젝트 및 'hosting' 설정을 진행한다.(방향키로 선택지를 고르고, 스페이스바로 원하는 항목을 체크하고, 엔터키로 확정한다.)

image

image

  1. 'hosting'을 선택한 후, "Use an existing project"를 선택한다. 이후 "ESP32_LIGHT-SENSOR"를 선택한다.

image

image

  1. 이후 나오는 3개의 질문은 강의 자료에 나온 대로 답한다.

image

  1. 아까 진행한 4번째 실습인 "웹 앱 구현 및 데이터 시각화"를 진행할 때 작성한 index.html을 'd:\project\public' 폴더로 복사한다.(복사할 때 파일 덮어쓰기)
  2. 'firebase deploy' 명령어 사용하여 웹 앱 호스팅을 시작한다.(아래 이미지에 나오는 Hosting URL의 주소로 해당 웹 페이지에 접속한다.)

image

실행 결과

  • CLI 화면에 표시된 호스팅 URL을 브라우저에 입력해 주면 세계 어디서나 ESP32에서 보내는 빛 센서 값을 모니터링 할 수 있다.
  • 노트북에서 구글 크롬으로 접속한 이미지

image

  • 모바일로 접속한 이미지

Screenshot_20250529_021834_Chrome

⚠️ **GitHub.com Fallback** ⚠️