The Bike Box project was a collaborative effort with members sharing many responsibilities. However, as the only computer engineer on a team of electrical engineers, I held the sole responsibility of microcontroller hardware selection, programming, and logic design.
Microcontroller Selection
An Atmega1284P microcontroller controls module logic and Bluetooth communication, seated into a 40-pin DIP socket on the main board. The main features of this microcontroller are its small form factor as a dual inline package chip, the availability of commonly available programming tools, and its low cost (leaving room in the budget to purchase backup microcontrollers). The only downside to using the 1284P is the difficulty of programming and debugging in comparison to pre-assembled development boards with built-in ports for peripherals such as USB.
Programming
Programming the Atmega chip involved using an Arduino Uno as an in-circuit serial programmer, using the ISP sketch included with Arduino IDE, and connecting the appropriate four ISCP connections between the Uno and the microcontroller. Although Arduino IDE only supports Arduino boards out of the box, additional third party plugins or “cores” offer compatibility options for boards with other Atmel chips such as the 1284P. Arduino.cc recommends MightyCore for this purpose. The microcontroller is ready to program in Arduino IDE after installing this core, carefully adjusting settings such as clock speed and board variant, and burning the bootloader. The code is written entirely in Arduino language with the assistance of three third party libraries for timing, debouncing digital signals, writing data to EEPROM.
Pin Assignments
| Name | Pin | Type | Description |
| Alarm Armed | Pin 2 | Output | Produces a logic high if the user has armed the alarm and a logic low otherwise |
| Headlight Connected | Pin 4 | Input | Reads a logic high if the headlight is connected and a low otherwise |
| Blinkers Connected | Pin 6 | Input | Reads a logic high if the blinker panel is connected and a low otherwise |
| Headlight Switch | Pin 8 | Input | Reads a logic high if the headlight switch is toggled on and a low otherwise |
| Power | Pin 10 | VCC | 5V power to the microcontroller |
| Ground | Pin 11 | Gnd | Ground, connects to main ground |
| TX | Pin 14 | Data Output | Transmits outgoing data to the bluetooth module |
| RX | Pin 15 | Data Input | Recieves incoming data from the bluetooth module |
| Left Blinker Panel | Pin 18 | Data Input | Produces a logic high to illuminate the left segment of the blinker panel |
| Center Blinker Panel | Pin 19 | Data Input | Produces a logic high to illuminate the center segment of the blinker panel |
| Right Blinker Panel | Pin 20 | Data Input | Produces a logic high to illuminate the right segment of the blinker panel |
| Headlight | Pin 21 | PWM Output | Produces a pulse-width modulated output depending on the desired brightness of the headlight |
| Blinker Switch Left | Pin 22 | Digital Input | Receives a logic high when the blinker switch is in the left position and a logic low otherwise |
| Blinker Switch Right | Pin 23 | Digital Input | Receives a logic high when the blinker switch is in the right position and a logic low otherwise |
| Ground | Pin 31 | Gnd | Ground, connects to main ground |
| AREF | Pin 32 | Analog | Sets the analog reference voltage of the microcontroller to 5V |
| Sensor | Pin 35 | Digital Input | Receives a modulated digital input from the microwave sensor that changes depending on the speed of approaching objects |
| Alarm | Pin 37 | Digital Output | Oscillates power to the buzzer if the alarm has been triggered |
| Accelerometer Z | Pin 38 | Analog Input | Receives an analog voltage proportional to the acceleration measured in the z direction |
| Accelerometer Y | Pin 39 | Analog Input | Receives an analog voltage proportional to the acceleration measured in the y direction |
| Accelerometer X | Pin 40 | Analog Input | Receives an analog voltage proportional to the acceleration measured in the x direction |

Bluetooth Communication and Codes
The Atmega1284P communicates with a Kedsum HC-06 Bluetooth module, sending encoded strings through serial TX/RX pins. It broadcasts the name Bike Box and has a default security pin of 1234. The microcontroller and phone app use a custom set of codes to communicate through the module. The latest version is a simple key and value pair in the format {key}={value}/ where keys are parsed as strings and values are parsed as integers. Codes may be directional (microcontroller to app or app to microcontroller) or bi-directional. The / symbol indicates the end of a code. Examples:
- The string module_connected=0/ is a directional code that the app might receive from the microcontroller. It indicates that the user has connected the headlight module to the Bike Box.
- The string arm_alarm=1/ is a directional code that the microcontroller might receive from the app. It indicates that the user has armed the security module from the smartphone app and that calibration should begin.
- The string blinker_state=2/ is a bi-directional code. If the microcontroller sends it to the app, it indicates that the user has toggled the physical blinker switch to the left position and that the left blinker is on. If the app sends it to the microcontroller, it indicates that the user has toggled the virtual left blinker button on the app, and is a request for the microcontroller to turn the left blinker on (only an option if the physical switch is in the neutral position).
A hello code with no value indicates that the Android app has connected with the bluetooth module. The microcontroller sends a hello code in response, as well as useful initial data for the app, such as the modules connected and the current state of the headlights and blinkers. On each iteration of the main program, the microcontroller checks the serial port and reads available data in as a string. Then, it parses the data according to the key-value format, and delivers the value to an appropriate function based on the key.
void checkBluetooth() {
if (Serial.available()) {
String message = Serial.readString();
if (message.endsWith(CODE_DELIMITER))
message.replace(CODE_DELIMITER, "");
int value, index = message.indexOf(PARAM_DELIMITER);
String key;
if (index == -1)
key = message;
else {
key = message.substring(0, index);
value = message.substring(index + 1).toInt();
}
handleCode(key, value);
sendCode("code_processed");
}
}
void handleCode(String key, int value) {
if (key == "hello")
hello();
else if (key == "alarm_duration")
alarmDuration = value;
else if (key == "headlight_brightness")
setHeadlight(value);
else if (key == "blinker_state")
setBlinkerState(value);
else if (key == "blinker_duration")
blinkerDurationCode(value);
else if (key == "calibration_duration")
calibrationDurationCode(value);
else if (key == "arm_alarm")
armAlarmCode(value);
else if (key == "verify_pin")
verifyPin(value);
else if (key == "movement_threshold")
movementThresholdCode(value);
else if (key == "set_pin")
setPin(value);
else if (key == "reset_pin")
resetPin();
}
void hello() {
sendCode("hello");
sendCode("module_connected", 0);
sendCode("module_connected", 1);
sendCode("module_connected", 2);
if (headlightConnected.read() == HIGH)
sendCode("module_connected", 0);
if (rearPanelConnected.read() == HIGH) {
sendCode("module_connected", 1);
sendCode("module_connected", 2);
}
if (headlight.read() == HIGH)
sendCode("headlight_state", 1);
else
sendCode("headlight_state", 0);
sendCode("alarm_state", alarmState);
}
void sendCode(String code) {
Serial.print(code + "/");
}
void sendCode(String code, int parameter) {
Serial.print(code + "=" + String(parameter) + "/");
}
void sendCode(String code, String parameter) {
Serial.print(code + "=" + parameter + "/");
}
Headlight and Blinkers
The microcontroller sets headlight brightness using a pwm signal, requiring only one digital pin. The user flips a 2-state switch to turn the light on and off (full brightness and zero brightness). If the user desires to adjust brightness, they must do so using the Android app, which sends a headlight_brightness code with a value corresponding to the desired pwm duty cycle.
Three separate segments of LEDs on the rear panel can create a left arrow, right arrow, or solid horizontal line. The user flips a 3-state toggle switch to change the direction of the rear blinkers, changing the two appropriate left/right logic inputs. When the microcontroller needs to change the state of the blinker panel, it first resets all panels and turns all timers off, and then runs a function to begin oscillating voltages on the correct pins to illuminate the correct LED segments. It also sends a blinker_state code to the app to let it know that the physical blinker switch has been toggled. When the microcontroller receives a blinker_duration code to change the blinker speed, it resets the blinker timers to operate under the code's value in milliseconds. The microcontroller uses the following integer code values to communicate state information with the Android app.
- 0 = Turn blinker off
- 1 = Turn right blinker on
- 2 = Turn left blinker on
- 3 = Turn hazards on
void checkHeadlightSwitch() {
if (headlight.fell()) {
setHeadlight(0);
sendCode("headlight_state", 0);
}
else if (headlight.rose()) {
setHeadlight(MAX_PWM);
sendCode("headlight_state", 1);
}
}
void setHeadlight(int pwm) {
analogWrite(HEADLIGHT_OUT, pwm);
if (pwm == 0)
digitalWrite(CENTER_PANEL_OUT, LOW);
else
digitalWrite(CENTER_PANEL_OUT, HIGH);
}
void checkBlinkerToggle() {
if (rightBlinker.rose()) {
turnRightBlinkerOn();
sendCode("blinker_state", 1);
}
else if (leftBlinker.rose()) {
turnLeftBlinkerOn();
sendCode("blinker_state", 2);
}
else if (rightBlinker.fell() || leftBlinker.fell()) {
turnBlinkersOff();
sendCode("blinker_state", 0);
}
}
void turnBlinkersOff() {
blinkerState = 0;
resetAllPanels();
if (digitalRead(HEADLIGHT_IN) == HIGH) digitalWrite(CENTER_PANEL_OUT, HIGH);
}
void turnRightBlinkerOn() {
blinkerState = 1;
resetAllPanels();
rightTimer = timer.oscillate(RIGHT_PANEL_OUT, blinkDuration, LOW);
centerTimer = timer.oscillate(CENTER_PANEL_OUT, blinkDuration, LOW);
}
void turnLeftBlinkerOn() {
blinkerState = 2;
resetAllPanels();
leftTimer = timer.oscillate(LEFT_PANEL_OUT, blinkDuration, LOW);
centerTimer = timer.oscillate(CENTER_PANEL_OUT, blinkDuration, LOW);
}
void turnHazardsOn() {
blinkerState = 3;
resetAllPanels();
rightTimer = timer.oscillate(RIGHT_PANEL_OUT, blinkDuration, LOW);
leftTimer = timer.oscillate(LEFT_PANEL_OUT, blinkDuration, LOW);
centerTimer = timer.oscillate(CENTER_PANEL_OUT, blinkDuration, LOW);
}
void resetAllPanels() {
timer.stop(rightTimer);
timer.stop(leftTimer);
timer.stop(centerTimer);
digitalWrite(RIGHT_PANEL_OUT, LOW);
digitalWrite(LEFT_PANEL_OUT, LOW);
digitalWrite(CENTER_PANEL_OUT, LOW);
}
void blinkerDurationCode(int parameter) {
blinkDuration = parameter;
refreshBlinkers();
}
void refreshBlinkers() {
if (blinkerState > 0) {
timer.stop(centerTimer);
centerTimer = timer.oscillate(CENTER_PANEL_OUT, blinkDuration, LOW);
if (blinkerState == 1 || blinkerState == 3) {
timer.stop(rightTimer);
rightTimer = timer.oscillate(RIGHT_PANEL_OUT, blinkDuration, LOW);
}
if (blinkerState == 2 || blinkerState == 3) {
timer.stop(leftTimer);
leftTimer = timer.oscillate(LEFT_PANEL_OUT, blinkDuration, LOW);
}
}
}
Accelerometer and Security Alarm
To deter theft of both the bike and Bike Box, an optional security module plugs into a socket inside the main hub, and can be configured using the Android app. The Bike Box senses motion using an ADXL335 accelerometer with analog readings for acceleration in the X, Y, and Z directions. Arduino language’s analogRead() function maps voltages from 0-5 volts to a 10-bit integer between 0 and 1024. Ideally, small changes in motion, for example those that could be caused by the wind or a pedestrian bumping into the bike, should be ignored. To accomplish this, the microcontroller calculates the difference in acceleration between the current reading and previous, adding each into a rolling average using an array of a pre-specified number size. This process is repeated for each axis. The average change in acceleration for each axis is compared to the maximum calculated during calibration and added to a threshold to determine if the alarm should sound. This threshold can be increased or decreased through the Android app to change the movement sensitivity. The microcontroller communicates state information about the alarm to the Android app through the following integer code values
- 0 = The alarm is off/disarmed
- 1 = The accelerometer is being calibrated
- 2 = The alarm is armed
- 3 = The alarm has been tripped
void checkAccelerometer(void *context) {
int xAccel = analogRead(X_ACCEL_IN);
int yAccel = analogRead(Y_ACCEL_IN);
int zAccel = analogRead(Z_ACCEL_IN);
if (accelerometerXYZCodeEnabled) {
sendCode("accelerometer_x", xAccel);
sendCode("accelerometer_y", yAccel);
}
int xAccelChange = abs(xAccel - xAccelPrev);
int yAccelChange = abs(yAccel - yAccelPrev);
int zAccelChange = abs(zAccel - zAccelPrev);
xAccelPrev = xAccel;
yAccelPrev = yAccel;
zAccelPrev = zAccel;
xTotal += xAccelChange - xAccelChanges[0];
yTotal += yAccelChange - yAccelChanges[0];
zTotal += zAccelChange - zAccelChanges[0];
for (int i = 1; i < MOVEMENT_WINDOW_SIZE; i++) {
xAccelChanges[i - 1] = xAccelChanges[i];
yAccelChanges[i - 1] = yAccelChanges[i];
zAccelChanges[i - 1] = zAccelChanges[i];
}
xAccelChanges[MOVEMENT_WINDOW_SIZE - 1] = xAccelChange;
yAccelChanges[MOVEMENT_WINDOW_SIZE - 1] = yAccelChange;
zAccelChanges[MOVEMENT_WINDOW_SIZE - 1] = zAccelChange;
int xAverage = xTotal / MOVEMENT_WINDOW_SIZE;
int yAverage = yTotal / MOVEMENT_WINDOW_SIZE;
int zAverage = zTotal / MOVEMENT_WINDOW_SIZE;
if (alarmState == 1) {
if (xAverage > xRestingMax)
xRestingMax = xAverage;
if (yAverage > yRestingMax)
yRestingMax = yAverage;
if (zAverage > zRestingMax)
zRestingMax = zAverage;
}
else if (alarmState == 2) {
boolean xMovement = xAverage > xRestingMax + movementThreshold;
boolean yMovement = yAverage > yRestingMax + movementThreshold;
boolean zMovement = zAverage > zRestingMax + movementThreshold;
if (xMovement || yMovement || zMovement)
soundAlarm();
}
}
void setAlarmState(int state) {
alarmState = state;
sendCode("alarm_state",state);
}
void soundAlarm() {
setAlarmState(3);
alarmTimer1 = timer.oscillate(ALARM_OUT, alarmPulseDuration, LOW);
alarmTimer2 = timer.after(alarmDuration, alarmFinished, (void*)0);
}
void alarmFinished(void *context) {
silenceAlarm();
armAlarm();
}
void silenceAlarm() {
timer.stop(alarmTimer1);
digitalWrite(ALARM_OUT, LOW);
}
void armAlarm() {
resetAccelerometerVariables();
calibrationOn();
movementTimer = timer.every(ACCEL_SAMPLE_RATE, checkAccelerometer, (void*)0);
digitalWrite(ALARM_ARMED_OUT, HIGH);
}
void disarmAlarm() {
silenceAlarm();
setAlarmState(0);
digitalWrite(ALARM_ARMED_OUT, LOW);
sendCode("alarm_state", 0);
}
Security Pin and EEPROM Storage
Because the HC-06 Bluetooth module is restricted to a predetermined pairing pin, anyone within 10 meters of the Bike Box who has the app would be able to disarm its security module. To resolve this potential issue, the user must enter a pin before accessing the security section of the Bike Box app. This feature requires the microcontroller to store the pin and handle pin verification requests from the app. Luckily, the 1284P features 4 kilobytes of EEPROM, a nonvolatile long-term storage option that can be accessed using Arduino’s EEPROM library. Each memory location holds a byte and can be accessed using the library’s read() and write() functions. When the microcontroller receives a set_pin Bluetooth code, each character is iterated through and stored in a byte of EEPROM. It sends a set_pin_complete code back to the app as confirmation. To retrieve the pin when it receives a verify_pin code, the microcontroller once again iterates through the first four memory locations, concatenating the haracters together and then comparing the result to the value received from the code. The microcontroller sends a verify code with a value of 1 if they match, or 0 if they do not.
void setPin(int pinInt) {
String pin = String(pinInt);
for (int i = 0; i < DEFAULT_PIN.length(); i++)
EEPROM.write(i, pin.charAt(i));
sendCode("set_pin_complete");
}
String getPin() {
String pin = "";
for (int i = 0; i < 4; i++)
pin += String((char)EEPROM.read(i));
return pin;
}
void resetPin() {
for (int i = 0; i < 4; i++)
EEPROM.write(i, DEFAULT_PIN.charAt(i));
sendCode("reset_pin_complete");
}