์ค๋งํธ ๋์ด๋ฒจ (Smart Doorbell, SDB)์ IoT ๊ธฐ์ ์ ํ์ฉํ์ฌ ๋ฐฉ๋ฌธ์๋ฅผ ๊ฐ์งํ๊ณ , ์ฌ์ฉ์์๊ฒ ์๋ฆผ์ ๋ณด๋ด๋ฉฐ, ์๊ฒฉ์ผ๋ก ๋ฌธ์ ์ ์ดํ ์ ์๋ ์์คํ ์ด๋ค. Raspberry Pi์ Flutter ์ฑ์ ์ค์ฌ์ผ๋ก ์ค๊ณ๋์ด, ์ค์๊ฐ ์์ ์คํธ๋ฆฌ๋ฐ, ์๋ฐฉํฅ ์์ฑ ํตํ, ๋ นํ ๊ธฐ๋ฅ, ๋ฐฉ๋ฌธ์ ๊ฐ์ง ์๋ฆผ ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ํตํด ์ฌ์ฉ์์๊ฒ ํธ์์ฑ๊ณผ ์์ ์ฑ์ ์ ๊ณตํ๋ค.
ํ๋ ๊ฐ์ ์ ๋ณด์ ์๊ตฌ ์ฆ๊ฐ
- 1์ธ ๊ฐ๊ตฌ์ ์ฆ๊ฐ์ ํจ๊ป ๊ฐ์ ๋ด ๋ณด์ ๋ฌธ์ ๋๋
- ๋น์งํธ์ด, ๊ฐ๋ ์ฌ๊ฑด ๋ฑ ์์์น ๋ชปํ ์ํ ์์์ ๋ํ ๋์ฒด ํ์
- ๋ฐฉ๋ฌธ์ ํ์ธ ๋ฐ ๋ณด์ ๊ฐํ๋ฅผ ์ํ ํจ๊ณผ์ ์ธ ์๋ฃจ์ ์๊ตฌ
IoT ๊ธฐ์ ์ ๋ฐ์ ๊ณผ ํ์ฉ
- ์ผ์, ์นด๋ฉ๋ผ, ํด๋ผ์ฐ๋ ์๋น์ค ์ฐ๋์ ํตํ ์ค๋งํธ ๋ณด์ ์์คํ ๊ตฌํ
- ์ ๋น์ฉ์ผ๋ก ๋ค์ํ ์ฅ์น ํตํฉ ๊ฐ๋ฅ
- ์ค๋งํธ ํ ๋ณด์ ์๋ฃจ์ ์ ํต์ฌ ๊ธฐ์ ๋ก ์๋ฆฌ ์ก์ IoT
์์ ํ ๊ฐ์ ํ๊ฒฝ ๊ตฌ์ถ์ ํ์์ฑ
- ์์ง์ ๊ฐ์ง, ์ค์๊ฐ ์์ ์คํธ๋ฆฌ๋ฐ, ๋ นํ ๋ฐ ์๋ฆผ์ ํตํ ์ฆ๊ฐ์ ์ธ ์ํ ๋์
- SDB ํ๋ก์ ํธ์ ํตํฉ ๊ธฐ์ ๋ก ํธ๋ฆฌํจ๊ณผ ๋ณด์ ์ ๊ณต
๋ฐฉ๋ฌธ์ ๊ด๋ฆฌ์ ํธ๋ฆฌ์ฑ ์ ๊ณต
- ์ค์๊ฐ ์์ ์คํธ๋ฆฌ๋ฐ์ผ๋ก ๋ฌธ ์ ์ํฉ์ ์ค๋งํธํฐ์์ ๋ฐ๋ก ํ์ธ
- ์๋ฐฉํฅ ์์ฑ ํตํ๋ฅผ ํตํด ๋ฐฉ๋ฌธ์์ ์ง์ ์ํต
- ๋์ด๋ฝ ์๊ฒฉ ์ ์ด๋ก ๋ฌผ๋ฆฌ์ ๊ฑฐ๋ฆฌ์ ์๊ด์์ด ๋์ด๋ฝ ์ ์ด ๊ฐ๋ฅ
์์ ํ ๊ฐ์ ๋ณด์ ์์คํ ๊ตฌํ
- ๋ฐฉ๋ฌธ์์ ์์ง์์ ๊ฐ์งํ๊ณ , ์์ง์ ๊ฐ์ง ์ด๋ฒคํธ ๋ฐ์์ ๊ธฐ๋ก
- ๋ นํ๋ ์์์ ์๋ฒ์ ์ ์ฅํด ์ธ์ ๋ ์ง ์ํฉ ํ์ธ ๊ฐ๋ฅ
- ์๋ฆผ ์์คํ ์ ํตํด ์ํ ์ํฉ ๋ฐ์ ์ ๋น ๋ฅด๊ฒ ๋์ฒ ๊ฐ๋ฅ
IoT ๊ธฐ์ ์ ํตํฉ ํ์ฉ
- Raspberry Pi์ ๋ค์ํ ์ผ์๋ฅผ ์ฐ๋ํ์ฌ ์ค๋งํธ ๋ณด์ ํ๊ฒฝ ์ ๊ณต
- ํด๋ผ์ฐ๋ ์๋น์ค๋ฅผ ํ์ฉํด ์ค์๊ฐ ๋ฐ์ดํฐ ๊ด๋ฆฌ์ ์๋ฆผ ์ ์ก
- ์ค๋งํธํฐ ์ฑ์ ํตํด ์์คํ ์ ์ฒด๋ฅผ ์๊ฒฉ์ผ๋ก ์ ์ดํ๊ณ ๋ชจ๋ํฐ๋ง ๊ฐ๋ฅ
์ด์ ์ฒด์ ๋ฐ ํ๊ฒฝ
- Raspberry Pi OS (Lite): ํ๋์จ์ด ์ ์ด์ ์๋ฒ ์ด์
- Python Virtual Environment: ์ข ์์ฑ ๊ด๋ฆฌ ๋ฐ ์คํ ํ๊ฒฝ ์ ๊ณต
ํ๋ก๊ทธ๋๋ฐ ์ธ์ด
- C: ์ด์ธ์ข ๋ฐ ๋ฐฉ๋ฌธ์ ๊ฐ์ง, ์๋ณด๋ชจํฐ ๋ฐ ํ๋์จ์ด ์ ์ด
- Python: Flask ์๋ฒ ๊ตฌํ, Firebase ์ฐ๋, ๋ นํ ๋ฐ ์คํธ๋ฆฌ๋ฐ ์ ์ด
- Dart (Flutter): ์ฌ์ฉ์ ์ธํฐํ์ด์ค, ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ ๋ฐ ๋์ด๋ฒจ ์ ์ด
๋ฐฑ์๋
- Flask: HTTP ์๋ฒ, ๋น๋์ค/์ค๋์ค ์คํธ๋ฆฌ๋ฐ, ํ์ผ ์ ๋ก๋
- Firebase: Cloud Messaging(์๋ ์ ์ก), Storage(๋ นํ ํ์ผ ๊ด๋ฆฌ), Admin SDK(Python, Flutter ํตํฉ)
graph TD
A[PIR ์ผ์] -->|์์ง์ ๊ฐ์ง| B[Raspberry Pi]
C[์ด์ธ์ข
๋ฒํผ] -->|๋ฒํผ ํด๋ฆญ ์ด๋ฒคํธ| B
D[๋ผ์ฆ๋ฒ ๋ฆฌํ์ด ์นด๋ฉ๋ผ] -->|์์ ์บก์ฒ| B
B -->|๋ฐ์ดํฐ ์ ์ก| E[Flask ์๋ฒ]
E -->|๋
นํ ์์ ์ ์ฅ| F[Firebase Storage]
E -->|์ด๋ฒคํธ ๊ธฐ๋ก| G[Firebase Firestore]
E -->|ํธ์ ์๋ฆผ ์ ์ก| H[Firebase Messaging]
H -->|์๋ฆผ ์์ | I[Flutter ์ฑ]
F -->|๋
นํ ์์ ํ์ธ| I
B -->|์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ| I
I -->|์๊ฒฉ ์ ์ด ์์ฒญ| B
B -->|๋์ด๋ฝ ์ ์ด| J[์๋ณด๋ชจํฐ]
B -->|์๋ฆผ์ ์ถ๋ ฅ| K[๋ถ์ ]
classDef sensor fill:#ADFF2F,stroke:#8FBC8F,stroke-width:2px;
classDef raspberrypi fill:#f9b0b4,stroke:#ff0000,stroke-width:2px;
classDef flask fill:#b3d6f3,stroke:#0066cc,stroke-width:2px;
classDef firebase fill:#fffacd,stroke:#ffcc00,stroke-width:2px;
classDef flutter fill:#e6ccff,stroke:#9933ff,stroke-width:2px;
classDef actuator fill:#FFA500,stroke:#FF7F50,stroke-width:2px;
class A,C,D sensor;
class B raspberrypi;
class E flask;
class F,G,H firebase;
class I flutter;
class J,K actuator;
| ํ๋ก์ธ์ค ์ด๋ฆ | ์์ฑ ์์น | ์ฃผ์ ์ญํ |
|---|---|---|
| Main Process | motion_detection.c |
PIR ์ผ์ ๋ฐ ์ด์ธ์ข ๋ฒํผ ๊ฐ์ง, Flask ๋ฐ Motor ํ๋ก์ธ์ค ์์ฑ, ์ด๋ฒคํธ ํธ๋ฆฌ๊ฑฐ |
| Flask Process | flask_app.py |
Flask ์๋ฒ ์คํ, ๋น๋์ค/์ค๋์ค ์คํธ๋ฆฌ๋ฐ ์ ๊ณต, HTTP ์์ฒญ ์ฒ๋ฆฌ, ๋ นํ ๊ด๋ฆฌ |
| Recording Controller | flask_app.py ๋ด๋ถ |
control.txt ๋ชจ๋ํฐ๋ง, ๋
นํ ์ํ ๊ด๋ฆฌ, Firebase ์
๋ก๋ ์ฒ๋ฆฌ |
| Motor Process | motor.c |
Flask Process์ ์์ผ ํต์ ๋๊ธฐ, ๋ช ๋ น ์์ ํ ์๋ณด๋ชจํฐ ์ ์ด |
๐ ๊ณต์ ์์์ ํตํ ์ํ ๊ด๋ฆฌ
- ํ๋ก์ธ์ค ๊ฐ ์ํ ์ ๋ณด๋ฅผ ๊ธฐ๋กํ๊ณ ์ฝ๋ ๋ฐฉ์์ผ๋ก ๋๊ธฐํ ์ํ
- ์์ง์ ๊ฐ์ง ์ํ, ์คํธ๋ฆฌ๋ฐ ์ํ ๊ธฐ๋ก
๐ ์ํ ์ ์ก ์ฌ๋ก์ ๊ฐ ํ๋ก์ธ์ค์์์ ํ์ฉ ๋ฐฉ์
-
์์ง์ ๊ฐ์ง ์ โ ์์ง์ ๊ฐ์ง "ํ์ฑ" ์ํ ๊ธฐ๋ก
- ์์ง์ ๊ฐ์ง ํ๋ก์ธ์ค: PIR ์ผ์๋ฅผ ํตํด ์์ง์์ด ๊ฐ์ง๋๋ฉด ์์ง์ ๊ฐ์ง ์ํ ํ์ผ์ "ํ์ฑ" ์ํ ๊ธฐ๋ก
- ๋
นํ ํ๋ก์ธ์ค: ์์ง์ ๊ฐ์ง ์ํ ํ์ผ์ ์ฃผ๊ธฐ์ ์ผ๋ก ํ์ธํ๋ฉฐ, "ํ์ฑ" ์ํ๊ฐ ๊ฐ์ง๋๋ฉด ๋
นํ ์์
- ๋ง์ฝ ์คํธ๋ฆฌ๋ฐ ์ํ๊ฐ "ํ์ฑ"์ธ ๊ฒฝ์ฐ, ์์ง์ ๊ฐ์ง ์ํ์ ๊ด๊ณ์์ด ๋ นํ๋ฅผ ์ํํ์ง ์์
-
์์ง์ ์ข ๋ฃ ์ โ ์์ง์ ๊ฐ์ง "๋นํ์ฑ" ์ํ ๊ธฐ๋ก
- ์์ง์ ๊ฐ์ง ํ๋ก์ธ์ค: PIR ์ผ์๊ฐ ์ผ์ ์๊ฐ ์์ง์์ ๊ฐ์งํ์ง ๋ชปํ๋ฉด ์์ง์ ๊ฐ์ง ์ํ ํ์ผ์ "๋นํ์ฑ" ์ํ ๊ธฐ๋ก
- ๋ นํ ํ๋ก์ธ์ค: ์์ง์ ๊ฐ์ง ์ํ ํ์ผ์ด "๋นํ์ฑ" ์ํ๋ก ๋ณ๊ฒฝ๋๋ฉด ๋ นํ ์ข ๋ฃ
-
์คํธ๋ฆฌ๋ฐ ํ์ฑํ ์ โ ์คํธ๋ฆฌ๋ฐ "ํ์ฑ" ์ํ ๊ธฐ๋ก
- ์คํธ๋ฆฌ๋ฐ ํ๋ก์ธ์ค: ์คํธ๋ฆฌ๋ฐ ์์ฒญ์ด ๋ค์ด์ค๋ฉด ์คํธ๋ฆฌ๋ฐ ์ํ์ "ํ์ฑ" ์ํ ๊ธฐ๋ก
- ๋ นํ ํ๋ก์ธ์ค: ์คํธ๋ฆฌ๋ฐ ์ํ๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ํ์ธํ๋ฉฐ, "ํ์ฑ" ์ํ๊ฐ ๊ฐ์ง๋๋ฉด ๋ นํ ์ค์ง
- ์์ง์ ๊ฐ์ง ํ๋ก์ธ์ค: ์คํธ๋ฆฌ๋ฐ ์ํ๋ฅผ ์ฐธ์กฐํ์ฌ, ์คํธ๋ฆฌ๋ฐ์ด ํ์ฑ ์ํ์ธ์ง ํ์ธํ๋ฉฐ ์์ ์ํ
-
์คํธ๋ฆฌ๋ฐ ์ค์ง ์ โ ์คํธ๋ฆฌ๋ฐ "๋นํ์ฑ" ์ํ ๊ธฐ๋ก
- ์คํธ๋ฆฌ๋ฐ ํ๋ก์ธ์ค: ์คํธ๋ฆฌ๋ฐ ์ค์ง ์์ฒญ์ด ๋ค์ด์ค๋ฉด ์คํธ๋ฆฌ๋ฐ ์ํ์ "๋นํ์ฑ" ์ํ ๊ธฐ๋ก
- ์์ง์ ๊ฐ์ง ํ๋ก์ธ์ค: ์คํธ๋ฆฌ๋ฐ ์ํ๋ฅผ ํ์ธํ์ฌ, "๋นํ์ฑ" ์ํ๋ก ์์ ์ข ๋ฃ
๐ก ๊ณต์ ์์์ ์ฅ์
- ๊ฐ๋จํ๊ณ ๊ฐ๋ฒผ์ด ๋ฐฉ์์ผ๋ก ํ๋ก์ธ์ค ๊ฐ ๋ฐ์ดํฐ ๋๊ธฐํ ๊ฐ๋ฅ
- ๋ค๋ฅธ ์ธ์ด๋ฅผ ์ฌ์ฉํ๋ ํ๋ก์ธ์ค ๊ฐ ๋ฐ์ดํฐ ๊ณต์ ๊ฐ ๊ฐํธ
- ๊ฐํ ๊ฒฐํฉ์ ํผํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ๊ตฌ์กฐ ์ ๊ณต
-
Main Process (
motion_detection.c)์ค๋ ๋ ์ด๋ฆ ์ฃผ์ ์ญํ Motion Detection Thread PIR ์ผ์๋ฅผ ํตํด ์์ง์ ๊ฐ์ง, Flask ์๋ฒ ์คํ Button Press Thread ์ด์ธ์ข ๋ฒํผ ์ ๋ ฅ ๊ฐ์ง, Firebase ์๋ฆผ ์ ์ก, ํ์ ์์ ํธ๋ฆฌ๊ฑฐ Buzzer Thread ์ด์ธ์ข ๋ฒํผ ์ ๋ ฅ ๋ฐ์ ์ ๋ถ์ ๋ฅผ ์ธ๋ ค ์ฌ์ฉ์ ์๋ฆผ ์ ๊ณต -
Flask Process (
flask_app.py)์ค๋ ๋ ์ด๋ฆ ์ฃผ์ ์ญํ Flask Request Threads HTTP ์์ฒญ ์ฒ๋ฆฌ, /stream,/audio,/upload๋ฑ ๊ฐ ์์ฒญ๋ณ๋ก ์ค๋ ๋ ์์ฑRecording Thread Picamera2๋ก ๋น๋์ค ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ Audio Streaming Thread PyAudio๋ก ์ค์๊ฐ ์ค๋์ค ๋ฐ์ดํฐ ์คํธ๋ฆฌ๋ฐ Audio Playback Thread Pygame์ผ๋ก ์ ๋ก๋๋ ์ค๋์ค ํ์ผ ์ฌ์ Cleanup Thread ์ ํ๋ฆฌ์ผ์ด์ ์ข ๋ฃ ์ ์นด๋ฉ๋ผ ๋ฐ ์ค๋์ค ๋ฆฌ์์ค ์ ๋ฆฌ -
Motor Process (
motor.c)์ค๋ ๋ ์ด๋ฆ ์ฃผ์ ์ญํ Socket Thread ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ช ๋ น ์์ , ์์ ํ์ ์ ์ฅ Output Thread ์์ ํ์์ ๋ช ๋ น์ ์ฝ์ด ์๋ณด๋ชจํฐ ๊ฐ๋ ์ ์ด
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ | ์ญํ ๋ฐ ์ค๋ช |
|---|---|
| WiringPi | Raspberry Pi GPIO ํ ์ ์ด |
| Pthread | ํ๋์จ์ด ์ด๋ฒคํธ๋ฅผ ๋ณ๋ ฌ ์ฒ๋ฆฌํ์ฌ ์ค์๊ฐ ์ฑ๋ฅ ์ต์ ํ |
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ | ์ญํ ๋ฐ ์ค๋ช |
|---|---|
| Picamera2 | Raspberry Pi ์นด๋ฉ๋ผ ๋ชจ๋๋ก ๋น๋์ค ํ๋ ์ ์บก์ฒ ๋ฐ ์คํธ๋ฆฌ๋ฐ |
| OpenCV | ์ค์๊ฐ ๋น๋์ค ํ๋ ์ ์ฒ๋ฆฌ ๋ฐ JPEG ํ์ ๋ณํ |
| FFmpeg | h264 ํ์์ ๋ นํ ์์์ mp4๋ก ๋ณํ |
| PyAudio | ์ค๋์ค ๋ฐ์ดํฐ ์บก์ฒ ๋ฐ ์๋ฐฉํฅ ์ค๋์ค ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ |
| Flask | HTTP ์๋ฒ๋ก ๋น๋์ค ์คํธ๋ฆฌ๋ฐ, ์ค๋์ค ์ก์์ , ํ์ผ ์ ๋ก๋ ๊ด๋ฆฌ |
| Firebase Admin SDK | ์ด๋ฒคํธ ๋ฐ์ดํฐ์ ์์ ํ์ผ ์ ์ฅ ๋ฐ ์ฐ๋ |
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ | ์ญํ ๋ฐ ์ค๋ช |
|---|---|
| http | Flask ์๋ฒ์์ ๋ฐ์ดํฐ ์์ฒญ ๋ฐ ์๋ต ์ฒ๋ฆฌ |
| firebase_messaging | Firebase๋ฅผ ํตํด ํธ์ ์๋ฆผ ์์ ๋ฐ ๊ด๋ฆฌ |
| just_audio | Flutter ์ฑ์์ ์ค๋์ค ์คํธ๋ฆฌ๋ฐ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ |
์ฃผ์ ์ญํ
- PIR ์ผ์๋ฅผ ํตํด ์์ง์์ ๊ฐ์งํ๊ณ , ์์ง์ ๊ฐ์ง ์ํ ํ์ผ์ ํตํด ๋ นํ ํ๋ก์ธ์ค์ ์ํ๋ฅผ ๋๊ธฐํ
- ์์ง์ ๊ฐ์ง ์ ๋น๋์ค๋ฅผ ๋
นํํ๊ณ , ๋
นํ๋ ํ์ผ์
.mp4๋ก ๋ณํํ ํ Firebase์ ์ ๋ก๋
๋์ ๋ฐฉ์
-
์์ง์ ๊ฐ์ง
- PIR ์ผ์๊ฐ ์์ง์์ ๊ฐ์งํ๋ฉด ์์ง์ ๊ฐ์ง ์ํ ํ์ผ์ "ํ์ฑ" ์ํ๋ฅผ ๊ธฐ๋ก
- ๋ ธ์ด์ฆ๋ก ์ธํด 0์ผ๋ก ๋ฐ๋๊ฑฐ๋ ์ฌ๋์ด ์ ๊น ๊ฐ์ง๋์ง ์๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํ์ฌ ์์ง์์ด 8์ด๊ฐ ๊ฐ์ง๋์ง ์์ ๊ฒฝ์ฐ "๋นํ์ฑ" ์ํ๋ก ์ด๊ธฐํ
-
๋ นํ ์์ ๋ฐ ์ข ๋ฃ
- Flask ํ๋ก์ธ์ค๊ฐ ์์ง์ ๊ฐ์ง ์ํ ํ์ผ์ ์ฃผ๊ธฐ์ ์ผ๋ก ์ฝ์ด ์ํ๋ฅผ ํ์ธ
- "ํ์ฑ" ์ํ์ผ ๊ฒฝ์ฐ ๋ นํ๋ฅผ ์์ํ๊ณ , "๋นํ์ฑ" ์ํ๋ก ์ ํ๋๋ฉด ๋ นํ๋ฅผ ์ข ๋ฃ
- ๋
นํ ์ทจ์: ์ฌ๋์ด 13์ด ์ด๋ด์ ์ฌ๋ผ์ง๋ฉด ๋
นํ๊ฐ ์ทจ์๋๋ฉฐ, ํด๋น ์์์ ์ ์ฅ๋์ง ์์
- PIR ์ผ์ ํ ์คํธ ๊ฒฐ๊ณผ, ์ผ์ ํ์ฑํ ์ํ์์ ๋นํ์ฑํ ์ํ๋ก ์ ํ๋๋๋ฐ ์ฝ 5์ด ๊ฑธ๋ฆผ
- ์ด๋ฅผ ๊ฐ์ํ์ฌ 5์ด(ํ์ฑ -> ๋นํ์ฑ) + 8์ด(๋ ธ์ด์ฆ ๋ฐฉ์ง) = 13์ด๋ผ๋ ์๊ฐ ์ ์ฉ
- ์ค๋ฒํค๋ ๋ฐ์์ ๊ฐ์ํ์ฌ ์ฝ๋์์๋ 13์ด๊ฐ ์๋ 10์ด๋ก ์ค์
-
Firebase ์ ๋ก๋
- 13์ด ์ด๋ด์ ๋
นํ๊ฐ ์ข
๋ฃ๋์ง ์์๋ค๋ฉด
.h264ํ์ผ์.mp4๋ก ๋ณํ - ๋ณํ๋
.mp4ํ์ผ์ Firebase Storage์ ์ ๋ก๋
- 13์ด ์ด๋ด์ ๋
นํ๊ฐ ์ข
๋ฃ๋์ง ์์๋ค๋ฉด
ํต์ฌ ์ฝ๋
-
์์ง์ ๊ฐ์ง (
Motion_Detection.c):void *motionDetectionThread(void *arg) { int motionDetected = 0; unsigned long lastMotionTime = 0; while (1) { motionDetected = digitalRead(PIR_PIN); if (motionDetected) { lastMotionTime = millis(); // ์์ง์ ๊ฐ์ง ์๊ฐ ์ ๋ฐ์ดํธ if (!personDetected) { personRecognition = 1; personDetected = 1; writeSignalToFile("1"); // "ํ์ฑ" ์ํ ๊ธฐ๋ก uploadToFirebase_Sensor(); // Firebase ์๋ฆผ ์ ์ก } } else if (!motionDetected && (millis() - lastMotionTime > 8000) && personRecognition) { personRecognition = 0; personDetected = 0; writeSignalToFile("0"); // "๋นํ์ฑ" ์ํ ๊ธฐ๋ก } delay(200); // ์งง์ ๋๊ธฐ } }
-
๋ นํ ๋ฐ Firebase ์ ๋ก๋ (
flask_app.py):def start_recording(): timestamp = time.strftime("%Y%m%d_%H%M%S") video_path = os.path.join(video_dir, f"{timestamp}.h264") print("๋ นํ ์์...") command = ["libcamera-vid", "-t", "0", "-o", video_path] process = subprocess.Popen(command) return process, video_path def convert_to_mp4(video_path): convert_command = [ "ffmpeg", "-i", video_path, "-c:v", "copy", video_path.replace(".h264", ".mp4") ] print("MP4 ๋ณํ ์ค...") subprocess.run(convert_command) print("MP4 ๋ณํ ์๋ฃ") def upload_to_firebase(video_path): firebase_video_upload.upload_file_to_firebase( video_path, "videos/" + os.path.basename(video_path) ) def recording_controller(): prev_state = None recording_process = None current_video_path = None global capture while True: if not streaming_video.value: try: # ์ํ ํ์ผ์์ ์ํ ์ฝ๊ธฐ with open(file_path, "r") as f: state = f.read().strip() if state == "1" and prev_state != "1": prev_state = state # ๋ นํ ์์ if recording_process is None: capture = True print("๋ นํ๊ฐ ์์๋์์ต๋๋ค.") time.sleep(2) recording_process, current_video_path = start_recording() for _ in range(100): # 10์ด ๋์ ์ํ ํ์ธ (0.1์ด ๊ฐ๊ฒฉ) time.sleep(0.1) with open(file_path, "r") as f: updated_state = f.read().strip() if updated_state == "0": print("๋ นํ๋ฅผ ์ทจ์ํฉ๋๋ค.") recording_process.terminate() recording_process.wait() recording_process = None if current_video_path: try: os.remove(current_video_path) # ์งง์ ๋ นํ๋ณธ ์ญ์ print("์งง์ ๋ นํ๋ณธ ์ญ์ ") except Exception as e: print("ํ์ผ ์ญ์ ์คํจ") current_video_path = None break else: print("10์ด ์ด์ ์ธ์๋์ด ๋ นํ๋ฅผ ์ ์งํฉ๋๋ค.") else: print("์ด๋ฏธ ๋ นํ ์ค์ ๋๋ค.") elif state == "0" and prev_state != "0": prev_state = state # ๋ นํ ์ค์ง if recording_process is not None: print("์์ ๋ นํ๋ฅผ ์ค์งํฉ๋๋ค...") recording_process.terminate() recording_process.wait() recording_process = None if current_video_path: convert_to_mp4(current_video_path) firebase_video_upload.upload_file_to_firebase( current_video_path.replace('.h264', '.mp4'), "videos/" + time.strftime("%Y%m%d_%H%M%S") + ".mp4" ) current_video_path = None else: print("๋ นํ ์ค์ด ์๋๋๋ค.") release_camera() capture = False time.sleep(0.1) # ์ํ ํ์ผ์ ์ฃผ๊ธฐ์ ์ผ๋ก ํ์ธ except FileNotFoundError: print(f"{file_path} ํ์ผ์ด ์กด์ฌํ์ง ์์ต๋๋ค. ๋๊ธฐ ์ค...") except Exception as e: print(f"์ค๋ฅ ๋ฐ์: {e}") time.sleep(0.1)
์ฃผ์ ์ญํ
- ์คํธ๋ฆฌ๋ฐ ํ์ฑํ:
/stream์์ฒญ ์ ๋น๋์ค ์คํธ๋ฆผ ํ์ฑํ - ์คํธ๋ฆฌ๋ฐ ์ค์ง:
/stop_stream์์ฒญ ์ ๋น๋์ค ์คํธ๋ฆผ ์ค์ง
๋์ ๋ฐฉ์
-
์คํธ๋ฆฌ๋ฐ ํ์ฑํ
- ์คํธ๋ฆฌ๋ฐ ์ํ ๋ณ์(
streaming_video.value)๋ฅผ "ํ์ฑ"์ผ๋ก ์ค์ - ์นด๋ฉ๋ผ ์ด๊ธฐํ ๋ฐ ํ๋ ์ ์์ฑ ์์
- ์คํธ๋ฆฌ๋ฐ ์ํ ๋ณ์(
-
์คํธ๋ฆฌ๋ฐ ์ค์ง
- ์ํ ๋ณ์๋ฅผ "๋นํ์ฑ"์ผ๋ก ์ค์ ํ๊ณ , ์นด๋ฉ๋ผ ๋ฆฌ์์ค๋ฅผ ํด์
ํต์ฌ ์ฝ๋
-
์คํธ๋ฆฌ๋ฐ ํ์ฑํ ๋ฐ ์ค์ง (
flask_app.py):@app.route('/stream') def stream(): with lock: if not streaming_video.value: initialize_camera() streaming_video.value = True return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame') @app.route('/stop_stream') def stop_stream(): with lock: streaming_video.value = False release_camera() return "์คํธ๋ฆฌ๋ฐ ์ค์ง"
-
ํ๋ ์ ์์ฑ (
flask_app.py):def generate_frames(): """Picamera2์์ ํ๋ ์ ์์ฑ""" global picam2 initialize_camera() # Picamera2 ์ด๊ธฐํ while streaming_video.value: try: if picam2 is None: break frame = picam2.capture_array() _, buffer = cv2.imencode('.jpg', frame) frame = buffer.tobytes() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') except Exception as e: print(f"Error generating frame: {e}") break
์ฃผ์ ์ญํ
- ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด ๋ง์ดํฌ๋ก ์ ๋ ฅ๋ ์์ฑ์ ์ค์๊ฐ์ผ๋ก ์บก์ฒํ๊ณ Flask ์๋ฒ๋ฅผ ํตํด ์ค๋งํธํฐ์ผ๋ก ์ ์ก
๋์ ๋ฐฉ์
- ์ค๋์ค ์บก์ฒ
PyAudio๋ฅผ ์ฌ์ฉํ์ฌ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ ๋ง์ดํฌ ์ ๋ ฅ์ ์บก์ฒ- ์ด๋ ๊ธฐ๋ณธ ์ฅ์น ๋ฒํธ๋ฅผ ์ฌ์ฉํ๋ คํ๊ธฐ ๋๋ฌธ์ ๊ธฐ๊ธฐ ๋ฒํธ๋ฅผ ๋ง์ดํฌ ๋ฒํธ์ ๋ง๊ฒ ์ง์
- ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ
- Flask ์๋ฒ์
/audio์๋ํฌ์ธํธ๋ฅผ ํตํด ์ค๋งํธํฐ์ผ๋ก ์ค๋์ค ๋ฐ์ดํฐ๋ฅผ ์ ์ก
- Flask ์๋ฒ์
ํต์ฌ ์ฝ๋
-
์ค๋์ค ์คํธ๋ฆฌ๋ฐ (
flask_app.py):def audio_stream(): CHUNK = 256 stream = audio1.open(format=FORMAT, channels=CHANNELS, input_device_index=2, rate=RATE, input=True, frames_per_buffer=CHUNK) print("์ค๋์ค ์คํธ๋ฆฌ๋ฐ ์์") while is_streaming: data = stream.read(CHUNK, exception_on_overflow=False) yield data stream.stop_stream() stream.close() @app.route('/audio') def audio(): global is_streaming is_streaming = True return Response(audio_stream(), mimetype="audio/wav")
์ฃผ์ ์ญํ
- ์ค๋งํธํฐ์์ ์ ์ก๋ ์ค๋์ค ๋ฐ์ดํฐ๋ฅผ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์์ ์์ ํ์ฌ ์คํผ์ปค๋ก ์ถ๋ ฅ
๋์ ๋ฐฉ์
- ์ค๋์ค ์
๋ก๋
- ์ค๋งํธํฐ์์ ๋
น์๋ ํ์ผ์
/upload์๋ํฌ์ธํธ๋ก ์ ์ก
- ์ค๋งํธํฐ์์ ๋
น์๋ ํ์ผ์
- ์ค๋์ค ์ฌ์
- ์
๋ก๋๋ ํ์ผ์
Pygame์ ์ด์ฉํ์ฌ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ ์คํผ์ปค๋ก ์ถ๋ ฅ
- ์
๋ก๋๋ ํ์ผ์
ํต์ฌ ์ฝ๋
-
์ค๋์ค ํ์ผ ์ ๋ก๋ (
flask_app.py):@app.route('/upload', methods=['POST']) def upload_audio(): if 'file' not in request.files: return "No file part", 400 file = request.files['file'] if file.filename == '': return "No selected file", 400 path = os.path.join(audio_path, file.filename) file.save(path) threading.Thread(target=play_audio, args=(path,)).start() return f"File saved at {path}", 200 def play_audio(audio_path): print("์ค๋์ค ์ฌ์ ์ค...") pygame.mixer.init() pygame.mixer.music.load(audio_path) pygame.mixer.music.play() while pygame.mixer.music.get_busy(): continue pygame.mixer.music.stop() pygame.mixer.quit()
์ฃผ์ ์ญํ
- ์๊ฒฉ ๋์ด๋ฝ ์ ์ด๋ฅผ ์ํ ์๋ณด๋ชจํฐ ๋ช ๋ น ์ฒ๋ฆฌ
- ์์ผ ์๋ฒ๋ฅผ ํตํด ๋ช ๋ น ์์ ๋ฐ ์๋ณด๋ชจํฐ ํ์
๋์ ๋ฐฉ์
- ๋ช
๋ น ์์
- ์์ผ ์๋ฒ์์ ๋ช ๋ น์ ์์ ํ์ฌ ์์ ํ์ ์ ์ฅ
- ๋ช
๋ น ์คํ
- ์์ ํ์์ ๋ช ๋ น์ ์ฝ์ด ์๋ณด๋ชจํฐ ํ์ ๊ฐ๋ ์ค์
ํต์ฌ ์ฝ๋
-
์๋ณด๋ชจํฐ ์ ์ด (
motor.c):void *outputThread(void *arg) { while (1) { if (!queueIsEmpty()) { char *command = dequeue(); if (strcmp(command, "LOCK") == 0) { setServoAngle(0); // ๋์ด๋ฝ ์ ๊ธ } else if (strcmp(command, "UNLOCK") == 0) { setServoAngle(90); // ๋์ด๋ฝ ํด์ } free(command); } usleep(50000); } }
์ฃผ์ ์ญํ
- ์์ง์ ๊ฐ์ง ์๋ฆผ: PIR ์ผ์์์ ์์ง์์ ๊ฐ์งํ๋ฉด Firebase๋ฅผ ํตํด ์๋ฆผ ์ ์ก
- ์ด์ธ์ข ์๋ฆผ: ์ด์ธ์ข ๋ฒํผ์ ๋๋ฅด๋ฉด Firebase๋ฅผ ํตํด ์๋ฆผ ์ ์ก
๋์ ๋ฐฉ์
- ์์ง์ ๊ฐ์ง ์๋ฆผ
- PIR ์ผ์์์ ์์ง์์ด ๊ฐ์ง๋๋ฉด
uploadToFirebase_Sensor()ํธ์ถํ์ฌ Firebase๋ก ์๋ฆผ ์ ์ก
- PIR ์ผ์์์ ์์ง์์ด ๊ฐ์ง๋๋ฉด
- ์ด์ธ์ข
์๋ฆผ
- ์ด์ธ์ข
๋ฒํผ์ด ๋๋ฆฌ๋ฉด
uploadToFirebase_Bell()ํธ์ถํ์ฌ Firebase๋ก ์ด์ธ์ข ์๋ฆผ ์ ์ก
- ์ด์ธ์ข
๋ฒํผ์ด ๋๋ฆฌ๋ฉด
ํต์ฌ ์ฝ๋
-
์์ง์ ๊ฐ์ง ์๋ฆผ (Motion_Detection.c)
// ์ผ์ firebase ์๋ ํธ๋ฆฌ๊ฑฐ void uploadToFirebase_Sensor() { int result = system("python3 /home/pi/SDB/firebaseUpload_Sensor.py"); if (result == 0) { printf("Sensor Firebase ์ ๋ก๋ ์ฑ๊ณต\n"); } else { printf("Sensor Firebase ์คํจ\n"); } } // ๋ชจ์ ๊ฐ์ง ์ค๋ ๋ void *motionDetectionThread(void *arg) { int personDetected = 0; // ์ฌ๋ ๊ฐ์ง ์ฌ๋ถ int motionDetected = 0; // ์ผ์ ์ถ๋ ฅ ์ํ unsigned long lastMotionTime = 0; // ๋ง์ง๋ง์ผ๋ก ์์ง์์ ๊ฐ์งํ ์๊ฐ while (1) { motionDetected = digitalRead(PIR_PIN); if (motionDetected) { lastMotionTime = millis(); // ์์ง์ ๊ฐ์ง ์๊ฐ ์ ๋ฐ์ดํธ if (!personDetected) { personRecognition = 1; personDetected = 1; printf("์ฌ๋ ๊ฐ์ง, ๋ นํ ํ๋ก๊ทธ๋จ ์์\n"); writeSignalToFile("1"); uploadToFirebase_Sensor(); // Firebase๋ก ์๋ฆผ ์ ์ก } } else if (!motionDetected && (millis() - lastMotionTime > 8000) && personRecognition) { personRecognition = 0; personDetected = 0; printf("์ฌ๋ ์์, ๋ นํ ํ๋ก๊ทธ๋จ ์ข ๋ฃ\n"); writeSignalToFile("0"); } delay(200); // ์งง์ ๋๊ธฐ } return NULL; }
-
์ด์ธ์ข ์๋ฆผ (Motion_Detection.c)
// ์ด์ธ์ข firebase ์๋ ํธ๋ฆฌ๊ฑฐ void uploadToFirebase_Bell() { int result = system("python3 /home/pi/SDB/firebaseUpload_Bell.py"); if (result == 0) { printf("Bell Firebase ์ ๋ก๋ ์ฑ๊ณต\n"); } else { printf("Bell Firebase ์คํจ\n"); } } // ์ด์ธ์ข ์ค๋ ๋ void *buttonPressThread(void *arg) { int buttonState = HIGH; // ๋ฒํผ ์ด๊ธฐ ์ํ int lastButtonState = HIGH; // ๋ฒํผ ๋์ค ์ํ while (1) { buttonState = digitalRead(BUTTON_PIN); if (buttonState == LOW && lastButtonState == HIGH) { doorbellState = 1; // Doorbell was pressed printf("์ด์ธ์ข ์ด ๋๋ฌ์ง, ์๋ ์ ์ก\n"); uploadToFirebase_Bell(); // Firebase๋ก ์ด์ธ์ข ์๋ฆผ ์ ์ก printf("ํ์ํตํ ํ๋ก์ธ์ค ์์\n"); } lastButtonState = buttonState; delay(50); // ๋๋ฐ์ด์ฑ ์ฒ๋ฆฌ } return NULL; }
Flutter๋ก ๊ฐ๋ฐ๋ ์ค๋งํธ ๋์ด๋ฒจ ์ฑ์ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์์ ์ค์๊ฐ ํต์ ์ ํตํด ์์ ์คํธ๋ฆฌ๋ฐ, ์์ฑ ์ก์์ , ๋์ด๋ฝ ์ ์ด ๋ฐ ๋ นํ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. ์ฌ์ฉ์๋ ์ง๊ด์ ์ธ UI๋ฅผ ํตํด ๊ฐํธํ๊ฒ ์ค๋งํธ ๋์ด๋ฒจ์ ๋ค์ํ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์๋ค.
Flutter๋ก ๊ฐ๋ฐ๋ ์ฑ์ ๋ค์์ ์ฃผ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค:
- IP ์ ๋ ฅ ๋ฐ ์ ์ฅ: ์ฌ์ฉ์๊ฐ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ IP ์ฃผ์๋ฅผ ์ ๋ ฅํ๊ณ ์ ์ฅ
- ์์ ์คํธ๋ฆฌ๋ฐ: ์ค์๊ฐ์ผ๋ก ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์์ ์คํธ๋ฆฌ๋ฐ๋๋ ์์์ ํ์
- ์์ฑ ์ก์ ๋ฐ ์์ : ์ฌ์ฉ์ ์์ฑ์ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ ์ ์กํ๊ฑฐ๋ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ ๋ง์ดํฌ ์ ๋ ฅ์ ์ค์๊ฐ์ผ๋ก ์ฒญ์ทจ
- ๋์ด๋ฝ ์ ์ด: ๋ฒํผ์ ํตํด ์๊ฒฉ์ผ๋ก ๋์ด๋ฝ์ ์ ๊ทธ๊ฑฐ๋ ํด์
- ๋ นํ ๋ชฉ๋ก ๊ด๋ฆฌ: Firebase Storage์์ ๋ นํ๋ ์์ ๋ชฉ๋ก์ ๊ฐ์ ธ์ ์ฌ์
IpInputScreen: ๋ฉ์ธ ํ๋ฉด์ผ๋ก, IP ์ ๋ ฅ, ์์ ์คํธ๋ฆฌ๋ฐ, ์์ฑ ์ก์ /์์ , ๋์ด๋ฝ ์ ์ด๋ฅผ ์ ๊ณตVideoListScreen: Firebase Storage์ ์ ์ฅ๋ ๋ นํ ์์ ๋ชฉ๋ก์ ํ์VideoPlayerScreen: ์ ํ๋ ์์์ ์ฌ์
์ฌ์ฉ์๊ฐ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ IP ์ฃผ์๋ฅผ ์ ๋ ฅํ์ฌ ์ฑ์ ๋ชจ๋ ๊ธฐ๋ฅ์ด ํด๋น IP๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋๋ก ์ค์
ํต์ฌ ์ฝ๋:
void _saveIp() {
setState(() {
_savedIp = _ipController.text;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("IP ์ฃผ์๊ฐ ์ ์ฅ๋์์ต๋๋ค: $_savedIp")),
);
}
์ฌ์ฉ์๊ฐ ์นด๋ฉ๋ผ ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์์ ์์ ์คํธ๋ฆฌ๋ฐ์ ์์ํ๊ฑฐ๋ ์ค์ง
ํต์ฌ ์ฝ๋:
void _toggleWebcam() {
if (_savedIp.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("๋จผ์ IP ์ฃผ์๋ฅผ ์ ์ฅํด์ฃผ์ธ์.")),
);
return;
}
setState(() {
if (!_showWebcam) {
_showWebcam = true;
_webViewController.loadRequest(
Uri.parse('http://$_savedIp:5000/stream'));
} else {
_showWebcam = false;
_webViewController.loadRequest(
Uri.parse('http://$_savedIp:5000/stop_stream'));
}
});
}
์ฌ์ฉ์๊ฐ ๋ง์ดํฌ ๋ฒํผ์ ๋๋ฅด๊ณ ์๋ ๋์ ์์ฑ์ ๋ น์ํ์ฌ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ ์ ์ก
ํต์ฌ ์ฝ๋:
GestureDetector(
onLongPressStart: (details) {
setState(() {
isRecordPressed = true;
});
startRecording();
},
onLongPressEnd: (details) async {
setState(() {
isRecordPressed = false;
});
await stopRecording();
await uploadAudio();
},
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: isRecordPressed ? Colors.blue : Colors.white,
shape: CircleBorder(),
padding: const EdgeInsets.all(20),
),
child: Icon(
Icons.mic,
color: isRecordPressed ? Colors.white : Colors.blue,
size: 30,
),
),
),์ฌ์ฉ์๊ฐ ์คํผ์ปค ๋ฒํผ์ ๋๋ฌ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ ๋ง์ดํฌ ์ ๋ ฅ์ ์ค์๊ฐ์ผ๋ก ์ถ๋ ฅ
ํต์ฌ ์ฝ๋:
Future<void> _startStreaming() async {
if (_savedIp.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("๋จผ์ IP ์ฃผ์๋ฅผ ์ ์ฅํด์ฃผ์ธ์.")),
);
return;
}
final String audioUrl = "http://$_savedIp:5000/audio";
try {
await _audioPlayer.stop();
await _audioPlayer.setUrl(audioUrl);
_audioPlayer.play();
setState(() {
_isPlaying = true;
});
} catch (e) {
print("Error starting audio stream: $e");
}
}์ฌ์ฉ์๊ฐ ๋์ด๋ฝ ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด์ ์ ํธ๋ฅผ ์ ์กํ์ฌ ๋์ด๋ฝ์ ์ ๊ทธ๊ฑฐ๋ ํด์ ์ ํธ ์ ๋ฌ
ํต์ฌ ์ฝ๋:
Future<void> _sendRequest() async {
if (_savedIp.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("๋จผ์ IP ์ฃผ์๋ฅผ ์ ์ฅํด์ฃผ์ธ์.")),
);
return;
}
final url = Uri.parse("http://$_savedIp:5000/trigger");
try {
final response = await http.get(url);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์ ์ก๋์์ต๋๋ค.")),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("์์ฒญ ์คํจ: $e")),
);
}
}Firebase Storage์์ ๋ นํ๋ ์์ ๋ชฉ๋ก์ ๊ฐ์ ธ์ ์ฌ์ฉ์์๊ฒ ํ์ํ๋ฉฐ, ์ฌ์ฉ์๋ ์ํ๋ ์์์ ์ ํํ์ฌ ์ฌ์
Firebase Storage์์ ๋ นํ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ:
Future<void> _fetchVideoList() async {
try {
final ref = FirebaseStorage.instance.ref('videos');
final result = await ref.listAll();
final videoInfo = await Future.wait(
result.items.map((fileRef) async {
final metadata = await fileRef.getMetadata();
final url = await fileRef.getDownloadURL();
final createdTime = metadata.timeCreated ?? DateTime.now();
final formattedTime = '${createdTime.year}-${createdTime.month.toString().padLeft(2, '0')}-${createdTime.day.toString().padLeft(2, '0')} ${createdTime.hour.toString().padLeft(2, '0')}:${createdTime.minute.toString().padLeft(2, '0')}';
return {'url': url, 'name': formattedTime};
}).toList(),
);
setState(() {
videoData = videoInfo;
isLoading = false;
});
} catch (e) {
print('Error fetching video list: $e');
setState(() {
isLoading = false;
});
}
}๋น๋์ค ์ฌ์:
lass VideoPlayerScreen extends StatefulWidget {
final String videoUrl;
const VideoPlayerScreen({required this.videoUrl});
@override
_VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
late VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(widget.videoUrl)
..initialize().then((_) {
setState(() {});
_controller.play();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Play Video'),
),
body: Center(
child: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: CircularProgressIndicator(),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}๋์ ๊ฐ์ง ์ผ์ & ๋ นํ ๊ธฐ๋ฅ
ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย
- ๋ฐฉ๋ฌธ์์ ์์ง์ ๊ฐ์ง ์ ์๋์ผ๋ก ๋ นํ ์์
- ์ผ์ ์๊ฐ ๋์ ๊ฐ์ง๋๋ฉด ์ค๋งํธํฐ์ผ๋ก ๋ฌธ ์์ ๋๊ฐ ์๋ค๊ณ ์๋ฆผ ์ ์ก
- ๋ นํ๋ ์์์ ์ค๋งํธํฐ ์ฑ์์ ํ์ธ ๊ฐ๋ฅ
์ด์ธ์ข ๊ธฐ๋ฅ
- ์ด์ธ์ข ๋ฒํผ ๋๋ฅผ ์ ๋ถ์ ๋ฅผ ํตํด "๋ฉ๋" ์๋ฆฌ๊ฐ ๋จ
- ์ค๋งํธํฐ์ผ๋ก ๋๊ตฐ๊ฐ๊ฐ ์ด์ธ์ข ์ ๋๋ ๋ค๊ณ ์๋ฆผ ์ ์ก
ํ์ ํตํ ๊ธฐ๋ฅ
ํตํ ์์์ ๋ณด๋ ค๋ฉด ํตํ ํด๋ฆญ
- ์ด์ธ์ข ์ ์นด๋ฉ๋ผ๋ก ๋ฐ๊นฅ ์ํฉ์ ์ค์๊ฐ์ผ๋ก ์ฑ์์ ํ์ธ
- ์ค๋งํธํฐ ์ฑ์์ ์์ฑ ๋ฐ ์์ ํตํ ์คํ
- ์ด์ธ์ข ๋ง์ดํฌ๋ก ์์ฑ ์ ๋ฌ, ์ค๋งํธํฐ ์คํผ์ปค๋ก ์ถ๋ ฅ
- ์ค๋งํธํฐ ๋ง์ดํฌ๋ก ์์ฑ ์ ๋ฌ, ์ด์ธ์ข ์คํผ์ปค๋ก ์ถ๋ ฅ
| Profile | Role | Part |
|---|---|---|
| ํ์ฅ | - ํ๋ก์ ํธ ๋ฆฌ๋ - ํ๋ก์ธ์ค ์ผ์ ๋ฐ ์ ๋ฐ์ ์ธ ๊ด๋ฆฌ - ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด ํตํ ํ๋ก์ธ์ค ๊ตฌ์ถ - ์นด๋ฉ๋ผ ์ฐ๊ฒฐ ๋ฐ ํ๋์จ์ด ์ฐ๊ฒฐ - ํ์ ํ๋ก์ธ์ค ๊ตฌ์ถ |
|
| ํ์ | - ๋ฉ์ธ ์ค๋ ๋ ๊ตฌ์ถ - ํ๋ก์ธ์ค ์ฐ๊ฒฐ ๋ด๋น - ์ด์ธ์ข , ๋์ ๊ฐ์ง ์ผ์ ๋ฑ ํ๋์จ์ด ์์คํ ๊ด๋ฆฌ |
|
| ํ์ | - ๋ฐ์ดํฐ ์ก์์ ๊ด๋ฆฌ - firebase ์๋ฒ ๊ตฌ์ถ ๋ฐ ์์ผ ์ฐ๊ฒฐ - ์ค๋งํธํฐ ํตํ ํ๋ก์ธ์ค ๊ตฌ์ถ - ํ์ ํ๋ก์ธ์ค ๊ตฌ์ถ - flutter ์ฑ ๊ฐ๋ฐ |
|
| ํ์ | - ๋
นํ ์์คํ
๊ฐ๋ฐ - ๋ นํ ํ์ผ ์ ์ฅ ๋ฐ ์ ์ก ์์คํ ๊ตฌ์ถ |
- ์ค์๊ฐ ์์ฑ ๋ฐ ์์ ์ ์ก: Raspberry Pi์ ์ฑ ๊ฐ์ ์ค์๊ฐ ์์ฑ ์ ๋ฌ ๋ฐ ์คํธ๋ฆฌ๋ฐ ๊ธฐ๋ฅ ๊ตฌํ ์ฑ๊ณต
- ์์ง์ ๊ธฐ๋ฐ ๋ นํ: PIR ์ผ์๋ฅผ ํตํ ์์ง์ ๊ฐ์ง ๋ฐ ๋ นํ, Firebase Storage๋ก ๋ นํ ์์ ์ ๋ก๋ ์์คํ ๊ตฌ์ถ
- ์ฅ์น ๊ฐ ์ถฉ๋ ๋ฌธ์ ํด๊ฒฐ: ์ค๋์ค, ์นด๋ฉ๋ผ ๋ชจ๋ ๋ฑ ์ฅ์น ๊ฐ ๊ฐ์ญ ๋ฌธ์ ๋ฅผ ์ํํธ์จ์ด ๋ฐ ํ๋์จ์ด์ ์ผ๋ก ํด๊ฒฐ
-
์ค์๊ฐ ์์ฑ ์ ๋ฌ ๊ฐ ์ค๋ฅ ๋ฐ์
- ๋ฌธ์ : ์์ฑ ์ฒ๋ฆฌ ์๋๊ฐ ์์ฑ ํ์ผ ์์ฑ ์๋๋ฅผ ๋ฐ๋ผ๊ฐ์ง ๋ชปํด ์ค๋งํธํฐ์์์ WAV ํ์ผ ์ ๋ฌ ๊ฐ ํ ์ค๋ฒํ๋ก์ฐ ๋ฐ์
- ํด๊ฒฐ ๋ฐฉ์: ์ฑ์์ "๋๋ฌ์ ๋งํ๊ธฐ" ๊ธฐ๋ฅ ๋์ ์ผ๋ก ์์ฑ ํ์ผ ์์ฑ๊ณผ ์ ์ก ๊ฐ์ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ๊ฐ์
-
์ค๋์ค ์ฅ์น ์ค์ ๊ฐ ๋ฌธ์ ๋ฐ์
- ๋ฌธ์ : ๊ธฐ๋ณธ ์ค๋์ค ์ ๋ ฅ ๋ฐ ์ถ๋ ฅ ์ฅ์น ๊ฐ์ ์ถฉ๋
- ํด๊ฒฐ ๋ฐฉ์:
- pyaudio, pygame ๋๋ค ๊ธฐ๋ณธ ์ฅ์น ๋ฒํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ถฉ๋
- ALSA์์ ๊ธฐ๋ณธ ์ฅ์น ์ค์ ๋ณ๊ฒฝ (
/etc/asound.conf) - Pygame์ ๊ธฐ๋ณธ ์ฅ์น๋ก 3.5mm ์คํผ์ปค ์ฌ์ฉ, PyAudio๋ ์นด๋ ๋ฒํธ๋ก ๋ง์ดํฌ ์ฅ์น ์ง์
-
์ฑ์์ ์ ์กํ ์์ฑ ์ฌ์ ๋ถ๊ฐ ๋ฌธ์
- ๋ฌธ์ : ๋ น์, ์ ์ก, ์ ์ฅ ์ฑ๊ณต ํ Pygame์์ ์ฌ์ ์ค๋ฅ ๋ฐ์
- ํด๊ฒฐ ๋ฐฉ์: ์ฑ์ ์์ฑ ์ฝ๋ฑ(Encoder)๊ณผ Raspberry Pi ์ฌ์ ์ฝ๋ฑ(Decoder)์ ์ผ์น์์ผ ์ค๋ฅ ์ ๊ฑฐ
-
์นด๋ฉ๋ผ ๊ด๋ จ ์ฝ๋ ํตํฉ ๊ฐ ์ถฉ๋
- ๋ฌธ์ : ์คํธ๋ฆฌ๋ฐ(Picamera2)๊ณผ ๋ นํ(Libcamera-Vid) ๋์ ์ฒ๋ฆฌ ์ ํ๋์จ์ด ์ฑ๋ฅ ๋ฌธ์ ๋ก ํ๋ ์ ์ ํ ๋ฐ์
- ํด๊ฒฐ ๋ฐฉ์:
- Lock์ ์ฌ์ฉํด ์นด๋ฉ๋ผ ๋ชจ๋ ์ถฉ๋ ๋ฐฉ์ง
- ์คํธ๋ฆฌ๋ฐ: ๋ก๋ฉ ์๋๊ฐ ๋น ๋ฅธ Picamera2 ์ฌ์ฉ
- ๋ นํ: ์์ ์ ์ธ Libcamera-Vid ์ฌ์ฉ
-
PIR ์ผ์ ๋ฏผ๊ฐ๋ ๋ฌธ์
- ๋ฌธ์ : PIR ์ผ์์ High ์ ํธ ์ ๋ฌ ์๊ฐ์ด ๊ธธ์ด ๋ถ๊ท์น์ ๋์ ๋ฐ์
- ํด๊ฒฐ ๋ฐฉ์:
- ๊ฐ๋ณ ์ ํญ์ด ๋ฌ๋ฆฐ PIR ์ผ์๋ก ๊ต์ฒด
- ํ๋์จ์ด์ ์ผ๋ก ํ๋น ์ฐจ๋จ ๋ฐ ์ธก์ ๋ฒ์ ์ ํ
- ์ํํธ์จ์ด์ ์ผ๋ก LOW ์ํ์์๋ ๋ นํ ์ง์๋๋๋ก ์กฐ์
-
3.5mm ์ค๋์ค ๊ฐ์ญ ๋ฌธ์
- ๋ฌธ์ : GPIO ํ ๊ฐ์ญ์ผ๋ก ์ธํด ์คํผ์ปค์์ ๋น์ ์์ ์ธ ์์ ๋ฐ์
- ํด๊ฒฐ ๋ฐฉ์: ์๋ณด๋ชจํฐ์ GPIO ํ์ 18, 19์์ 13๋ฒ์ผ๋ก ๋ณ๊ฒฝํ๊ณ SoftPWM ๋ฐฉ์์ผ๋ก ๊ฐ์ญ ์ ๊ฑฐ
-
์ผ๊ตด ์ธ์์ ํตํ ์ฌ์ฉ์ ์๋ฆผ
- OpenCV๋ YOLO์ ๊ฐ์ ์ผ๊ตด ์ธ์ ๊ธฐ์ ์ ํ์ฉํด ๋ฐฉ๋ฌธ์๋ฅผ ๋ฑ๋ก ๋ฐ ์๋ณ
- ์ผ๊ตด ๋ฑ๋ก์ ํตํด "[๋ฐฉ๋ฌธ์ 1]๋์ด ๋์ฐฉํ์์ต๋๋ค."์ ๊ฐ์ ๋ง์ถคํ ์๋ฆผ ์ ๊ณต
- ๋ณด์ ์์ค์ ๋์ด๊ณ , ๊ฐ์กฑ ๋ฐ ์น๊ตฌ์ ๊ฐ์ ์์ฃผ ๋ฐฉ๋ฌธํ๋ ์ฌ๋๋ค์๊ฒ ํธ๋ฆฌํจ ์ ๊ณต
-
์์ฑ ํ์ง ํฅ์
- ์์ ์ ๊ฑฐ ๋ฐ ํํฐ๋ง ๊ธฐ์ ์ ํ์ฉํ ์์ฑ ํ์ง ๊ฐ์ (์: WebRTC, Noise Suppression)
- ๋ง์ดํฌ์ ์คํผ์ปค ๊ฐ ๊ฐ์ญ ์ต์ํ๋ฅผ ์ํ ํ๋์จ์ด ๋ฐ ์ํํธ์จ์ด ์ต์ ํ
- ๋ ์์ฐ์ค๋ฌ์ด ์๋ฐฉํฅ ์์ฑ ํต์ ์ง์
-
์์ ์์ฑ ์ ๋ฌ ๊ตฌํ
- ์ค๋งํธํฐ ์ฑ์์ ์ฒญํฌ ๋จ์๋ก ์์ฑ ํ์ผ ์ ๋ฌํ๋ ๊ธฐ๋ฅ ๊ตฌํํ์ผ๋ ์ค๋ฒํ๋ก์ฐ ๋ฐ์
- ํํฐ๋ง ๊ธฐ์ ์ ํตํ ํ์ผ ์ ์ก ์ต์ํ๋ก ์ด๋ฆฐ ๋ง์ดํฌ ๊ธฐ๋ฅ ๊ตฌํ
-
์ฌ์ฉ์ ํธ์์ฑ ์ฆ๋
- ์ฑ UI ๊ฐ์ : ์ฌ์ฉ์ ๊ฒฝํ์ ์ค์ฌ์ผ๋ก ์ง๊ด์ ์ธ ๋์์ธ์ผ๋ก ๋ฆฌ๋ด์ผ
- ์๋ฆผ ์ปค์คํฐ๋ง์ด์ง ๊ธฐ๋ฅ ์ถ๊ฐ: ์ฌ์ฉ์๊ฐ ์๋ฆผ ํค, ์ง๋ ํจํด ๋ฑ์ ์ ํํ ์ ์๋๋ก ์ค์ ๋ฉ๋ด ์ ๊ณต
- ๋ฐฉ๋ฌธ์ ๊ธฐ๋ก ํ์ธ ๊ธฐ๋ฅ ์ถ๊ฐ: ๋ฐฉ๋ฌธ์ ํ์คํ ๋ฆฌ๋ฅผ ๋ ์ง๋ณ๋ก ํ์ธ ๊ฐ๋ฅ
-
๋ณด์ ๊ฐํ
- ๋์์ ๋ฐ์ดํฐ์ ์ํธํ ๋ฐ ํด๋ผ์ฐ๋ ์ ์ฅ ์ ๋ณด์ ๊ฐํ
- ์ฑ๊ณผ ๋ผ์ฆ๋ฒ ๋ฆฌํ์ด ๊ฐ ํต์ ์ HTTPS ์ ์ฉ์ผ๋ก ๋ฐ์ดํฐ ์ ์ก ์ค ๋ณด์ ํ๋ณด
- ์๋ฆผ์ ์ด์ค ์ธ์ฆ ๊ธฐ๋ฅ ์ถ๊ฐ๋ก ์ฌ์ฉ์์ ์ ๋ณด ๋ณดํธ















