Arduino Bike tracker
In 2016 I started riding a recumbent bike. Apart from making you look really cool*, these are unfortunately very unique and slightly expensive. I wanted to use it as my daily commuting bike and was slightly worried about theft. In reality I was more excited to have an excuse to try to make a more complicated hardware device!
At the time I couldn’t find any cheap solutions that would allow you to use GPS, SMS and GPRS at the same time. The cheap ones would allow you to send SMSs but I wanted to be able to upload data as well as SMSs so that I could integrate it with a tracking map.
After a bit of research I decided to build one with an Arduino and a few components cobbled together.
I used this alarm for about a year before my work finally added secure lockable bike storage and I moved to a house with a lockable garage. After that I became slightly less concerned with theft and stopped using the tracker.
Components
- LIS3DH - Triple-axis accelerometer with low power usage and ability to drive an interrupt wake-up circuit. This was key to having a low-ish power usage.
- SIM800L - GSM/2G GPRS module.
- NEO-6M - GPS module with cold/hot start functionality. The cold start would lock in between 30-120s depending on view of the sky.
- Arduino Pro Mini - Microcontroller acting as the brains.
- 1300mah Lipo Battery - A beefy rechargable.
- Adafruit Micro-lipo charger - Charging circuit for battery
Design
As 99.9% of the time the bike would not be stolen, I needed to reduce power as much as possible.
Each major component can be switched off fully with a transistor. When the device turns on it initially turns on the GSM device to allow it to receive commands. If it receives nothing in a minute it will go into alarm mode.
In alarm mode the arduino is in a low power state and the accelerometer is powered on. If it detects any movement it wakes up the arduino and it starts the alarm monitoring routine. It doesn’t initially power-up the GPS or GSM. It waits and starts to see how much more movement happens. If it’s in a bike rack and just gets knocked a few times when someone else is locking up it won’t trigger the alarm.
Once it gets a lot of jostling in a short period of time it’ll decide someone is messing with it and fire up both devices and start getting a GPS lock. It’ll initially start sending GPRS positions which are the tower based location estimates. Once it gets a GPS lock it’ll start sending out position reports. At this point it’ll also receive commands from selected phone numbers. You can disable the alarm, put it back to sleep, etc.
When it’s in normal monitoring mode, it’ll wake up the phone every few minutes to see if any SMS commands have come in. The alarm can be disabled so that you can ride it without panicking.
One feature I never finished was adding an external charging port. The device needed charging about once every 5 days if the alarm was never triggered. This got fairly annoying as I would have to remove a part from the bike to extract it. As it had the usb micro socket in the box I wanted to expose it with a waterproof connection but never finished it.
Alarm Routine
This a stripped down version of the alarm logic.
void loop() {
// SMS COMMAND ROUTINE - Check if a new SMS command has come in.
if(millis() - previousMillis > SMS_COMMAND_CHECK_DELAY) {
uint8_t smsCommand = gsm.checkForSMSCommand();
corePrint("Cm:");
corePrintln((int)smsCommand);
Serial.println(smsCommand);
if(smsCommand == REPLY_COMMAND_BATTERY) {
alarm.sendBatteryStatus(&gsm, &gps);
corePrintln("CmB");
} else if(smsCommand == REPLY_COMMAND_OFF) {
alarm.armed = false;
corePrintln("CmOf");
} else if(smsCommand == REPLY_COMMAND_ON) {
alarm.armed = true;
corePrintln("CmOn");
} else if(smsCommand == REPLY_COMMAND_RESET) {
corePrintln("CmR");
alarm.reset(&gsm, &gps);
alarm.triggered = false;
} else if(smsCommand == REPLY_COMMAND_STATUS) {
corePrintln("CmS");
alarm.sendStatus(&gsm, &gps);
} else if(smsCommand == REPLY_COMMAND_TRIGGER) {
corePrintln("CmT");
alarm.forceTrigger(&gsm, &gps);
}
previousMillis = millis();
}
// ALARM ROUTINE - the alarm has multiple states as the device stays in low power mode until the motion sensor is triggered.
// these are a series of counters to determine which systems to bring online depending on action. If the bike is nudged,
// it won't panic and turn the GPS on right away. It needs a bit more jostling.
if(alarm.armed) {
bool accelerometerTriggered = accelerometer.isTriggered();
if(!alarm.triggered && accelerometerTriggered) {
corePrintln("BA:!aT & aTI");
blink(3, 100);
alarm.reset(&gsm, &gps);
alarm.triggered = true;
} else if(alarm.triggered && accelerometerTriggered) {
corePrintln("aT && aTI");
blink(3, 100);
alarm.tick();
alarm.accelerometerTick();
} else if(alarm.triggered) {
corePrintln("aT");
blink(5, 75);
alarm.tick();
}
}
if(alarm.triggered) {
// If the alarm counter is past the pre-checks we now start sending position reports until we
// receive a cancel or die!
if(alarm.counter >= 20) { // #LIMITS
gps.status(); // Get the current GPS status
// Every minute send a position update
if( (alarm.counter % 60) == 0) { // #LIMITS
if(alarm.consecutiveEmptyAccelerometerPeriods >= 500) { // #LIMITS
corePrintln("CA. Res A");
}
if(alarm.accelerometerCounter == alarm.previousAccelerometerCounter) {
alarm.consecutiveEmptyAccelerometerPeriods++;
} else {
alarm.previousAccelerometerCounter = alarm.accelerometerCounter;
alarm.consecutiveEmptyAccelerometerPeriods = 0;
}
//fonaSendSMS
corePrintln("BA:SMS");
if(alarm.alertsSent <= 50) { // TODO: Remove alerts limit // #LIMITS
alarm.sendPositionAlert(&gsm, &gps);
}
}
} else if(alarm.counter == 10 &&
( (alarm.accelerometerCounter - alarm.previousAccelerometerCounter) == 0 ) )
{ // #LIMITS
corePrintln("None last. Stop.");
alarm.reset(&gsm, &gps);
} else if(alarm.counter == 10 &&
( (alarm.accelerometerCounter - alarm.previousAccelerometerCounter) > 0 ) )
{
corePrintln("SMS:1st");
alarm.sendPositionAlert(&gsm, &gps);
alarm.previousAccelerometerCounter = alarm.accelerometerCounter;
} else if(alarm.counter == 5 && alarm.accelerometerCounter == 0) {
corePrintln("Fal A");
alarm.reset(&gsm, &gps);
} else if(alarm.counter == 5 && alarm.accelerometerCounter > 0) {
corePrintln("Pre-SMS wait");
blink(7, 75);
if(!alarm.gpsActive) {
corePrintln("GPSON");
gps.enable();
gsm->enableGPRS();
}
alarm.previousAccelerometerCounter = alarm.accelerometerCounter;
alarm.gpsActive = true;
} else {
alarmPreviousAccelInterruptCounter = alarmAccelInterruptCounter;
}
}
delay(500);
}
Appendix A
- Definitely not cool. Like the opposite.