본문 바로가기
카테고리 없음

[IOT 연습] Esp8266와 조도 센서를 활용한 감시 장치 만들기.

by newbeverse 2025. 3. 22.

목차

  1. 예시 상황
  2. 원리
  3. 느낀 점

 

예시 상황

1. 슬금슬금 회사의 기밀이 담긴 USB를 찾기위해 주인 A의 방에 침입하는 수상한 사람...!

1


2. 침입까진 성공했지만, 주인 A씨의 방이 매우 더럽다. 게다가 너무 어둡다. 어쩔수 없이 살며시 불을 키게 되는데...!?

2


3. 마우스 (조도센서가 내장된)  에서는 빛을 측정하고 있음 ▶ 조도 값을 서버에 전달함 ▶ 주인이 외부에서 바뀐 데이터값을 확인 가능함.  따라서 주인은 스마트폰을 보고 조도 값을 확인 한뒤 바로 경호팀에 연락을 한다..

3


4. 결국 경호팀은 수상한 사람 체포에 성공합니다.

 

원리

준비물 : 마우스에 ESP8266와 조도센서를 내장한 마우스. (왼쪽 사진)

회로도 : (오른쪽 사진. 어렵지않아요! "아두이노 조도센서" 로 검색하시면 많은 자료를 참고 가능합니다.)

ESP8266 이란?

  • 아두이노에 WIFI 기능과 BLE(단거리 블루투스) 기능이 추가된 보드라고 생각하면 쉽습니다. 즉, 아두이노와 동일한 방식으로 코딩이 가능하므로 쉽습니다.
  • 저렴하기때문에 많은 학생들이 IOT 프로젝트에서 사용합니다. 값싸지만 충분히 좋은 보드입니다.

 

Arduino IDE에서의 ESP8266 코드 

1. 저와 여러분 모두의 시간은 소중하므로, 핵심만 설명하겠습니다. 추가 질문은 GPT에게 물어보시는걸 권장하며, 답글을 달아주셔도 답변해드리겠습니다.

  • ssid 에 와이파이를 입력
  • password에 비밀번호를 입력
  • serverAddress에 서버 주소를 입력
  • 조도센서가 연결된 A0 핀에서, 조도센서 값을 계속 읽어옵니다. 그 결과값을 analogPin 에 저장합니다. 이 결과값이 보정값을 거치면 우리가 알아볼수 있도록 0~1023 사이의 디지털 값으로 변환됩니다. 이 값이 임계값을 넘어서면 불이 켜진 상태이며, 그 이하는 불이 꺼져있다고 판단할 수 있습니다.
  • 5초에 한번 데이터를 서버에 전송합니다.
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>

const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
const char* serverAddress = "http://YOUR_IP_ADRESS:YOUR_PORT/data"; // Change to your server IP

const int analogPin = A0;  // ESP8266 analog pin

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected to WiFi");
}

void loop() {
  if (WiFi.status() == WL_CONNECTED) {
    // Read analog sensor
    int sensorValue = analogRead(analogPin);
    
    // Create HTTP client
    WiFiClient client;
    HTTPClient http;
    
    // Prepare data
    String data = "value=" + String(sensorValue);
    
    // Send POST request
    http.begin(client, serverAddress);
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    
    int httpCode = http.POST(data);
    
    if (httpCode > 0) {
      Serial.printf("HTTP Response: %d\n", httpCode);
    }
    
    http.end();
  }
  
  delay(5000); // Wait 5 seconds before next reading
}

 

AWS에서의 Node.js 코드 (서버)

  • 데이터 베이스(?)가 따로 없는 간단한 방식입니다.
  • Esp8266이 조도센서 값을 이 서버의 IP 포트로 전송하면, 값을 저장하고 사용자가 보고있는 HTML 코드의 데이터를 업데이트 합니다.
const express = require('express');
const path = require('path');
const app = express();
const port = 3000;

// Store sensor readings in memory
let sensorData = [];

// Middleware to parse POST data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));

// Serve static files from 'public' directory
app.use(express.static('public'));

// API endpoint to receive sensor data
app.post('/data', (req, res) => {
    const value = req.body.value;
    const timestamp = Date.now();
    
    sensorData.push({ timestamp, value });
    
    // Keep only last 288 readings (24 hours worth of data at 5-minute intervals)
    if (sensorData.length > 10000) {
        sensorData.shift();
    }
    
    res.send('Data received!');
});

// API endpoint to get sensor data
app.get('/data', (req, res) => {
    res.json(sensorData);
});

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});

// Start server
app.listen(port, "0.0.0.0", () => {
    console.log(`Server running at http://0.0.0.0:${port}`);
});

 

 

 

HTML (사용자가 볼 수 있는.)

  • 조도센서의 값을 그래프로 그리고있습니다.
  • 실험을 통해 다음과 같은 사실을 파악합니다.
    • 불이 on 되었을때, 조도센서값이 25 이상
    • 불이 off 되었을때, 조도센서값이 25 미만
    • 이를 기반으로 방의 on/off를 파악합니다.

<!DOCTYPE html>
<html>
<head>
    <title>This is a Data!</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
    <style>
        .loading {
            opacity: 0.7;
            pointer-events: none;
        }
        
        #errorMessage {
            color: #ff4444;
            background: #ffebee;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
            display: none;
        }
        
        #loadingIndicator {
            display: none;
            color: #666;
            margin: 10px 0;
        }

        #dataCount {
            color: #666;
            margin: 10px 0;
            font-size: 0.9em;
        }

        .date-range-controls {
            margin: 15px 0;
            display: flex;
            gap: 15px;
            align-items: center;
            flex-wrap: wrap;
        }

        .date-input-group {
            display: flex;
            align-items: center;
            gap: 5px;
        }

        input[type="datetime-local"] {
            padding: 5px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }

        button {
            padding: 5px 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        button:hover {
            background-color: #45a049;
        }

        .preset-buttons {
            display: flex;
            gap: 10px;
            margin: 10px 0;
        }

        .preset-button {
            padding: 5px 10px;
            background-color: #008CBA;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        .preset-button:hover {
            background-color: #007399;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>ESP8266/ESP32 Sensor Monitor</h1>
        
        <div id="loadingIndicator">Loading data...</div>
        <div id="errorMessage"></div>
        
        <div id="sensorValue">Current Value: --</div>
        <div id="lastUpdate">Last Update: --</div>
        <div id="dataCount">Total Data Points: --</div>
        
        <div class="date-range-controls">
            <div class="date-input-group">
                <label for="startDate">시작:</label>
                <input type="datetime-local" id="startDate">
            </div>
            <div class="date-input-group">
                <label for="endDate">종료:</label>
                <input type="datetime-local" id="endDate">
            </div>
            <button onclick="applyDateRange()">적용</button>
        </div>

        <div class="preset-buttons">
            <button class="preset-button" onclick="setLastHours(1)">최근 1시간</button>
            <button class="preset-button" onclick="setLastHours(6)">최근 6시간</button>
            <button class="preset-button" onclick="setLastHours(24)">최근 24시간</button>
            <button class="preset-button" onclick="setLastDays(7)">최근 7일</button>
            <button class="preset-button" onclick="setLastDays(30)">최근 30일</button>
        </div>
        
        <div class="controls">
            <label for="updateInterval">Update Every:</label>
            <select id="updateInterval" onchange="handleIntervalChange()">
                <option value="1000">1 second</option>
                <option value="5000" selected>5 seconds</option>
                <option value="10000">10 seconds</option>
                <option value="30000">30 seconds</option>
                <option value="60000">1 minute</option>
            </select>

            <label for="decimation">Data Decimation:</label>
            <select id="decimation" onchange="handleDecimationChange()">
                <option value="1">None</option>
                <option value="2">1/2 points</option>
                <option value="4">1/4 points</option>
                <option value="8">1/8 points</option>
            </select>
        </div>
        
        <div class="chart-container">
            <canvas id="sensorChart"></canvas>
        </div>
    </div>

    <script>
        let chart;
        let updateInterval = 5000;
        let updateTimer;
        let decimationFactor = 1;
        let rawData = [];
        let startDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // 기본값: 24시간 전
        let endDate = new Date();
        
        function initChart() {
            const ctx = document.getElementById('sensorChart').getContext('2d');
            // ... (차트 초기화 코드는 이전과 동일)
        }

        function setLastHours(hours) {
            const end = new Date();
            const start = new Date(end - hours * 60 * 60 * 1000);
            
            document.getElementById('startDate').value = formatDateTimeLocal(start);
            document.getElementById('endDate').value = formatDateTimeLocal(end);
            
            startDate = start;
            endDate = end;
            updateChartFromRawData();
        }

        function setLastDays(days) {
            const end = new Date();
            const start = new Date(end - days * 24 * 60 * 60 * 1000);
            
            document.getElementById('startDate').value = formatDateTimeLocal(start);
            document.getElementById('endDate').value = formatDateTimeLocal(end);
            
            startDate = start;
            endDate = end;
            updateChartFromRawData();
        }

        function formatDateTimeLocal(date) {
            return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
                .toISOString()
                .slice(0, 16);
        }

        function applyDateRange() {
            const start = document.getElementById('startDate').value;
            const end = document.getElementById('endDate').value;
            
            if (start && end) {
                startDate = new Date(start);
                endDate = new Date(end);
                updateChartFromRawData();
            } else {
                showError('시작 날짜와 종료 날짜를 모두 선택해주세요.');
            }
        }

        function handleIntervalChange() {
            const intervalSelect = document.getElementById('updateInterval');
            updateInterval = parseInt(intervalSelect.value);
            
            clearInterval(updateTimer);
            updateTimer = setInterval(fetchData, updateInterval);
        }

        function handleDecimationChange() {
            const decimationSelect = document.getElementById('decimation');
            decimationFactor = parseInt(decimationSelect.value);
            updateChartFromRawData();
        }

        function formatDateTime(timestamp) {
            const date = new Date(timestamp);
            const month = (date.getMonth() + 1).toString().padStart(2, '0');
            const day = date.getDate().toString().padStart(2, '0');
            const hours = date.getHours().toString().padStart(2, '0');
            const minutes = date.getMinutes().toString().padStart(2, '0');
            const seconds = date.getSeconds().toString().padStart(2, '0');
            
            return `${month}/${day} ${hours}:${minutes}:${seconds}`;
        }

        function decimateData(data, factor) {
            if (factor === 1) return data;
            return data.filter((_, index) => index % factor === 0);
        }

        function updateChartFromRawData() {
            try {
                // 날짜 범위로 데이터 필터링
                const filteredData = rawData.filter(reading => {
                    const timestamp = new Date(reading.timestamp);
                    return timestamp >= startDate && timestamp <= endDate;
                });
                
                const decimatedData = decimateData(filteredData, decimationFactor);
                
                const labels = decimatedData.map(reading => 
                    formatDateTime(reading.timestamp)
                );
                
                const values = decimatedData.map(reading => 
                    parseFloat(reading.value));
                
                chart.data.labels = labels;
                chart.data.datasets[0].data = values;
                chart.update('none');

                document.getElementById('dataCount').textContent = 
                    `선택된 기간 데이터: ${filteredData.length}개 (표시: ${decimatedData.length}개)`;
            } catch (error) {
                showError('차트 업데이트 오류: ' + error.message);
            }
        }

        async function fetchData() {
            const loadingIndicator = document.getElementById('loadingIndicator');
            const errorMessage = document.getElementById('errorMessage');
            
            try {
                loadingIndicator.style.display = 'block';
                errorMessage.style.display = 'none';
                
                const response = await fetch('/data');
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const data = await response.json();
                
                if (data.length > 0) {
                    rawData = data;
                    const latest = data[data.length - 1];
                    
                    document.getElementById('sensorValue').textContent = 
                        `Current Value: ${parseFloat(latest.value).toFixed(0)}`;
                    document.getElementById('lastUpdate').textContent = 
                        `Last Updated: ${new Date(latest.timestamp).toLocaleString()}`;
                    
                    updateChartFromRawData();
                }
            } catch (error) {
                showError('데이터 가져오기 오류: ' + error.message);
            } finally {
                loadingIndicator.style.display = 'none';
            }
        }
        
        function showError(message) {
            const errorElement = document.getElementById('errorMessage');
            errorElement.textContent = message;
            errorElement.style.display = 'block';
            
            setTimeout(() => {
                errorElement.style.display = 'none';
            }, 5000);
        }

        function initialize() {
            // 초기 날짜 설정
            document.getElementById('startDate').value = formatDateTimeLocal(startDate);
            document.getElementById('endDate').value = formatDateTimeLocal(endDate);
            
            initChart();
            fetchData();
            
            updateTimer = setInterval(fetchData, updateInterval);
            
            document.addEventListener('visibilitychange', () => {
                if (document.hidden) {
                    clearInterval(updateTimer);
                } else {
                    fetchData();
                    updateTimer = setInterval(fetchData, updateInterval);
                }
            });
        }
        
        initialize();
    </script>
</body>
</html>

 

느낀 점

  • 회고
    • 일회성 프로젝트라도 깃허브를 잘 관리하자. 언젠가 쓰이며(오늘과 같은 블로그작성시) 데이터가 정확한지 파악이 안되면서 귀찮아진다.
    • aws 서버에 의존하지말고, 개인 서버를 갖추자. 프리티어 서버로 인해 프로젝트가 단기적으로 끝나게된다.. --> 라즈베리 파이로 소규모 iot 서버를 만들고 이를 장기간 관리해보자.
  • 목표
    • 유용한 iot 서비스가 뭐가 있을지 틈틈히 고민해보자
반응형