실시간 씽어아이오(Thinger.io) 지도 연동 안정적인 자동차 GPS 트래커 제작
부품 및 소모품
| 부품 | 수량 | 비고 |
| 아두이노 레오나르도 | 1 | 전용 하드웨어 시리얼 확보용 |
| SIM900 개발 보드 | 1 | GSM/GPRS 통신용 |
| u-blox NEO-6M GPS 모듈 | 1 | |
| GPS 안테나 | 1 | 저가형은 수신 불량 가능, 품질 좋은 것 권장 |
| 220Ω 저항 | 2 | 상태 LED용 |
| LED | 2 | 전원 및 GPS 락 표시 |
| 납땜 보드 7x5cm | 1 | 3D 프린트 인클로저 맞춤 크기 |
| 12V→5V 스텝다운 모듈 | 1 | 차량 배터리/시거잭 전원용 |
| 아다프루트 SD 카드 리더 | 1 | 5V 호환 (SPI 헤더 연결 용이) |
| 마이크로 SD 카드 | 1 | |
필수 도구 및 장비
앱 및 온라인 서비스
- Thinger.io 플랫폼 (무료 IoT 서비스, 대시보드 제공)
프로젝트 소개
간단한 "GPS 트래커 아이디어"로 시작해 "다목적 GPS 트래커"로 발전했습니다. 첫 프로젝트라 학습 곡선이 가파르니 피드백과 개선 제안 환영합니다!
차량에 장착하며 주요 기능:
- GPS 좌표 추적 → 2분마다 씽어아이오 IoT 클라우드 대시보드에 마지막 위치 전송 (지도 표시). HTTP POST 요청 사용.
- SMS 명령 응답: 현재/마지막 위치 구글 맵 링크 회신.
- SMS 알림: 설정 거리(예: 4000km)마다 오일 캐치 탱크 비우기 알림 전송. 소프트웨어 "주행거리계" 기능.
아두이노 메모리 제약으로 오버헤드 최소화 및 효율적 코드 작성, 경량 라이브러리 사용.
사용 라이브러리
- NeoGPS: GPS 추적/디코딩. TinyGPS++ 대비 RAM 절약. 라이브러리 매니저에서 설치.
GPSPort.h 수정 필요 (Serial1 사용 정의).
- Time: UTC 시간대 변환. PaulStoffregen/Time.
- PetitFS: SD 카드 읽기/쓰기. 초경량 FAT 라이브러리. greiman/PetitFS. 제약 많으나 코드 크기 절감에 필수.
씽어아이오(Thinger.io) 라이브러리를 쓰지 않는 이유
공식 라이브러리는 편하지만 플래시 메모리 80% 차지해 나머지 코드 공간 부족. HTTP POST 요청으로 직접 통신 구현.
SMS 명령어
- POS: 현재 좌표 + 구글 맵 링크 회신 (GPS 미고정 시 마지막 위치).
- GETKM: 현재 누적 거리(km) 회신.
- RESETKM: 거리 카운터 리셋.
NeoGPS 설정
GPSPort.h 파일을 아래 내용으로 전체 교체:
#ifndef GPSport_h
#define GPSport_h
#define gpsPort Serial1
#define GPS_PORT_NAME "Serial1"
#define DEBUG_PORT Serial
#endif
다른 보드 사용 시 해당 시리얼 포트(Serial2, Serial3 등)로 변경.
정확도 참고: GPS는 정지 시에도 좌표 드리프트 발생. 10m 미만 이동은 정지로 간주, 15초 샘플링. 직선 거리 계산이므로 실제 주행거리와 차이 남.
PetitFS 설정
pffArduino.h: SD_CS_PIN을 10으로 변경 (SPI SS 핀).
pffconf.h: _USE_DIR, _USE_LSEEK, _FS_FAT12, _FS_FAT16을 0으로 비활성화 (코드 크기 절감).
초기 컴파일 에러 시 pff.cpp에 FRESULT 반환 타입 추가.
SD 카드 준비
FAT32로 포맷 (공식 SD Card Formatter 권장). 라이브러리가 파일 생성 불가하므로 dist.txt, settings.txt 미리 생성해 카드에 복사. 제공된 참조 파일 사용 권장 (포맷/내용 매우 엄격). dist.txt는 초기 거리(예: 0), settings.txt는 알림 트리거 거리(미터 단위) 저장.
SIM900 보드 준비
- 외부 전원 필수 (아두이노 전원 부족). 5V-9V DC, 2A 이상, 5.5mm 배럴 잭.
- 파워 셀렉터를 외부 전원으로 설정.
- 시리얼 셀렉터를 소프트웨어 시리얼로 설정 (점퍼 정렬).
- 소프트웨어 트리거: R13 점퍼 납땜 후 아두이노 D7→SIM900 #9 연결로 전원 제어 가능.
- SIM 카드 PIN 락 해제 필수.
- 펌웨어 버전 중요! HTTP POST Content-Type 헤더 설정을 위해 B10 이상 필요.
AT+CGMR로 확인. 구버전(B01 등)은 업데이트 필요 (ACOPTEX 가이드 참조).
코드 수정 사항
- APN 정보 변경 (
connectGPRS() 내 TeleXXX → 본인 APN).
- 씽어아이오 URL 및 인증 키를 본인 것으로 교체 (
postDataThinger() 함수).
- 시간대 설정 (
UTC_OFFSET define).
- 알림 트리거 거리 및 메시지 설정 (
settings.txt 및 코드 내 triggerDistance).
- 기본 알림 전화번호 설정 (
DEFAULT_NUMBER).
- 디버깅 시
while (!Serial); 주석 해제, 외부 전원 사용 시 반드시 주석 처리.
Thinger.io 연동
- 계정 생성 → 버킷(데이터베이스) 생성.
- HTTP 디바이스 생성 (공식 문서 참조). 디바이스 이름은 짧게 (인증 키 길이 문제 방지).
- 디바이스 콜백 설정에서 'write bucket'을 생성한 버킷으로 지정.
- 디바이스 콜백 개요에서 메서드 URL과 인증 헤더(Bearer 제외) 확인.
- 코드 내
postDataThinger()의 URL과 인증 키를 본인 것으로 교체. https→http로 변경 필수 (SIM900 SSL 미지원).
- 성공 시 시리얼 모니터에
+HTTPACTION: 1,200,0 표시. 400 에러 시 URL/키/펌웨어 확인.
- 대시보드에서 맵 위젯으로 버킷 데이터 시각화.
- 더 많은 데이터 전송 시 JSON 포맷에 필드 추가 (
sprintf 문법 엄수).
인클로저
3D 프린팅 가능한 완전한 인클로저 STL 파일 제공. M3 나사 사용. 7x5cm 납땜 보드용 설계 (브레드보드 사용 시 글루건 고정). 상단 케이스 마운트 홀은 막혀 있어 출력 후 3mm 드릴로 뚫기. 0.2mm 레이어, 서포트 없이 출력 가능.
차량 배터리/전원 연결
시거잭 회로(시동 ON/OFF 연동)에 연결해 차량 시동 시만 동작하도록 구성. 상시 전원 원하면 배터리 직결 또는 배터리 팩 추가. 12V→5V 스텝다운 모듈로 아두이노와 SIM900 각각 USB 포트로 전원 공급. GSM 활동 시 약 110mA 소모 (통신 불량 지역 시 증가).
알려진 이슈
- SMS 수신과 HTTP POST 동시 발생 시 명령 누락 가능. 몇 초 후 재전송하면 해결.
- 네트워크 음영 지역에서 부팅 시 추후 네트워크 복구해도 재접속 안 됨 (시작 시 1회만 접속 시도). 추후 수정 예정.
코드
GPS_tracker_Leonardo_v2.ino (주요 부분 발췌)
#include
#include
#include
#include
#include "PF.h"
#include "PetitSerial.h"
#define UTC_OFFSET 1
#define TXPin 8
#define RXPin 9
#define PWRPin 7
#define DEFAULT_NUMBER "+4712345678"
FATFS fs;
String textMessage, number = DEFAULT_NUMBER;
char sms_msg[160];
float Lat = 0, Long = 0;
boolean valid_location = false;
uint8_t num_sat;
NeoGPS::Location_t prevFix;
NMEAGPS gps;
gps_fix fix;
const char *googlePrefix = "https://maps.google.com/maps?q=";
const char *filename = "DIST.TXT";
const char *settings = "SETTINGS.TXT";
NeoGPS::time_t timeFix;
char datePrint[13], timePrint[10];
float totalDistance = 0, triggerDistance = 4000000;
SoftwareSerial SIM900(TXPin, RXPin);
void setup() {
pinMode(3, OUTPUT); pinMode(4, OUTPUT);
digitalWrite(3, HIGH); // 전원 LED
Serial.begin(9600);
// while (!Serial); // 디버깅 시만 활성화
SIM900.begin(9600);
gpsPort.begin(9600);
initializeSD();
SIM900power();
SIM900.println(F("AT")); delay(500);
SIM900.println(F("AT+CMGF=1")); delay(500);
SIM900.println(F("AT+CNMI=1,2,0,0,0")); delay(500);
connectGPRS();
}
void loop() {
while (gps.available(gpsPort)) {
fix = gps.read();
num_sat = fix.satellites;
if (fix.valid.location) {
digitalWrite(4, HIGH);
Lat = fix.latitude(); Long = fix.longitude();
if (Lat != 0 && Long != 0 && !valid_location) {
valid_location = true; prevFix = fix.location;
}
}
if (fix.valid.date && fix.valid.time) {
timeFix = fix.dateTime; updateTime();
}
if (valid_location) {
// 15초마다 거리 계산
static const unsigned long REFRESH_INTERVAL_UPD = 15000;
static unsigned long lastRefreshTime = millis();
if (millis() - lastRefreshTime >= REFRESH_INTERVAL_UPD) {
lastRefreshTime += REFRESH_INTERVAL_UPD;
float distanceDelta = prevFix.DistanceKm(fix.location) * 1000;
if (distanceDelta > 10 && distanceDelta < 10000) totalDistance += distanceDelta;
prevFix = fix.location;
}
// 2분마다 SD 기록 & 씽어아이오 전송
static const unsigned long REFRESH_INTERVAL_WRITE_SD = 120000UL;
static unsigned long lastRefreshTimeWriteSD = millis();
if (millis() - lastRefreshTimeWriteSD >= REFRESH_INTERVAL_WRITE_SD) {
lastRefreshTimeWriteSD += REFRESH_INTERVAL_WRITE_SD;
// SD 쓰기 (PetitFS)
char buf[9]; dtostrf(totalDistance, 8, 0, buf);
if (PF.open(filename)) Serial.println(F("error open file"));
while (1) {
UINT nr;
if (PF.writeFile(buf, sizeof(buf), &nr)) Serial.println(F("error write file"));
if (nr == sizeof(buf)) { PF.writeFile(0, 0, &nr); break; }
}
// 씽어아이오 HTTP POST
char httpContent[60], tempstrLong[10], tempstrLat[10];
dtostrf(Lat, 2, 6, tempstrLat); dtostrf(Long, 2, 6, tempstrLong);
sprintf(httpContent, "{\"longitude\": %s , \"latitude\": %s , \"date\": \"%s %s\"}", tempstrLong, tempstrLat, datePrint, timePrint);
char httpdataLen[20]; sprintf(httpdataLen, "AT+HTTPDATA=%i,10000", strlen(httpContent));
postDataThinger(httpdataLen, httpContent);
}
}
}
// 주행거리 알림
if (totalDistance > triggerDistance) {
char distanceTotalMsg[10];
itoa((totalDistance / 1000), distanceTotalMsg, 10);
sprintf(sms_msg, "Empty catchtank! Current distance: %skm", distanceTotalMsg);
textMessage = ""; number = DEFAULT_NUMBER; totalDistance = 0;
sendSMS(sms_msg);
}
updateSerial();
}
// SMS 수신/파싱, 발신, GPRS 연결, HTTP POST, SD 초기화, 시간 보정 등 함수들 구현됨
커스텀 파트 및 인클로저
- 상단 케이스 (높은 버전/컴팩트 버전) - 마운트 홀 막혀 있음, 출력 후 드릴로 뚫기
- 하단 케이스 (SIM900, 아두이노 보드 장착, 커넥터 컷아웃)
- SIM900 고정용 락 (마운트 홀 없는 보드용 2종)
dist.txt, settings.txt 참조 파일 (SD 카드에 복사용)
회로도
전체 회로도 이미지 제공 (아두이노 레오나르도, SIM900, GPS, SD 카드, LED, 스텝다운, 전원 연결)