RD-03D 및 ESP32-C6을 이용한 레이더 기반 홈 시큐리티 시스템
소개
전통적인 홈 보안 시스템은 PIR(수동 적외선) 센서에 의존하는 경우가 많지만, 애완동물, 움직이는 커튼, 온도 변화 등에 의해 발생하는 오경보 문제가 존재합니다. 또한 탐지 범위가 제한적이며, 느리게 움직이는 침입자나 두꺼운 옷을 입은 사람을 감지하는 데 어려움을 겪습니다.
RD-03D mmWave 레이더 센서와 Beetle ESP32-C6을 결합하면 더 정밀하고 스마트한 보안 시스템을 구축할 수 있습니다. 이 시스템은 실시간 침입자 감지, 스마트 경고, 레이더 시각화를 제공하며 Home Assistant와 완벽하게 통합됩니다.
PIR 문제
- 거짓 긍정: 애완 동물, 커튼, 온도 변화 및 햇빛으로 인해 지속적인 오경보가 발생합니다
- 제한된 범위: 대부분의 PIR 센서는 3-5미터 이내에서만 효과적으로 작동합니다.
- 좁은 감지: 일반적인 90° 시야각은 사각지대를 남깁니다.
- 날씨 민감도: 온도 변화는 성능에 큰 영향을 미칩니다.
- 패배하기 쉬움: 느린 움직임이나 열 차폐로 우회할 수 있습니다.
주요 기능들
- 레이더 기반 탐지(8m 범위, 120° 필드)
- 실시간 알림(LED + 부저)
- Web UI(ESP32에서 호스팅됨)
- 실시간 레이더 시각화
- 배터리로 구동되며 충전 가능(Type-C)
- 웹 인터페이스를 통한 임계값 구성
- Matter, Thread 및 Zigbee 지원
- Home Assistant 호환(MQTT/REST 통합 옵션)
부품 리스트
ESP32-C6 비틀 | 마이크로컨트롤러 |
RD-03D | 24GHz mmWave 레이더 센서 |
3.7V 1500mAh LiPo 배터리 | 충전식 배터리 |
RGB LED | 시각적 알림 |
슬라이드 스위치 | 전원 관리 |
전선 및 커넥터 | 배선 |
3D 프린팅 인클로저 | 방수 설계 |
주요 기능
- 레이더 기반 탐지 (8m 범위, 120° 시야각)
- 실시간 알림 (LED 및 부저)
- 웹 UI (ESP32에서 호스팅)
- 실시간 레이더 시각화
- 배터리 충전 가능 (Type-C)
- Home Assistant 지원 (MQTT/REST)
회로 연결 방법
ESP32-C6과 RD-03D 연결:
- RD-03D TX → ESP32 RX(GPIO 17)
- RD-03D RX → ESP32 TX(GPIO 16)
- GND → GND
- VCC → 5V
RGB LED 연결:
- 빨간색 LED 양극 → GPIO 21
- 녹색 LED 양극 → GPIO 22
- 파란색 LED 양극 → GPIO 23
- 음극 → GND
배터리 연결:
- LiPo 양극 → ESP32 배터리 핀
- LiPo 접지 → ESP32 GND 핀
3D 프린팅 인클로저 제작
Fusion 360을 사용하여 설계
- RD-03D 센서용 장착 슬롯
- LED 및 부저용 구멍
- USB-C 충전 포트 컷아웃
- 스냅핏 방식 뚜껑
3D 프린팅
- 재질: PLA 또는 PETG
- 슬라이싱 소프트웨어: Cura
- 추천 레이어 높이: 0.2mm
- 프린팅 시간: 3시간 이내
후처리
- LED 및 센서 장착
- ESP32 고정 및 배선 정리
소스 코드 및 펌웨어 작성
ESP32의 펌웨어는 Arduino IDE를 사용하여 작성할 수 있습니다. RD-03D 레이더를 처리하기 위해 UART 통신을 활용합니다.
#include <SoftwareSerial.h>
SoftwareSerial radarSerial(16, 17); // RX, TX
void setup() {
Serial.begin(115200);
radarSerial.begin(115200);
}
void loop() {
if (radarSerial.available()) {
String radarData = radarSerial.readStringUntil('\n');
Serial.println("Radar Data: " + radarData);
}
}
웹 인터페이스 ESP32는 웹 서버를 호스팅하여 경보 상태를 표시하며, 사용자는 브라우저를 통해 데이터를 확인할 수 있습니다.
1단계: Autodesk Fusion 360에서 사례 설계
휴대성과 내구성을 보장하기 위해 Autodesk Fusion 360에서 소형 인클로저를 설계했습니다. 인클로저에는 다음이 포함됩니다.
1. 본문 : 여기에서 STL 파일을 다운로드합니다.

2. 상단 표지: 여기에서 STL 파일 다운로드

디자인에는 다음이 있습니다.
- RD-03D 센서용 장착 슬롯
- LED 구멍
- 버저용 구멍
- USB-C 프로그래밍 및 충전 액세스를 위한 컷아웃
- 스냅핏 뚜껑

디자인은 벽이나 천장에 장착할 수 있도록 최소한으로 컴팩트하게 유지됩니다.

2단계: 케이스 3D 프린팅
Fusion 360 설계를 STL로 내보냈습니다. FDM 프린터용 Cura를 사용하여 슬라이딩(0.2mm 레이어 높이 권장)
재질 : PLA 또는 PETGPrinted 3 시간 이내

후처리: LED를 삽입하고, 레이더를 장착하고, ESP32를 고정하고, 전선을 깔끔하게 배선합니다.
3단계: 회로 조립
ESP32-C6에 대한 레이더 센서
RD-03D TX → ESP32 RX(GPIO 17)
RD-03D RX → ESP32 TX(GPIO 16)
GND → GND
VCC → 5V
LED 표시 등
- 적색 LED 양극 → GPIO 21
- 녹색 LED 양극 → GPIO 22
- 파란색 LED 양극 → GPIO 23
- 음극에서 GND로

LiPo 배터리의 양극 단자를 Beetle ESP32C6의 배터리 핀에 납땜하고 접지 단자를 GND 핀에 PowerSolder합니다.


4단계: 펌웨어 및 웹 인터페이스
Arduino IDE를 사용하여 장치의 펌웨어를 작성하기로 선택한 이유는 단순성과 광범위한 커뮤니티 지원 때문입니다.
Arduino IDE에 ESP32-C6을 지원하는 최신 ESP32 Arduino Core가 있는지 확인하십시오. 코드를 컴파일하기 전에 이 RD-03D 라이브러리가 설치되어 있는지 확인하십시오. 그것을 다운로드하고 Arduino 라이브러리 폴더에 압축을 풉니다.
코드 개요
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
#include "RadarSensor.h"
// #include "web_interface.h"
// WiFi credentials - Update these with your network details
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// Hardware pins
const int buzzerPin = 7;
const int ledPin = 15; // Built-in LED
const int ledRPin = 21; // Red LED
const int ledGPin = 22; // Green LED
const int ledBPin = 23; // Blue LED
// Radar sensor
RadarSensor radar(Serial1);
// Web server
WebServer server(80);
// System state variables
struct SystemConfig {
bool armed = true;
int detectionDistance = 1000; // mm (1 meter default)
int alarmDuration = 10000; // ms (10 seconds default)
bool alarmActive = false;
unsigned long alarmStartTime = 0;
bool systemEnabled = true;
};
SystemConfig config;
RadarTarget currentTarget;
bool targetDetected = false;
unsigned long lastBlinkTime = 0;
bool blinkState = false;
void setup() {
Serial.begin(115200);
Serial1.begin(256000, SERIAL_8N1, 17, 16); // D7 = RX, D6 = TX
// Initialize pins
pinMode(buzzerPin, OUTPUT);
pinMode(ledPin, OUTPUT);
pinMode(ledRPin, OUTPUT);
pinMode(ledGPin, OUTPUT);
pinMode(ledBPin, OUTPUT);
// Initialize radar
radar.begin();
Serial.println("Radar Sensor Started");
// Initialize WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println();
Serial.print("Connected! IP address: ");
Serial.println(WiFi.localIP());
// Setup web server routes
setupWebServer();
server.begin();
// Initial LED state
updateLEDs();
}
void loop() {
server.handleClient();
// Handle radar updates
if (radar.update()) {
currentTarget = radar.getTarget();
targetDetected = (currentTarget.distance > 0 &&
currentTarget.distance <= config.detectionDistance);
Serial.print("Distance: "); Serial.print(currentTarget.distance);
Serial.print("mm, Angle: "); Serial.print(currentTarget.angle);
Serial.print("°, Target: "); Serial.println(targetDetected ? "YES" : "NO");
}
// Handle alarm logic
handleAlarm();
// Update LEDs and buzzer
updateLEDs();
updateBuzzer();
delay(50);
}
void handleAlarm() {
if (!config.systemEnabled || !config.armed) {
config.alarmActive = false;
return;
}
if (targetDetected && !config.alarmActive) {
// Start alarm
config.alarmActive = true;
config.alarmStartTime = millis();
Serial.println("ALARM TRIGGERED!");
}
if (config.alarmActive) {
// Check if alarm duration exceeded
if (millis() - config.alarmStartTime >= config.alarmDuration) {
config.alarmActive = false;
Serial.println("Alarm timeout - stopping");
}
}
}
void updateLEDs() {
if (!config.systemEnabled) {
// System disabled - all LEDs off
digitalWrite(ledRPin, LOW);
digitalWrite(ledGPin, LOW);
digitalWrite(ledBPin, LOW);
return;
}
if (!config.armed) {
// System disarmed - blue LED on
digitalWrite(ledRPin, LOW);
digitalWrite(ledGPin, LOW);
digitalWrite(ledBPin, HIGH);
return;
}
if (config.alarmActive) {
// Alarm active - red LED blinks
unsigned long currentTime = millis();
if (currentTime - lastBlinkTime >= 250) {
blinkState = !blinkState;
lastBlinkTime = currentTime;
}
digitalWrite(ledRPin, blinkState ? HIGH : LOW);
digitalWrite(ledGPin, LOW);
digitalWrite(ledBPin, LOW);
} else if (targetDetected) {
// Target detected but not alarming - red LED solid
digitalWrite(ledRPin, HIGH);
digitalWrite(ledGPin, LOW);
digitalWrite(ledBPin, LOW);
} else {
// Normal operation - green LED on
digitalWrite(ledRPin, LOW);
digitalWrite(ledGPin, HIGH);
digitalWrite(ledBPin, LOW);
}
}
void updateBuzzer() {
if (config.alarmActive && config.systemEnabled) {
// Buzzer blinks at same rate as red LED
digitalWrite(buzzerPin, blinkState ? HIGH : LOW);
} else {
digitalWrite(buzzerPin, LOW);
}
}
void setupWebServer() {
// Serve main HTML page
server.on("/", HTTP_GET, []() {
server.send(200, "text/html", getMainHTML());
});
// API endpoints
server.on("/api/status", HTTP_GET, handleGetStatus);
server.on("/api/config", HTTP_POST, handleSetConfig);
server.on("/api/config", HTTP_GET, handleGetConfig);
server.on("/api/arm", HTTP_POST, handleArmDisarm);
server.on("/api/stop-alarm", HTTP_POST, handleStopAlarm);
server.on("/api/radar-data", HTTP_GET, handleGetRadarData);
// Enable CORS
server.enableCORS(true);
}
void handleGetStatus() {
StaticJsonDocument<300> doc;
doc["armed"] = config.armed;
doc["alarmActive"] = config.alarmActive;
doc["targetDetected"] = targetDetected;
doc["systemEnabled"] = config.systemEnabled;
doc["detectionDistance"] = config.detectionDistance;
doc["alarmDuration"] = config.alarmDuration;
doc["uptime"] = millis();
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
void handleGetConfig() {
StaticJsonDocument<200> doc;
doc["detectionDistance"] = config.detectionDistance;
doc["alarmDuration"] = config.alarmDuration;
doc["systemEnabled"] = config.systemEnabled;
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
void handleSetConfig() {
if (server.hasArg("plain")) {
StaticJsonDocument<200> doc;
deserializeJson(doc, server.arg("plain"));
if (doc.containsKey("detectionDistance")) {
config.detectionDistance = doc["detectionDistance"];
}
if (doc.containsKey("alarmDuration")) {
config.alarmDuration = doc["alarmDuration"];
}
if (doc.containsKey("systemEnabled")) {
config.systemEnabled = doc["systemEnabled"];
}
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(400, "application/json", "{\"error\":\"Invalid request\"}");
}
}
void handleArmDisarm() {
if (server.hasArg("plain")) {
StaticJsonDocument<100> doc;
deserializeJson(doc, server.arg("plain"));
if (doc.containsKey("armed")) {
config.armed = doc["armed"];
config.alarmActive = false; // Stop any active alarm
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(400, "application/json", "{\"error\":\"Missing armed parameter\"}");
}
} else {
server.send(400, "application/json", "{\"error\":\"Invalid request\"}");
}
}
void handleStopAlarm() {
config.alarmActive = false;
server.send(200, "application/json", "{\"success\":true}");
}
void handleGetRadarData() {
StaticJsonDocument<200> doc;
doc["distance"] = currentTarget.distance;
doc["angle"] = currentTarget.angle;
doc["x"] = currentTarget.x;
doc["y"] = currentTarget.y;
doc["speed"] = currentTarget.speed;
doc["detected"] = targetDetected;
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
String getMainHTML() {
return R"(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Radar Security System</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: white;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
height: calc(100vh - 40px);
}
.panel {
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
border: 1px solid rgba(255,255,255,0.2);
}
.radar-panel {
display: flex;
flex-direction: column;
align-items: center;
}
.status {
text-align: center;
margin-bottom: 20px;
}
.status h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.status-indicator {
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
margin-left: 10px;
animation: pulse 2s infinite;
}
.armed { background: #4CAF50; }
.disarmed { background: #FF9800; }
.alarm { background: #F44336; animation: blink 0.5s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.radar-display {
width: 400px;
height: 400px;
position: relative;
margin: 20px 0;
}
.radar-svg {
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(0,255,0,0.1) 0%, rgba(0,100,0,0.05) 100%);
border-radius: 50%;
border: 2px solid #00ff00;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-top: 20px;
}
.control-group {
background: rgba(255,255,255,0.05);
padding: 15px;
border-radius: 10px;
}
.control-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.control-group input, .control-group select {
width: 100%;
padding: 8px;
border: none;
border-radius: 5px;
background: rgba(255,255,255,0.1);
color: white;
margin-bottom: 10px;
}
.control-group input::placeholder {
color: rgba(255,255,255,0.7);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
text-transform: uppercase;
transition: all 0.3s ease;
margin: 5px;
}
.btn-primary { background: #2196F3; color: white; }
.btn-success { background: #4CAF50; color: white; }
.btn-danger { background: #F44336; color: white; }
.btn-warning { background: #FF9800; color: white; }
.btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 20px;
}
.info-item {
background: rgba(255,255,255,0.05);
padding: 10px;
border-radius: 8px;
text-align: center;
}
.info-item .label { font-size: 0.9em; opacity: 0.8; }
.info-item .value { font-size: 1.2em; font-weight: bold; margin-top: 5px; }
@media (max-width: 768px) {
.container { grid-template-columns: 1fr; }
.radar-display { width: 300px; height: 300px; }
}
</style>
</head>
<body>
<div class="container">
<div class="panel">
<div class="status">
<h1>Radar Security <span id="statusIndicator" class="status-indicator armed"></span></h1>
<p id="statusText">System Armed</p>
</div>
<div class="controls">
<div class="control-group">
<label>Detection Distance (mm)</label>
<input type="number" id="detectionDistance" value="1000" min="100" max="8000">
<button class="btn btn-primary" onclick="updateConfig()">Update</button>
</div>
<div class="control-group">
<label>Alarm Duration (seconds)</label>
<input type="number" id="alarmDuration" value="10" min="1" max="300">
<button class="btn btn-primary" onclick="updateConfig()">Update</button>
</div>
<div class="control-group">
<label>System Control</label>
<button class="btn btn-success" id="armBtn" onclick="armSystem()">ARM</button>
<button class="btn btn-warning" id="disarmBtn" onclick="disarmSystem()">DISARM</button>
</div>
<div class="control-group">
<label>Alarm Control</label>
<button class="btn btn-danger" onclick="stopAlarm()">STOP ALARM</button>
<button class="btn btn-primary" onclick="toggleSystem()">ENABLE/DISABLE</button>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<div class="label">Distance</div>
<div class="value" id="targetDistance">-- mm</div>
</div>
<div class="info-item">
<div class="label">Angle</div>
<div class="value" id="targetAngle">--°</div>
</div>
<div class="info-item">
<div class="label">Speed</div>
<div class="value" id="targetSpeed">-- cm/s</div>
</div>
<div class="info-item">
<div class="label">Status</div>
<div class="value" id="detectionStatus">Clear</div>
</div>
</div>
</div>
<div class="panel radar-panel">
<h2>Radar Display</h2>
<div class="radar-display">
<svg class="radar-svg" viewBox="0 0 400 400">
<!-- Radar grid -->
<defs>
<pattern id="radarGrid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(0,255,0,0.2)" stroke-width="1"/>
</pattern>
</defs>
<rect width="400" height="400" fill="url(#radarGrid)"/>
<!-- Distance circles -->
<circle cx="200" cy="400" r="50" fill="none" stroke="rgba(0,255,0,0.3)" stroke-width="1"/>
<circle cx="200" cy="400" r="100" fill="none" stroke="rgba(0,255,0,0.3)" stroke-width="1"/>
<circle cx="200" cy="400" r="150" fill="none" stroke="rgba(0,255,0,0.3)" stroke-width="1"/>
<circle cx="200" cy="400" r="200" fill="none" stroke="rgba(0,255,0,0.3)" stroke-width="1"/>
<!-- 120-degree arc -->
<path d="M 27 273 A 200 200 0 0 1 373 273" fill="none" stroke="#00ff00" stroke-width="2"/>
<!-- Center lines -->
<line x1="200" y1="400" x2="200" y2="200" stroke="rgba(0,255,0,0.5)" stroke-width="1"/>
<line x1="200" y1="400" x2="27" y2="273" stroke="rgba(0,255,0,0.5)" stroke-width="1"/>
<line x1="200" y1="400" x2="373" y2="273" stroke="rgba(0,255,0,0.5)" stroke-width="1"/>
<!-- Target dot -->
<circle id="targetDot" cx="200" cy="400" r="0" fill="#ff0000" stroke="#ffffff" stroke-width="2" opacity="0">
<animate attributeName="r" values="5;8;5" dur="1s" repeatCount="indefinite"/>
</circle>
<!-- Radar sweep -->
<line id="radarSweep" x1="200" y1="400" x2="200" y2="200" stroke="#00ff00" stroke-width="2" opacity="0.7">
<animateTransform attributeName="transform" attributeType="XML" type="rotate"
values="240 200 400;300 200 400;240 200 400" dur="3s" repeatCount="indefinite"/>
</line>
</svg>
</div>
<div style="text-align: center; margin-top: 10px;">
<small>Detection Range: 8m | Field of View: 120°</small>
</div>
</div>
</div>
<script>
let systemConfig = {
armed: true,
systemEnabled: true,
detectionDistance: 1000,
alarmDuration: 10000
};
function updateStatus() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
const indicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
if (!data.systemEnabled) {
indicator.className = 'status-indicator disarmed';
statusText.textContent = 'System Disabled';
} else if (data.alarmActive) {
indicator.className = 'status-indicator alarm';
statusText.textContent = 'ALARM ACTIVE!';
} else if (data.armed) {
indicator.className = 'status-indicator armed';
statusText.textContent = 'System Armed';
} else {
indicator.className = 'status-indicator disarmed';
statusText.textContent = 'System Disarmed';
}
systemConfig = data;
});
}
function updateRadarData() {
fetch('/api/radar-data')
.then(response => response.json())
.then(data => {
document.getElementById('targetDistance').textContent = data.distance + ' mm';
document.getElementById('targetAngle').textContent = data.angle + '°';
document.getElementById('targetSpeed').textContent = data.speed + ' cm/s';
document.getElementById('detectionStatus').textContent = data.detected ? 'TARGET' : 'Clear';
updateRadarDisplay(data);
});
}
function updateRadarDisplay(data) {
const targetDot = document.getElementById('targetDot');
if (data.detected && data.distance > 0) {
// Convert distance to radar display coordinates
const maxDistance = 8000; // 8 meters in mm
const radarRadius = 200; // pixels
const scale = radarRadius / maxDistance;
// Convert angle and distance to x,y coordinates
const angleRad = (data.angle) * Math.PI / 180;
const distance = Math.min(data.distance, maxDistance);
const x = 200 + (distance * scale * Math.sin(angleRad));
const y = 400 - (distance * scale * Math.cos(angleRad));
targetDot.setAttribute('cx', x);
targetDot.setAttribute('cy', y);
targetDot.style.opacity = '1';
} else {
targetDot.style.opacity = '0';
}
}
function updateConfig() {
const distance = document.getElementById('detectionDistance').value;
const duration = document.getElementById('alarmDuration').value * 1000; // Convert to ms
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
detectionDistance: parseInt(distance),
alarmDuration: parseInt(duration)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Configuration updated successfully!');
}
});
}
function armSystem() {
fetch('/api/arm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ armed: true })
});
}
function disarmSystem() {
fetch('/api/arm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ armed: false })
});
}
function stopAlarm() {
fetch('/api/stop-alarm', { method: 'POST' });
}
function toggleSystem() {
fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ systemEnabled: !systemConfig.systemEnabled })
});
}
// Update data every 500ms
setInterval(() => {
updateStatus();
updateRadarData();
}, 500);
// Initial load
updateStatus();
updateRadarData();
</script>
</body>
</html>
)";
}
핵심 구성 요소
- WiFi 웹 서버: 원격 제어 및 모니터링 인터페이스
- 레이더 통합: RD-03D 센서 데이터(거리, 각도, 속도) 읽기
- 상태 관리: 무장/해제/경보 상태를 추적합니다.
주요 작업
루프 프로세스:
- 레이더 읽기: 목표 거리 및 위치 데이터 가져오기
- 위협 확인: 임계값 설정과 거리를 비교합니다.
- 제어 하드웨어: 상태에 따라 LED 및 부저 업데이트
- 웹 요청 처리: 웹 인터페이스에서 API 호출 처리
LED 상태
- 녹색: 무장 & 투명
- 빨간색: 대상이 감지됨(알람 중 깜박임)
- 파란색: 무장 해제
- 꺼짐: 시스템 비활성화
웹 API
- /api/status - 시스템 상태 및 센서 판독값
- /api/config - 감지 거리/알람 지속 시간 조정
- /api/arm - 시스템 시동/시동 해제
- /api/radar-data - 웹 레이더 디스플레이를 위한 라이브 데이터
스마트 로직
시스템이 무장되어 있고 범위 내에서 유효한 목표가 감지된 경우에만 경보를 트리거합니다. 구성 가능한 알람 타임아웃 및 실시간 120° 레이더 시각화 기능이 있는 전문 웹 인터페이스를 통한 원격 제어가 포함됩니다.
5단계: 액세스 Web 인터페이스
- 장치 켜기
- ESP32의 Wi-Fi 핫스팟 또는 로컬 네트워크 IP에 연결합니다.
- 브라우저에서 인터페이스 로드
- 설정: 최대 감지 거리(예: 3m)알람 기간(예: 5초)알람 켜기/끄기
- 레이더 아크에서 실시간 감지를 관찰합니다.


전력 및 배터리 수명
3.7V LiPo 셀은 Beetle 보드에 내장된 배터리 충전 관리 기능을 통해 충전되는 시스템에 전원을 공급합니다. RD-03D와 ESP32-C6의 저전력 소비로 인해 시스템은 한 번 충전으로 며칠 또는 몇 주 동안 작동할 수 있습니다. 딥 슬립 모드를 구현하면 배터리 수명을 몇 달 동안 연장할 수 있습니다.
레이더 보안 시스템의 작동 방식
이 시스템은 24GHz 전파를 방출하는 RD-03D 레이더 센서를 활용하여 120° 시야를 스캔하고 움직임을 감지하여 존재 여부뿐만 아니라 최대 8미터 떨어진 움직이는 물체의 속도, 각도 및 거리를 측정합니다. 열 신호에 의존하는 기존 PIR 센서와 달리 레이더는 완전한 어둠 속에서도 벽, 안개 또는 먼지를 통해 효과적으로 작동할 수 있으며 애완 동물이나 햇빛과 같은 일반적인 잘못된 트리거에 영향을 받지 않습니다. 이 시스템의 핵심은 들어오는 레이더 데이터를 실시간으로 처리하는 초고효율 소형 마이크로컨트롤러인 Beetle ESP32-C6입니다. 위협적이지 않은 움직임을 필터링하고 잠재적인 침입을 분류하는 스마트 탐지 엔진을 실행합니다. 위협이 감지되면 ESP32-C6는 시각 및 오디오 경고(LED 및 부저)를 트리거하고, 이벤트를 기록하고, 사용자가 실시간 레이더 시각화를 보고, 설정을 조정하고, 시스템을 원격으로 시동 또는 해제할 수 있는 웹 기반 대시보드를 업데이트합니다.
웹 인터페이스 및 시각화
ESP32는 웹 서버를 호스팅합니다. 사용자는 Wi-Fi를 통해 연결할 수 있으며 다음을 수행할 수 있습니다.
레이더 감지 거리 임계값 설정
- 레이더 감지 거리 임계값 설정
- 알람 지속 시간 구성(부저가 울리고 LED가 깜박이는 시간)
- 경보 시스템 켜기/끄기
- 레이더 감지 시각화를 120° 스윕으로 보기
- 실시간 감지 업데이트 모니터링
레이더 감지는 JavaScript 캔버스 요소에 그려져 해양 레이더와 유사한 호 기반 시각적 개체를 형성합니다.
Ai Thinker의 적절한 센서 장착 권장 사항


사용 사례 시나리오
- 집 입구를 모니터링하여 창문이나 유리를 통해 움직임이 있는지 확인합니다.
- 침입자가 가만히 있을 때도 존재 감지
- 스마트 에너지 관리(방에 아무도 없을 때 조명이 꺼짐)
- 카메라 없이 수용 인원을 감지하는 노인 요양 시스템
Home Assistant 통합(선택 사항)
MQTT 또는 REST API 엔드포인트를 사용하여 레이더를 홈어시스턴트 대시보드에 연결합니다.
- MQTT 또는 REST API 엔드포인트를 사용하여 레이더를 홈어시스턴트 대시보드에 연결합니다.
- 다음과 같은 자동화를 트리거합니다.
- 조명 켜기
- 알림 보내기
- 카메라 활성화
성능 최적화
- 센서 배치: 최상의 적용 범위를 위해 최적의 높이(2-3미터)에 장착
- 환경 요인: 실외 설치를 위한 날씨 차폐를 고려하십시오.
- 네트워크 최적화: 최상의 성능을 위해 전용 IoT 네트워크 사용
향후 개선 사항
이 프로젝트는 고급 보안 기능의 기초 역할을 합니다.
- 맞춘 PCB: 보다 강력한 시스템을 위해
- 드론 통합: 주변 모니터링을 위한 자동 드론 배치
- Professional Monitoring: 클라우드 기반 보안 서비스 통합
- 모바일 앱: 향상된 제어를 위한 전용 스마트폰 애플리케이션
더 스마트하고 강력한 홈 보안
이 DIY 레이더 보안 시스템은 신뢰할 수 있는 고급 보호 기능이 높은 가격표와 함께 제공될 필요가 없다는 것을 증명합니다. RD-03D 센서와 ESP32-C6 Beetle을 사용하면 기존 PIR 설정을 능가하는 정확하고 비바람에 견디는 스마트 모션 감지를 얻을 수 있습니다. 컴팩트하고 배터리로 구동되며 스마트 홈과 완전히 통합됩니다. 가정, 작업장 또는 외딴 건물에 관계없이 이 프로젝트는 요구 사항에 맞게 조정되는 전문가 수준의 보안을 제공합니다.
결론
RD-03D 및 ESP32-C6을 활용하면 저비용으로 상용 솔루션에 버금가는 고급 보안 시스템을 구축할 수 있습니다. 기존 PIR 센서의 한계를 극복하고, 지능적인 필터링을 통해 더욱 신뢰할 수 있는 감지를 제공합니다.
이 프로젝트를 확장하여 더 많은 기능을 추가할 수도 있습니다. 예를 들면, 자동화된 스마트 경보, AI 기반 침입자 패턴 분석, 홈 네트워크와의 통합 등이 가능합니다.