블루투스·센서·실시간 업데이트 지원 고급 스마트 농구 스코어보드
부품 및 소모품
| 부품 | 수량 | 비고 |
| 아두이노 나노 R3 | 1 | |
| HC-05 블루투스 모듈 | 1 | |
| E18-D80NK 적외선 근접 센서 | 1 | 햇빛 간섭이 적은 타입 권장 |
| Seeed Grove 진동 센서 (SW-420) | 1 | |
| DHT22 온습도 센서 | 1 | |
| 8mm RGB LED | 1 | |
| 저항 10kΩ | 3 | |
| 저항 100Ω | 3 | |
| 저항 47Ω | 1 | |
| 저항 1kΩ | 1 | |
| 저항 2.21kΩ | 1 | |
| 9V 배터리 | 1 | |
앱 및 온라인 서비스
- 아두이노 IDE
- MIT App Inventor (안드로이드 앱 개발)
프로젝트 소개
저는 정기적으로 농구 훈련을 하며 슛 횟수, 득점, 실책 등 통계를 기록합니다. 이를 수동으로 하기엔 지루하고 어렵습니다. 스마트 볼을 쓰면 되지만 단점이 있습니다:
- 여러 공을 쓰려면 비싼 스마트 볼을 여러 개 사야 함
- 정확도가 좋지 않음
- 공의 내구성 문제
이전 프로젝트(스마트 농구 스코어보드 프로토타입)에서는 Avnet SmartEdge 장치로 가속도 센서(슛 감지)와 근접 센서(득점 감지)를 테스트했습니다. 이번 프로젝트는 농구 스코어보드 작업을 위한 결정적인 솔루션을 구현하는 것입니다.
버전
초기에는 집에 있던 하드웨어로 1.1.0 버전을 개발해 잘 작동함을 확인했습니다. 이후 더 저렴한 부품으로 교체하고 소프트웨어를 개선한 2.0.0 버전을 만들었습니다.
1단계: 아두이노
하드웨어
버전 2.0.0의 부품 목록은 위 표를 참고하세요. 1.1.0에서 2.0.0으로의 주요 변경사항:
- 아두이노 메가 2560 → 아두이노 나노 R3로 변경
- 블루투스 실드 → HC-05 블루투스 모듈로 변경
- 충전식 리이온 배터리 → 9V 배터리로 변경
- DHT22 온습도 센서 추가
기타 부품 고려사항:
- E18-D80NK 적외선 근접 센서: 햇빛 간섭이 없는 타입을 권장
- SW-420 진동 센서: 다른 진동 센서도 가능하나 이 제품이 잘 작동함
두 버전의 회로도는 회로도 섹션에 있습니다.
소프트웨어
아두이노 IDE로 개발했으며 전략은 다음과 같습니다:
- 초기화(변수, LED, 블루투스 등) 후 센서 상태를 지속적으로 모니터링
- 근접 센서가 공을 감지하면 득점으로 판정
- 진동 센서가 움직임을 감지하면 슛 발생으로 보고 2초간 대기하며 판정
- 대기 중 근접 센서가 공을 감지하면 즉시 득점 처리
- 2초 후에도 근접 센서가 감지하지 못하면 실책으로 판정
- 블루투스로 안드로이드 앱에 슛 결과(득점/실책) 전송
- 과정 반복
버전 2.0.0에서의 주요 소프트웨어 변경:
- 블루투스 실드 지원 → HC-05 모듈 지원으로 변경
- DHT22 온습도 센서 지원 추가
- 슛/득점/실책 감지 로직 및 LED 신호 개선
두 버전의 코드는 코드 섹션에 있습니다.
2단계: 안드로이드 앱
MIT App Inventor로 개발했으며 전략:
- 초기화(슛/득점/실책 카운트, 블루투스 등) 후 '시작' 버튼 대기
- '시작' 버튼 누르면 블루투스 연결 지속 모니터링
- 데이터 수신 시 스코어보드 업데이트 및 알림음 재생
- '일시정지' 버튼 누를 때까지 반복
- 블루투스 기기 선택, 카운트 리셋, 보드 수동 조정 버튼 포함
버전 2.0.0 변경사항:
- 레이아웃 세로 → 가로로 변경
- 조도, 습도, 온도, 열지수 표시 추가
- 조정 버튼 개선
- 화면 항상 켜짐 기능 추가
- 모든 버튼에 누름 소리 추가
- 50슛 단위(50, 100, 150…) 도달 시 경고 기능 추가
두 버전의 안드로이드 코드는 코드 섹션에 있습니다.
3단계: 농구 골대 장착
제가 훈련에 쓰는 일반 농구 골대입니다.
골대 아래 플라스틱 커버를 떼고 근접 센서용 구멍을 뚫었습니다.
진동 센서용 작은 구멍도 뚫었습니다.
플라스틱 커버가 곡면이라 센서를 직접 부착할 수 없어 MDF로 지지대를 만들었습니다.
볼트와 너트로 근접·진동 센서를 고정했습니다.
나머지 부품들을 연결했습니다.
연기 테스트(전원 투입 테스트)를 진행했습니다.
최종적으로 골대에 모두 장착했습니다.
4단계: 농구 훈련 테스트
이제 모든 것을 테스트할 차례입니다. 그리고 대망의 실전 테스트!
5단계: 최종 고찰
- 시스템이 매우 정확하게 작동했으며, 거짓 양성(오인식)은 거의 없고 거짓 음성(미감지)도 드뭅니다.
- 실시간으로 훈련 통계를 확인하고 훈련 후에도 볼 수 있어 매우 유용합니다.
즐거운 훈련 되세요!
코드
아두이노 코드 (버전 1.1.0)
//----------------------------------------------------------------------------//
// Filename : Scoreboard.ino
// Description : Smart Basketball Scoreboard
// Version : 1.1.0
// Author : Marcelo Avila de Oliveira <marceloavilaoliveira@gmail.com>
//----------------------------------------------------------------------------//
// DEFINITIONS
//----------------------------------------------------------------------------//
// #define DEBUG
// #define DEBUG_PROX
// #define DEBUG_VIBR
//----------------------------------------------------------------------------//
// CONSTANTS
//----------------------------------------------------------------------------//
const int prox_pin = 2;
const int vibr_pin = 3;
const int led_r_pin = 4;
const int led_g_pin = 5;
const int led_b_pin = 6;
const unsigned long wait_interval = 3000;
const float percent_to_bright_factor = 100 * log10(2) / log10(255);
//----------------------------------------------------------------------------//
// VARIABLES
//----------------------------------------------------------------------------//
unsigned long wait_time;
boolean prox = false;
boolean vibr = false;
boolean wait = false;
//----------------------------------------------------------------------------//
// FUNCTIONS (SETTINGS)
//----------------------------------------------------------------------------//
void setup() {
pinMode(prox_pin, INPUT);
pinMode(vibr_pin, INPUT);
pinMode(led_r_pin, OUTPUT);
pinMode(led_g_pin, OUTPUT);
pinMode(led_b_pin, OUTPUT);
set_led(5, 100);
Serial.begin(9600);
setup_bluetooth();
set_led(4, 100);
#ifdef DEBUG
Serial.println("Board is alive");
Serial.println();
#endif
}
void setup_bluetooth() {
#ifdef DEBUG
Serial.println("Setting Bluetooth");
Serial.println();
#endif
Serial1.begin(38400);
Serial1.print("\r\n+STWMOD=0\r\n");
Serial1.print("\r\n+STNA=Arduino\r\n");
Serial1.print("\r\n+STOAUT=1\r\n");
Serial1.print("\r\n+STAUTO=0\r\n");
delay(2000);
Serial1.print("\r\n+INQ=1\r\n");
delay(2000);
while (Serial1.available()) {
delay(50);
Serial1.read();
}
}
//----------------------------------------------------------------------------//
// FUNCTIONS (LIGHT)
//----------------------------------------------------------------------------//
int percent_to_bright(int percent) {
return 256 - pow(2, percent / percent_to_bright_factor);
}
void set_led(int color, int bright) {
#ifdef DEBUG
Serial.println("Setting LED");
Serial.println();
#endif
if (color < 0 || color > 6 || bright < 0 || bright > 100) return;
int led_r_bright = 255, led_g_bright = 255, led_b_bright = 255;
int bright_aux = percent_to_bright(bright);
switch (color) {
case 0: led_g_bright = bright_aux; break; // GREEN
case 1: led_r_bright = bright_aux; led_g_bright = bright_aux; break; // YELLOW
case 2: led_r_bright = bright_aux; break; // RED
case 3: led_g_bright = bright_aux; led_b_bright = bright_aux; break; // CYAN
case 4: led_b_bright = bright_aux; break; // BLUE
case 5: led_r_bright = bright_aux; led_b_bright = bright_aux; break; // MAGENTA
case 6: led_r_bright = bright_aux; led_g_bright = bright_aux; led_b_bright = bright_aux; break; // WHITE
}
analogWrite(led_r_pin, led_r_bright);
analogWrite(led_g_pin, led_g_bright);
analogWrite(led_b_pin, led_b_bright);
}
//----------------------------------------------------------------------------//
// FUNCTIONS (CHECK)
//----------------------------------------------------------------------------//
void check_prox() {
if (!prox && digitalRead(prox_pin) == LOW) {
#ifdef DEBUG_PROX
Serial.println("Proximity detected");
Serial.println();
#endif
prox = true;
if (!vibr) { wait = true; wait_time = millis() + wait_interval; }
set_shot(1);
}
}
void check_vibr() {
if (!prox && !vibr && digitalRead(vibr_pin) == HIGH) {
#ifdef DEBUG_VIBR
Serial.println("Vibration detected");
Serial.println();
#endif
vibr = true;
wait = true;
wait_time = millis() + wait_interval;
set_led(1, 100);
}
}
void check_wait() {
if (wait && millis() > wait_time) {
if (!prox) set_shot(0);
reset();
}
}
//----------------------------------------------------------------------------//
// FUNCTIONS (MIS)
//----------------------------------------------------------------------------//
void set_shot(int mode) {
if (mode == 0) set_led(2, 100); else set_led(0, 100);
Serial1.print(mode);
delay(1000);
}
void reset() {
vibr = false; prox = false; wait = false;
set_led(4, 100);
}
//----------------------------------------------------------------------------//
// MAIN
//----------------------------------------------------------------------------//
void loop() {
check_prox();
check_vibr();
check_wait();
}
안드로이드 코드 (버전 1.1.0)
MIT App Inventor 프로젝트 파일 (다운로드 필요)
아두이노 코드 (버전 2.0.0)
//----------------------------------------------------------------------------//
// Filename : Scoreboard.ino
// Description : Smart Basketball Scoreboard
// Version : 2.0.0
// Author : Marcelo Avila de Oliveira <marceloavilaoliveira@gmail.com>
//----------------------------------------------------------------------------//
// LIBRARIES
//----------------------------------------------------------------------------//
#include "DHT.h"
#include "SoftwareSerial.h"
//----------------------------------------------------------------------------//
// DEFINITIONS
//----------------------------------------------------------------------------//
// #define DEBUG
// #define DEBUG_BLUE
// #define DEBUG_PROX
// #define DEBUG_VIBR
// #define DEBUG_DHT
#define DHTPIN 7
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
SoftwareSerial Bluetooth(10, 11); // RX, TX
//----------------------------------------------------------------------------//
// CONSTANTS
//----------------------------------------------------------------------------//
const int prox_pin = 2;
const int vibr_pin = 3;
const int led_r_pin = 4;
const int led_g_pin = 5;
const int led_b_pin = 6;
const unsigned long wait_interval = 2000;
const unsigned long dht_interval = 10000;
const float percent_to_bright_factor = 100 * log10(2) / log10(255);
//----------------------------------------------------------------------------//
// VARIABLES
//----------------------------------------------------------------------------//
unsigned long wait_time, dht_time;
float humi, temp, heat;
boolean prox = false, vibr = false, wait = false;
//----------------------------------------------------------------------------//
// FUNCTIONS (SETTINGS)
//----------------------------------------------------------------------------//
void setup() {
pinMode(prox_pin, INPUT);
pinMode(vibr_pin, INPUT);
pinMode(led_r_pin, OUTPUT);
pinMode(led_g_pin, OUTPUT);
pinMode(led_b_pin, OUTPUT);
set_led(5, 100); // MAGENTA
Serial.begin(9600);
setup_bluetooth();
dht.begin();
set_led(4, 100); // BLUE
#ifdef DEBUG
Serial.println("Board is alive");
Serial.println();
#endif
}
void setup_bluetooth() {
#ifdef DEBUG_BLUE
Serial.println("Setting Bluetooth");
Serial.println();
#endif
Bluetooth.begin(9600);
while (Bluetooth.available()) { delay(50); Bluetooth.read(); }
}
//----------------------------------------------------------------------------//
// FUNCTIONS (LIGHT) - 1.1.0과 동일
//----------------------------------------------------------------------------//
int percent_to_bright(int percent) {
return 256 - pow(2, percent / percent_to_bright_factor);
}
void set_led(int color, int bright) {
#ifdef DEBUG
Serial.println("Setting LED");
Serial.println();
#endif
if (color < 0 || color > 6 || bright < 0 || bright > 100) return;
int led_r_bright = 255, led_g_bright = 255, led_b_bright = 255;
int bright_aux = percent_to_bright(bright);
switch (color) {
case 0: led_g_bright = bright_aux; break;
case 1: led_r_bright = bright_aux; led_g_bright = bright_aux; break;
case 2: led_r_bright = bright_aux; break;
case 3: led_g_bright = bright_aux; led_b_bright = bright_aux; break;
case 4: led_b_bright = bright_aux; break;
case 5: led_r_bright = bright_aux; led_b_bright = bright_aux; break;
case 6: led_r_bright = bright_aux; led_g_bright = bright_aux; led_b_bright = bright_aux; break;
}
analogWrite(led_r_pin, led_r_bright);
analogWrite(led_g_pin, led_g_bright);
analogWrite(led_b_pin, led_b_bright);
}
//----------------------------------------------------------------------------//
// FUNCTIONS (CHECK)
//----------------------------------------------------------------------------//
void check_prox() {
if (!prox && digitalRead(prox_pin) == LOW) {
#ifdef DEBUG_PROX
Serial.println("Proximity detected");
Serial.println();
#endif
set_led(0, 100); // GREEN
send_data(2);
prox = true;
if (!vibr) { wait = true; wait_time = millis() + wait_interval; }
}
}
void check_vibr() {
if (!prox && !vibr && digitalRead(vibr_pin) == HIGH) {
#ifdef DEBUG_VIBR
Serial.println("Vibration detected");
Serial.println();
#endif
set_led(1, 100); // YELLOW
vibr = true;
wait = true;
wait_time = millis() + wait_interval;
}
}
void check_wait() {
if (wait && millis() > wait_time) {
if (!prox) { set_led(2, 100); send_data(1); } // RED, MISS
reset();
}
}
void check_dht() {
if (!prox && !vibr && millis() > dht_time) {
humi = dht.readHumidity();
temp = dht.readTemperature();
heat = dht.computeHeatIndex(temp, humi, false);
#ifdef DEBUG_DHT
Serial.print("Humidity : "); Serial.print(humi); Serial.println("%");
Serial.print("Temperature: "); Serial.print(temp); Serial.println("C");
Serial.print("Heat Index : "); Serial.print(heat); Serial.println("C");
Serial.println();
#endif
send_data(0);
dht_time = millis() + dht_interval;
}
}
//----------------------------------------------------------------------------//
// FUNCTIONS (MIS)
//----------------------------------------------------------------------------//
void send_data(int shot) {
// shot: 0=데이터 없음, 1=실책, 2=득점
Bluetooth.print(humi);
Bluetooth.print(temp);
Bluetooth.print(heat);
Bluetooth.print(shot);
#ifdef DEBUG_BLUE
Serial.println("Bluetooth sent");
Serial.println();
#endif
}
void reset() {
vibr = false; prox = false; wait = false;
delay(1000);
set_led(4, 100); // BLUE
}
//----------------------------------------------------------------------------//
// MAIN
//----------------------------------------------------------------------------//
void loop() {
check_prox();
check_vibr();
check_wait();
check_dht();
}
안드로이드 코드 (버전 2.0.0)
MIT App Inventor 프로젝트 파일 (다운로드 필요)
회로도