IoT4Car: 아두이노 MKR1000으로 차량 텔레메트리 수집·분석하기
부품 및 소모품
부품
수량
아두이노 MKR WiFi 1000
1
SparkFun 로직 레벨 컨버터 (양방향)
1
SparkFun OBD-II UART 보드 (ELM327/STN1110 칩셋)
1
SparkFun OBD-II to DB9 케이블
1
앱 및 온라인 서비스
아두이노 IDE
freeboard.io (대시보드 시각화)
dweet.io (데이터 중계)
프로젝트 소개
배경
운전 중 계기판을 보며 '이 데이터들을 수집해 분석하면 어떨까?'라는 생각을 해보신 적 있나요? 주행 습관, 평균 연비, 신호 대기 시간 등 개인에게 유용한 정보부터 플릿 관리, 차량 상태 모니터링, 연비 효율 분석, 운전자 특성 파악까지 기업 입장에서도 가치 있는 데이터입니다. 아두이노 MKR 시리즈 같은 IoT 보드를 활용하면 직접 차량과 통신하며 클라우드에 텔레메트리 데이터를 올리는 장치를 만들 수 있습니다.
차량과 통신하기: OBD-II 인터페이스
OBD-II란? 차량의 자기 진단 시스템으로 1996년 이후 미국 판매 차량 의무 장착, 한국을 포함한 많은 국가에서 표준화. 5가지 신호 프로토콜 중 CAN 버스가 2008년 이후 미국 차량 필수. 16핀 커넥터를 통해 접근 가능.
OBD-II 16핀 커넥터 핀아웃
컨트롤러: 아두이노 MKR WiFi 1000
와이파이 내장으로 IoT에 적합. 더 넓은 커버리지가 필요하면 MKR GSM 1400 추천. 와이파이도 휴대폰 핫스팟으로 이동 중 인터넷 접속 가능.
통역 보드: SparkFun OBD-II UART 보드
차량의 OBD 프로토콜을 아두이노가 이해하는 UART 시리얼로 변환. STN1110 칩 탑재로 모든 주요 OBD-II 프로토콜(CAN 등) 지원.
SparkFun OBD-II UART 보드
레벨 시프터 필수!
OBD-II UART 보드는 5V 로직, MKR1000은 3.3V 로직. 직접 연결 시 MKR 손상 위험. 양방향 로직 레벨 컨버터로 5V ↔ 3.3V 변환 필요.
SparkFun 양방향 로직 레벨 컨버터
배선 연결
MKR 핀 13 (RX) ← 레벨 시프터 ← OBD 보드 TX
MKR 핀 14 (TX) → 레벨 시프터 → OBD 보드 RX
양쪽 GND 공통 연결
디버깅용 LCD 1602도 추가 연결 가능 (별도 튜토리얼 참조).
브레드보드 배선도 (녹색: MKR-OBD, 노란색: MKR-LCD)
실제 연결 모습 (마이크로 USB, OBD-II-DB9 케이블 포함)
중요: Serial이 아닌 Serial1 사용
MKR 보드에서 핀 13/14번 하드웨어 시리얼은 Serial1 객체입니다. Serial은 USB 디버그 포트이니 혼동하지 마세요.
void setup() {
Serial1.begin(9600); // OBD 통신용
}
void loop() {
Serial1.println(message); // 명령 전송
}
OBD 명령어 (PID)
서비스 모드 01 (현재 데이터)에서 필요한 PID:
010D – 차량 속도
010C – 엔진 RPM
012F – 연료 레벨
0105 – 냉각수 온도
OBD-II 서비스 모드 01
주요 PID 리스트 (위키피디아 참조)
응답 파싱
응답 형식: >1 0D 00 (첫 5자 에코, 이후 데이터). 캐리지 리턴(\r) 기준으로 수신 버퍼 처리.
void getResponse() {
while (Serial1.available() > 0) {
if (Serial1.peek() == '\r') {
inChar = Serial1.read();
rxData[rxIndex] = '\0';
rxIndex = 0;
} else {
inChar = Serial1.read();
rxData[rxIndex++] = inChar;
}
}
}
시리얼 모니터 응답 예시
데이터 변환 예시:
RPM: 0x098C / 4 = 611 rpm
연료: 0x1D / 255 * 100 = 11%
냉각수: 0x79 - 40 = 81°C
실차 테스트 중 (LCD로 실시간 표시)
데이터 클라우드 업로드 (dweet.io + freeboard.io)
MKR1000의 와이파이 기능을 이용해 10초 주기로 dweet.io에 데이터 전송 → freeboard.io에서 게이지 위젯으로 시각화.
void httpRequest() {
client.stop();
if (client.connect(server, 80)) {
String data = "POST /dweet/for/mkr1000?RPM=";
data.concat(vRPM);
data.concat("&Speed="); data.concat(vSpeed);
data.concat("&Fuel="); data.concat(vFuel);
data.concat("&Temp="); data.concat(vTemp);
client.println(data);
client.println("Host: https://www.dweet.io");
client.println("Connection: close");
client.println();
} else {
lcd.clear(); lcd.setCursor(0,0); lcd.println("Connection failed");
}
}
freeboard.io 대시보드 (속도, RPM, 연료, 냉각수 온도)
결론
실차 테스트 성공. 향후 PCB 일체형 설계 예정. MKR GSM 1400 등으로 확장 가능. 운전 중 디버깅은 위험하니 안전에 유의하세요!
전체 코드 (IoT4Car_code)
/* OBDII-UART-Serial v9
* Author: zhaoshentech
* Updated: 2018-08-27
*/
#include <SPI.h>
#include <WiFi101.h>
#include <LiquidCrystal.h>
char ssid[] = "YOUR_WIFI_SSID";
char pass[] = "YOUR_WIFI_PASSWORD";
char server[] = "www.dweet.io";
unsigned long lastConnectionTime = 0;
const unsigned long postingInterval = 10L * 1000L;
WiFiClient client;
int status = WL_IDLE_STATUS;
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
char rxData[20];
char rxIndex = 0;
char inChar = 0;
String message;
int vSpeed = 0, vRPM = 0, vFuel = 0, vTemp = 0;
void setup() {
lcd.begin(16, 2); lcd.clear();
if (WiFi.status() == WL_NO_SHIELD) { lcd.println("WiFi not ready"); while(true); }
while (status != WL_CONNECTED) {
lcd.clear(); lcd.setCursor(0,0); lcd.println("Connecting WiFi...");
status = WiFi.begin(ssid, pass); delay(5000);
}
lcd.setCursor(0,1); lcd.println("Connected!");
Serial1.begin(9600);
resetBuffer();
}
void loop() {
while (status != WL_CONNECTED) {
lcd.clear(); lcd.setCursor(0,0); lcd.println("Connect WiFi...");
status = WiFi.begin(ssid, pass); delay(5000);
}
getSpeed(); getRPM(); getFuel(); getCoolTemp();
if (millis() - lastConnectionTime > postingInterval) {
httpRequest(); lastConnectionTime = millis();
}
}
void getRPM() {
message = "010C"; Serial1.println(message); delay(200);
for (int i=8;i<16;++i) { lcd.setCursor(i,0); lcd.write(' '); }
lcd.setCursor(8,0); getResponse();
vRPM = ((strtol(&rxData[6],0,16)*256) + strtol(&rxData[9],0,16))/4;
lcd.print(vRPM); lcd.print(" rpm");
}
void getSpeed() {
message = "010D"; Serial1.println(message); delay(200);
for (int i=0;i<8;++i) { lcd.setCursor(i,0); lcd.write(' '); }
lcd.setCursor(0,0); getResponse();
vSpeed = strtol(&rxData[6],0,16) * 0.621371; // km/h → mph
lcd.print(vSpeed); lcd.print(" mph");
}
void getFuel() {
message = "012F"; Serial1.println(message); delay(200);
for (int i=0;i<8;++i) { lcd.setCursor(i,1); lcd.write(' '); }
lcd.setCursor(0,1); getResponse();
vFuel = 1.0 * strtol(&rxData[6],0,16) / 255 * 100;
lcd.print(vFuel); lcd.print(" %");
}
void getCoolTemp() {
message = "0105"; Serial1.println(message); delay(200);
for (int i=8;i<16;++i) { lcd.setCursor(i,1); lcd.write(' '); }
lcd.setCursor(8,1); getResponse();
vTemp = strtol(&rxData[6],0,16) - 40;
lcd.print(vTemp); lcd.write(0xDF); lcd.print("C");
}
void getResponse() {
while (Serial1.available() > 0) {
if (Serial1.peek() == '\r') {
inChar = Serial1.read(); rxData[rxIndex] = '\0'; rxIndex = 0;
} else {
inChar = Serial1.read(); rxData[rxIndex++] = inChar;
}
}
}
void resetBuffer() { for (int i=0;i<20;++i) rxData[i]=0; }
void httpRequest() {
client.stop();
if (client.connect(server, 80)) {
String data = "POST /dweet/for/mkr1000?RPM=";
data.concat(vRPM); data.concat("&Speed="); data.concat(vSpeed);
data.concat("&Fuel="); data.concat(vFuel); data.concat("&Temp="); data.concat(vTemp);
client.println(data);
client.println("Host: https://www.dweet.io");
client.println("Connection: close"); client.println();
} else { lcd.clear(); lcd.setCursor(0,0); lcd.println("Connection failed"); }
}
회로도 (Fritzing 파일 포함)
MKR1000 + OBD-II UART + 레벨 시프터 + LCD 1602 연결도
Fritzing 파일: frizling_schematics_M8kF26dafQ.fzz