1. 이번 편 목표
지금까지 Carpybara는 웹 슬라이더로 속도를 수동 입력하는 시뮬레이터 방식이었습니다. 이번 편에서는 실제 차량 OBD2 포트에 Bluetooth 동글을 꽂아 속도·RPM·냉각수 온도·연료량·부하·전압을 실시간으로 받아오는 구현을 다룹니다.
2. 사용 하드웨어 / 라이브러리
- OBD2 동글: Veepeak OBD2 Bluetooth (ELM327 칩셋)
- ESP32 BT:
BluetoothSerial(Classic BT SPP 클라이언트) - 멀티태스킹: FreeRTOS (
xTaskCreatePinnedToCore)
ESP32의 Classic Bluetooth SPP를 사용합니다. BLE가 아닌 Classic BT인 이유는 ELM327 동글 대부분이 SPP 프로파일만 지원하기 때문입니다.
3. 전체 구조 — 왜 별도 태스크인가
ELM327에 PID를 보내고 응답을 기다리는 시간이 최대 2~3초입니다. 이 대기가 메인 루프에 있으면 그동안 화면이 완전히 멈춥니다. 해결책은 BT OBD 통신을 별도 FreeRTOS 태스크로 분리하는 것입니다.
inline void petBtObdInit() {
PetBtObd::_bt.begin("CarPet_BT", true); // BT 클라이언트 모드
xTaskCreatePinnedToCore(
PetBtObd::taskFn, // 태스크 함수
"btObd", // 태스크 이름
8192, // 스택 크기
nullptr, 2,
nullptr,
0 // 코어 0 고정 (디스플레이/WiFi는 코어 1)
);
}
BT OBD 태스크를 코어 0에 고정하고, 디스플레이·WiFi 루프는 코어 1에서 돌아 서로 방해하지 않습니다. 공유 변수는 volatile로 선언해 컴파일러 레지스터 캐싱으로 인한 데이터 불일치를 막았습니다.
static volatile bool g_connected = false;
static volatile int16_t g_speed = 0;
static volatile int16_t g_rpm = 800;
static volatile int16_t g_coolant = 20;
// ...
4.구현 상세
1. ELM327 초기화 시퀀스
BT 연결 직후 AT 명령어 5개를 순서대로 보내 동글을 초기화합니다.
static const char* const AT_INIT[] = {
"ATZ\r", // 리셋
"ATE0\r", // 에코 끄기
"ATL0\r", // 줄바꿈 끄기
"ATS0\r", // 공백 끄기 (파싱 단순화)
"ATSP0\r" // 프로토콜 자동 감지
};
각 명령어 전송 후 > 프롬프트가 올 때까지 대기합니다. 3.5초 안에 응답이 없으면 초기화 실패로 판단하고 재연결을 시도합니다.
2. OBD2 PID 폴링
6종 PID를 순서대로 하나씩 보내고 응답을 받아 파싱하는 방식입니다. 한 번에 여러 PID를 보내면 응답이 섞이는 문제가 있어 순차 폴링을 택했습니다.
static const char* const PID_CMD[] = {
"010D\r", // 속도 (km/h)
"010C\r", // RPM
"0105\r", // 냉각수 온도
"012F\r", // 연료량 (%)
"0104\r", // 엔진 부하 (%)
"0142\r" // 제어 전압 (V)
};
응답 파싱은 OBD2 표준 공식을 그대로 적용합니다.
static void parseObd(uint8_t idx) {
char* p = strstr(g_rx, PID_TAG[idx]);
if (!p) return;
p += 4;
switch (idx) {
case 0: g_speed = (int16_t)hex2(p); break; // 속도: 1바이트
case 1: g_rpm = (int16_t)((hex2(p)*256 + hex2(p+2)) / 4); break; // RPM: 2바이트 /4
case 2: g_coolant = (int16_t)(hex2(p) - 40); break; // 냉각수: -40 오프셋
case 3: g_fuel = (int16_t)(hex2(p) * 100 / 255); break; // 연료: 0~100%
case 4: g_load = (int16_t)(hex2(p) * 100 / 255); break; // 부하: 0~100%
case 5: g_voltage = (int16_t)((hex2(p)*256 + hex2(p+2)) / 100); break; // 전압: V×10
}
}
3. 상태 머신으로 연결 관리
BT 태스크 내부는 4단계 상태 머신으로 구성되어 있습니다. 예기치 않은 연결 끊김이나 응답 타임아웃에도 자동으로 재연결을 시도합니다.
enum TState : uint8_t {
IDLE, // 재연결 대기
TCONNECT, // BT 연결 시도
INIT, // ELM327 초기화
POLL // PID 폴링 루프
};
- IDLE: 웹 "연결" 버튼 또는 10초 타이머 후 TCONNECT로 전환
- TCONNECT:
_bt.connect(BT_OBD_NAME)— 최대 10초 대기 - INIT: AT 명령어 5개 순서대로 전송, 실패 시 IDLE로 복귀
- POLL: PID를 80ms 간격으로 순환 폴링, 연결 끊기면 IDLE로 복귀
4. ODO 미터 — 다중 PID 폴백
누적 주행거리 PID는 차종마다 다릅니다. 4개 PID를 우선순위 순으로 시도해 응답이 오는 첫 번째를 사용합니다.
// 우선순위: 제조사 확장 PID → 표준 거리 PID 폴백
if (queryOnce("22F40D\r", 900) && parseMode22(...)) { /* 성공 */ }
else if (queryOnce("22A6\r", 900) && parseMode22(...)) { /* 성공 */ }
else if (queryOnce("0131\r", 900) && parseMode01(...)) { /* 성공 */ }
else if (queryOnce("0121\r", 900) && parseMode01(...)) { /* 성공 */ }
5. 웹 리모컨과 연동 — 연결 상태 실시간 표시
웹앱은 1.5초마다 /bt/status를 폴링해 연결 상태와 실차 데이터를 받아옵니다. 미연결 상태에서는 슬라이더 시뮬레이터 값을, 연결 시에는 실차 값을 카드에 표시합니다.
// JSON 응답 예시 (연결됨)
// {"c":true,"k":false,"ev":0,
// "spd":60,"rpm":2100,"cool":88,
// "fuel":72,"load":34,"volt":142,
// "trip":15,"avg":48,"sec":1120,"odo":45230}
5. 결과
- FreeRTOS 태스크 분리로 OBD2 폴링 중에도 디스플레이 애니메이션이 끊기지 않음
- 속도 반영 주기 약 80ms (PID 6개 × 80ms = 최대 480ms 사이클, 실사용 체감 부드러움)
- 웹 리모컨에서 연결/해제 버튼, 실시간 수치 카드, 트립/평균속도/ODO 모두 정상 동작
- 연결 끊김 시 10초 후 자동 재연결 확인
6. 느낀 점
FreeRTOS 멀티태스킹 없이 BT OBD를 메인 루프에 넣었다면 화면 업데이트와 HTTP 처리가 모두 블로킹됐을 것입니다. ESP32의 듀얼 코어를 역할에 따라 나누는 것만으로 구조가 훨씬 깔끔해졌습니다. 다음 편에서는 이 구현을 실제 차량에 연결한 테스트 결과를 공유합니다.
'개인 개발 > 자동차 속도 기반 IoT 장식 만들기' 카테고리의 다른 글
| 자동차 속도 기반 IoT 장식 만들기 07 - ESP32-S3 FreeRTOS로 차량용 IoT 펌웨어 재설계하기 (0) | 2026.05.22 |
|---|---|
| 자동차 속도 기반 IoT 장식 만들기 06 - 실차 테스트 결과와 방향 전환, OBD2에서 GPS+가속도 센서로 (0) | 2026.05.01 |
| 자동차 속도 기반 IoT 장식 만들기 04 - 실제 프로토타입 만들기(납땜과 3d printer) (0) | 2026.05.01 |
| 자동차 속도 기반 IoT 장식 만들기 03 - 펌웨어 개발 및 메모리 최적화 (0) | 2026.04.25 |
| 자동차 속도 기반 IoT 장식 만들기 02 - 하드웨어 스펙 설계 (0) | 2026.04.25 |