목차
- 예시 상황
- 원리
- 느낀 점
예시 상황
1. 슬금슬금 회사의 기밀이 담긴 USB를 찾기위해 주인 A의 방에 침입하는 수상한 사람...!
2. 침입까진 성공했지만, 주인 A씨의 방이 매우 더럽다. 게다가 너무 어둡다. 어쩔수 없이 살며시 불을 키게 되는데...!?
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 서비스가 뭐가 있을지 틈틈히 고민해보자
반응형