사건의 시작
2주 전 대표님과 이야기를 나누면서 대표님들(저희는 대표님이 공동 대표로 2분이십니다)이 사무실 상주여부를
특정 인원(대부분 팀장)들이 알 수 있는 방법이 있냐는 질문을 받았습니다.
직원들이 대표님들이 자리에 안계실 때 헛걸음 하는 일이 많았던 것 같습니다.
noty는 MS Teams webhook으로 간단하게 메세지를 전달하면 되는 거였고,
사무실(특정 공간)에 존재하는 지 여부를 어떻게 알 수 있을까가 가장 큰 고민이었습니다.
android native app
몇 년 전 학원을 다니면서 출석을 스마트폰 앱을 통해 “비콘(beacon)”으로 인식하는 시스템을 경험한 게 불현 듯 생각났습니다. (이 생각을 하지 말았어야 했습니다)
아까운 2만원..
비콘을 이용하면 사무실에 비콘을 두고, 스마트폰에 앱이 비콘 근처에 있을 때 webhook을 통해 메세지를 전달하면 되는 간단한(?) 문제처럼 보였습니다.
무슨 자신감이였는지 안드로이드 앱 개발을 시작하기로 했습니다.
foreground 에서의 어려움
안드로이드 앱 개발 이야기는 너무 길어지기 때문에 생략하겠습니다…
결론은 안드로이드 앱을 foreground 에서 실행하면서 주기적으로 비콘과 거리를 측정하는 것은 실패했습니다.
- 안드로이드 버전에 따른 API 제약
- 원인을 알 수 없는 crash
- 앱을 화면에서 실행할 때와 background에서 실행할 때의 큰 제약
거의 1주일을 투자했지만, 원하는 결과를 얻지 못했습니다.
다시 생각해보자
대표님께 앱은 어려울 것 같다고 말씀드리고, 다시 생각해보기로 했습니다.
방에 있다는 걸 가장 직관적으로 알 수 있는 방법은 무엇일까.
“전등” 이었습니다.
출근하거나 퇴근할 때 “전등”은 절대 빼먹지 않고 on / off를 하신다는 거였습니다.
어쩌면 당연한 거였습니다.
라즈베리파이와 아두이노와 같은 SBC(Single Board Computer)에 조도 센서를 연결해서 주기적으로 API를 호출하면 되는 간단한 상황입니다.
이번에 썼던 bh1750 조도 센서입니다. 3000원 정도에 구매할 수 있었습니다.
Raspberry Pi
라즈베리 파이(Raspberry Pi)는 영국의 Raspberry Pi Foundation에서 만든 저가형 단일 기판 컴퓨터(Single-Board Computer; SBC) 제품군으로, 교육용 프로젝트의 일환으로 개발되었다.
1980년대 BBC의 컴퓨터 교육 프로젝트였던 BBC Micro에서 영감을 받았다고 하는데, 예를 들어 영국에서 개발되었고, 다양한 I/O 포트가 지원되고, 높은 hackability와 그 시절 8비트 컴퓨터처럼 모니터가 없어도 (아날로그) TV에 끼워 쓸 수 있도록 컴포지트 출력이 지원된다는 점 등이 닮아 있다.
라즈베리파이는 간단하게 말해 아주 저렴하고 작은 컴퓨터입니다.
Debian 계열의 리눅스를 사용하며, 다양한 I/O 포트를 지원합니다.
SD카드에 OS를 설치하여 우리가 늘 하듯이 사용하면 됩니다.
라즈베리파이 zero 2W 제품을 구매해서 아래와 같이 코딩해봤습니다.
import smbus2
import time
import requests
import os
from datetime import datetime
import RPi.GPIO as GPIO
# BH1750 주소
BH1750_ADDRESS = 0x23
# 명령어 (조도 측정 명령어)
BH1750_CMD = 0x10
# 임계값 설정
LUX_THRESHOLD = 200 # todo: 장소마다 다르게 셋팅 해야함 LED를 켜기 위한 조도 임계값 (단위: lux)
API_ENDPOINT = "https://example.api.com"
# GPIO 핀 번호 설정 (BCM 모드)
LED_PIN = 18
# GPIO 설정
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)
def read_light():
bus = smbus2.SMBus(1) # I2C 버스 1 사용
data = bus.read_i2c_block_data(BH1750_ADDRESS, BH1750_CMD, 2)
bus.close()
# 조도 계산
light_level = (data[0] << 8) | data[1]
light_level /= 1.2 # 센서 해상도 보정
return light_level
def send_alert(light_level, status):
try:
username = os.getenv('USER') or os.getenv('LOGNAME') or os.getenv('USERNAME') # 현재 사용자 이름 가져오기
if not username:
username = "unknown_user"
payload = {
"lux": light_level,
"deviceId": username,
"status": status
}
response = requests.post(API_ENDPOINT, json=payload)
if response.status_code == 201:
print(f"Alert sent successfully: {response.json()}")
else:
print(f"Failed to send alert: {response.status_code}")
except Exception as e:
print(f"Error sending alert: {e}")
def control_led(light_level):
if light_level <= LUX_THRESHOLD:
GPIO.output(LED_PIN, GPIO.HIGH) # LED 켜기
print("LED ON")
return "NOT_EXISTS"
else:
GPIO.output(LED_PIN, GPIO.LOW) # LED 끄기
print("LED OFF")
return "EXISTS"
def get_sleep_duration():
now = datetime.now()
# 주말 확인 (토요일: 5, 일요일: 6)
if now.weekday() in [5, 6]:
return 1800 # 주말에는 30분 (1800초)
# 저녁 22시부터 다음 날 오전 8시까지는 5분 주기
elif (now.hour >= 22) or (now.hour < 8):
return 300 # 5분 (300초)
else:
return 10 # 10초
if __name__ == "__main__":
try:
while True:
light_level = read_light()
status = control_led(light_level) # 조도에 따라 LED 제어
send_alert(light_level, status) # 무조건 보내고 비즈니스 로직은 API에서 처리
sleep_duration = get_sleep_duration()
time.sleep(sleep_duration)
except KeyboardInterrupt:
print("Program interrupted by user")
finally:
GPIO.cleanup() # GPIO 핀 정리
I2C로 조도센서를 연결하여 특정 주기동안 조도를 측정하고, 임계값을 넘으면 LED를 켜고 API를 호출하는 코드입니다.
해당 코드를 systemd로 등록하여 부팅 시 자동 실행하면 됩니다.
라즈베리파이까지 필요할까?
굉장히 간단한 기능이기 때문에 라즈베리파이까지 필요할까라는 생각이 들었습니다.(아두이노를 몇 일 더 빨리 알았더라면..)
조금 찾아보니 라즈베리파이 대신 아두이노를 사용하면 더 간단하게 구현할 수 있을 것 처럼 보였습니다.
특정 IDE(arduino IDE, Arduino Studio)만 사용할 수 있는 점부터 뭔가 거부감이 들었지만
교육용으로 쓸만큼 간단한 코드를 작성하는 데는 무리가 없을 것 같았습니다.
Arduino
Arduino is an open-source electronics platform based on easy-to-use hardware and software.
아두이노는 사용하기 쉬운 하드웨어 및 소프트웨어를 기반으로 하는 오픈 소스 전자 플랫폼입니다
아두이노는 아두이노 개발환경을 통해 cpp로 코드를 작성하고 컴파일 후 usb로 연결하여 바로 업로드 하는 형식입니다.
라즈베리파이와는 다르게 리눅스 os같은 건 없고, flash memory라는 영역에 코드를 업로드하여
전원이 연결되면 바로 실행되게 됩니다.
전력도 매우 적게 먹기 때문에(발열도 훨씬 적음) 이번과 같이 간단한 기능을 구현할 때는 아두이노가 훨씬 적합합니다.
#include <Wire.h>
#include <BH1750.h>
#include <WiFiS3.h>
#include <time.h>
#include <LiquidCrystal_I2C.h>
// 기기 ID 설정
const char* deviceId = "arduino-002";
// WiFi 설정
const char* ssid = "와이파이 아이디";
const char* password = "와이파이 비밀번호";
// API 엔드포인트
const char* API_ENDPOINT = "엔드포인트";
const char* HOST = "호스트";
// 임계값 설정
const int LUX_THRESHOLD = 200;
BH1750 lightMeter;
unsigned long previousMillis = 0;
const long interval = 10000; // 10초
unsigned long wifiCheckMillis = 0;
const long wifiCheckInterval = 30000; // 30초 간격으로 WiFi 상태 확인
// LCD 핀 설정
LiquidCrystal_I2C lcd(0x27, 16, 2); // I2C 주소는 0x27로 가정, LCD는 16x2
void setup() {
Serial.begin(9600);
Wire.begin();
if (!lightMeter.begin()) {
Serial.println(F("BH1750 initialization failed"));
while (1);
}
Serial.println(F("BH1750 Test"));
// WiFi 연결 설정
connectToWiFi();
// LCD
lcd.init();
lcd.backlight(); // 백라이트 켜기
// LCD 초기화
lcd.begin(16, 2); // LCD의 열과 행 수를 설정
}
void loop() {
unsigned long currentMillis = millis();
// 10초마다 조도값 읽고 서버로 전송
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
float lux = lightMeter.readLightLevel();
Serial.print("Light: ");
Serial.print(lux);
Serial.println(" lx");
String status = (lux <= LUX_THRESHOLD) ? "NOT_EXISTS" : "EXISTS";
sendAlert(lux, status); // HTTP 요청 전송
}
// 30초마다 WiFi 상태 확인
if (currentMillis - wifiCheckMillis >= wifiCheckInterval) {
wifiCheckMillis = currentMillis;
checkWiFiConnection();
}
}
void connectToWiFi() {
// WiFi 모듈 체크
if (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
while (true);
}
// 이미 연결되어 있다면 반환
if (WiFi.status() == WL_CONNECTED) {
return;
}
String fv = WiFi.firmwareVersion();
if (fv < WIFI_FIRMWARE_LATEST_VERSION) {
Serial.println("Please upgrade the firmware");
}
lcd.setCursor(0, 0); // 첫 번째 줄 첫 번째 칸으로 커서 이동
lcd.print("Connectiong to WIFI.."); // 첫 줄 출력
// WiFi 네트워크에 연결 시도
Serial.print("Attempting to connect to WPA SSID: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
// 연결 대기
int retries = 10;
while (WiFi.status() != WL_CONNECTED && retries-- > 0) {
delay(1000);
Serial.print(".");
}
Serial.println("");
if (WiFi.status() == WL_CONNECTED) {
Serial.println("You're connected to the network");
} else {
Serial.println("Failed to connect to WiFi");
}
}
void checkWiFiConnection() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected. Attempting to reconnect...");
connectToWiFi();
} else {
Serial.println("WiFi is connected");
}
}
void sendAlert(float lightLevel, String status) {
connectToWiFi();
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected. Cannot send alert.");
return;
}
WiFiClient client;
if (!client.connect(HOST, 80)) {
Serial.println("Connection to server failed");
return;
}
String payload = "{\"lux\": " + String(lightLevel) + ", \"deviceId\": \"" + deviceId + "\", \"status\": \"" + status + "\"}";
client.println("POST " + String(API_ENDPOINT) + " HTTP/1.1");
client.println("Host: " + String(HOST));
client.println("User-Agent: ESP32");
client.println("Content-Type: application/json");
client.print("Content-Length: ");
client.println(payload.length());
client.println();
client.println(payload);
lcd.clear(); // LCD 초기화
lcd.setCursor(0, 0); // 첫 번째 줄 첫 번째 칸으로 커서 이동
lcd.print("lux: " + String(lightLevel)); // 첫 줄 출력
lcd.setCursor(0, 1); // 두 번째 줄 첫 번째 칸으로 커서 이동
lcd.print("status: " + status); // 첫 줄 출력
// 응답 확인
while (client.connected()) {
String line = client.readStringUntil('\n');
if (line == "\r") {
Serial.println("Headers received");
break;
}
}
String response = client.readString();
Serial.println("Response: " + response);
client.stop();
}
위 코드는 라즈베리파이의 파이썬 코드와 크게 다르지 않습니다.
LED 대신 LCD로 글씨를 찍어주는 정도네요.
아두이노는 라즈베리파이와 다르게 wifi 연결부터 직접 해줘야 합니다.
특별한 설명이 없어도 코드를 보면 어떻게 동작하는지 알 수 있을 것 같습니다.
이번에 해당 프로젝트를 경험하면서 알게 된 것들을 정리해보면..
- I2C(SDA, SCL)
- UART(TX, RX)
- jump wire와 색깔이 의미하는 것들
- VCC(+)와 GND(-)
- 브레드보드
- 저항(resistor)의 역할과 종류들
위와 같이 개발자로써 컴퓨터 공학 경험만 하다가 전자공학 영역의 경험을 하게된 재밌는 경험이었습니다.
마치며
이번 일을 겪으며 느낀 건 해결을 위해 가장 간단한 방법을 선택하는 것이 중요하다는 것입니다.
안드로이드 앱을 개발할 만한 중요한 이슈도 아니였고,
조금 더 정보를 찾고 해결방법을 모색했더라면 훨씬 더 쉽게 아두이노로 해결할 수 있었을 문제였는데 말이죠..
시간과 자원을 고려해 타협점을 찾는 것도 우리 개발자의 큰 능력이라는 걸 다시 한 번 느꼈습니다.
여러분도 비슷한 상황에 놓이게 되면, 가장 간단하고 빠른 방법을 선택해보세요.
- 간단하고 빠른 방법인지 아는 게 힘듬