Never Ride Alone: 스마트 자전거 안전 시스템
부품 및 소모품
| 5mm 적색 LED |
2 |
| u-blox NEO-6M GPS 모듈 |
1 |
| Arduino MKR Fox 1200 |
1 |
| 3.7V 리튬 배터리 |
1 |
| 부저 |
1 |
| 푸시버튼 스위치 12mm |
1 |
| 슬라이드 스위치 |
1 |
| 1kΩ 저항 |
1 |
| Seeed Grove 진동 센서 (SW-420) |
1 |
필요 도구
프로젝트 개요
안녕하세요, 전자공학도이자 창업가, 그리고 사이클리스트인 Jesús Soriano입니다.
12살부터 자전거를 탔고 주로 혼자 훈련합니다. 그동안 수많은 낙차와 사고를 겪었습니다. 어느 날 깨달았습니다. 심한 사고로 의식을 잃으면 구조 요청을 할 수 없고, 산속에서 혼자라면 죽을 수도 있다.
기존 솔루션의 한계
- Strava Summit: 사고 시 GPS 위치 전송 가능하지만 휴대폰 신호 필요
- Specialized ANGi: 헬멧 내장, 충돌 감지 시 구조 요청. 역시 휴대폰 신호 + 배터리 + GPS 모두 필요
산악 지대에서는 휴대폰 신호가 안 터지는 경우가 많습니다.
Never Ride Alone (NRA) 탄생
Sigfox 네트워크를 발견했습니다. 저전력 광역 통신으로 휴대폰 신호 없이도 데이터 전송 가능. Arduino MKR FOX 1200(Sigfox 모듈 내장)을 알게 되어 바로 개발 시작.
NRA 작동 방식 - 역방향 알람
- 라이딩 시작 시 버튼 눌러 NRA 활성화. LED 점멸 (후방 시인성 향상)
- 30초간 움직임 없음 감지 시 → 비상 모드 진입
- 부저 울림. 사용자가 끄지 않으면(단순 깜빡임이 아님) 부상자로 판단
- 자동으로 GPS 작동 → Sigfox 네트워크로 위도/경도/고도 전송
- 등록된 지인/가족에게 이메일 발송 → 구글맵 링크로 위치 확인 가능
디자인 철학
다수 사이클리스트 인터뷰 결과: 사용이 간편하고 배터리 오래 가는 미니멀한 기기 원함. NRA는 버튼만 누르면 끝. 라이딩 후 정지 버튼 누르고 충전만 하면 됨.
케이스는 ABS 플라스틱, 발렌시아 폴리테크닉 대학 3D 프린터로 제작. 설계 파일: Tinkercad에서 편집/다운로드 가능
하드웨어 구성
- Arduino MKR FOX 1200
- NEO-6M GPS
- 버튼, LED 2개, 1kΩ 저항, 부저, 진동 센서, 3.7V 150mAh 배터리
Sigfox 백엔드 설정
콜백 설정으로 원시 데이터를 위도/경도/고도로 변환. 이메일로 구글맵 링크 자동 발송.
조립 과정
- GPS 모듈: VCC, GND, 데이터선만 연결. 안테나는 프레임에 접착
- 아두이노를 GPS 위에 적층
- 배터리 + 슬라이드 스위치 연결 (전원 ON/OFF)
- 배터리 충전 커넥터 추가 (충전 시 OFF 상태)
- 드론 배터리 충전기로 충전 테스트 완료
- 안테나를 아두이노 위에 배치, 모든 부품을 케이스에 수납
테스트 결과
- 진동 센서 미사용 버전: 전원 켜면 10분마다 GPS 위치 전송. 결과 양호
- 실제 도로 주행 테스트: 사용 편리, 안심감 제공
환경 보호 활동
테스트 라이딩 중 발견한 쓰레기 수거. 지속가능발전목표(SDGs) 실천. 산악 쓰레기 대부분이 사이클리스트 것(에너지바 포장지, 젤, 타이어 튜브)임을 발견. 대회 중 '클린 포인트' 운영으로 쓰레기 버리기 편하게 하고 경기 후 일괄 수거.
안전 라이딩 하세요! :)
코드 (NRA v0.1)
///// NEVER RIDE ALONE V.0.1 /////
#include <SigFox.h>
#include <ArduinoLowPower.h>
#include <TinyGPS.h>
#define WAITING_TIME 10
#define GPS_PIN 2
#define GPS_INFO_BUFFER_SIZE 128
bool debug = true;
const int backlights = 4;
const int pulsador = 5;
const int audio = 3;
TinyGPS gps;
int year; byte month, day, hour, minute, second, hundredths;
unsigned long chars; unsigned short sentences, failed_checksum;
char GPS_info_char, GPS_info_buffer[GPS_INFO_BUFFER_SIZE];
unsigned int received_char;
bool message_started = false; int i = 0;
struct gpscoord { float a_latitude; float a_longitude; float a_altitude; };
float latitude = 0.0f, longitude = 0.0f, altitud = 0;
void Wait(int m, bool s) {
if (debug) { Serial.print("Waiting: "); Serial.print(m); Serial.println(" min."); }
digitalWrite(LED_BUILTIN, LOW);
int seg = s ? m * 30 : m * 15;
for (int i = 0; i < seg; i++) {
digitalWrite(backlights, HIGH); delay(500);
digitalWrite(backlights, LOW); delay(500);
}
}
void SendSigfox(String data) {
if (debug) { Serial.print("Sending: "); Serial.println(data); }
SigFox.begin(); delay(100);
SigFox.status(); delay(1);
if (debug) SigFox.debug(); delay(100);
SigFox.beginPacket(); SigFox.print(data);
if (debug) {
int ret = SigFox.endPacket(true);
if (ret > 0) Serial.println("No transmission"); else Serial.println("Transmission ok");
} else { SigFox.endPacket(); }
SigFox.end();
}
String ConvertGPSdata(const void* data, uint8_t len) {
uint8_t* bytes = (uint8_t*)data; String cadena;
for (uint8_t i = len - 1; i < len; --i) {
if (bytes[i] < 12) cadena.concat(byte(0));
cadena.concat(char(bytes[i]));
}
return cadena;
}
String GetGPSpositon() {
int messages_count = 0; String pos;
if (debug) Serial.println("GPS ON");
digitalWrite(GPS_PIN, HIGH); Wait(1, false);
while (messages_count < 5000) {
while (Serial1.available()) {
int GPS_info_char = Serial1.read();
if (GPS_info_char == '$') messages_count++;
if (gps.encode(GPS_info_char)) {
gps.f_get_position(&latitude, &longitude);
altitud = gps.altitude() / 100;
gpscoord coords = {altitud, longitude, latitude};
if (debug) { /* 상세 출력 생략 */ }
digitalWrite(GPS_PIN, LOW);
return ConvertGPSdata(&coords, sizeof(gpscoord));
}
}
}
return "No Signal";
}
void setup() {
pinMode(4, OUTPUT); pinMode(backlights, OUTPUT);
pinMode(pulsador, INPUT); pinMode(audio, OUTPUT);
if (debug) { Serial.begin(9600); while(!Serial); Serial.println("Serial Connected"); }
Serial1.begin(9600); while(!Serial1); if (debug) Serial.println("GPS Connected");
pinMode(GPS_PIN, OUTPUT);
if (!SigFox.begin()) { Serial.println("Shield error!"); return; }
if (debug) SigFox.debug(); else SigFox.end();
}
void loop() {
String position_data = GetGPSpositon();
SendSigfox(position_data);
Wait(WAITING_TIME, false);
}
CAD 파일
MKR1200, 배터리, 버튼, LED, GPS, 부저 수납 가능: nra_v_0_1_qCsc7I6QvO.zip
회로도
회로도 이미지 참조 (아두이노 사진은 MKR1200 아님)