BongoBot
Project Description
The goal of this project is to create a Bluetooth controlled bongo player. The device will also be able to play beats autonomously based off a text file read through an SD card reader. The text file should be formatted with a series of numbers: 1, 2, and 3. A number one represents hitting the large drum, a number two represents hitting the smaller drum, and a number three will make the bot hit both drums. The Adafruit Bluetooth App has been changed to connect to the bot to create a user-controlled experienced. LEDs also line the bottom of each drum and will light up blue when one drum is hit and red when both drums are hit.
Team Members
- Helen Zhang
 - Lauren Kearley
 - Randy Michnovicz
 - Theodore Johnson
 
Parts List
- LPC1768 mbed
 - (x2) RBG LED Tape Lights
 - (x2) 400 Pin Breadboards
 - (x3) N-channel power MOSFET
 - (x2) Car Door Lock Actuator Motor
 - H Bridge
 - Adafruit Bluefruit LE UART
 - 2' x 2' Plywood Sheet
 - (x4) 6-32 x 2" Nuts and Bolts
 - 3/4" x 2' 22 Gauge Steel Hanger Strap
 - (x2) 1-1/2" Binding Posts
 - 9V 2A Battery Power Supply
 - Bongos
 - Drum Sticks
 - (x2) Counterweights (we used a AA and D battery)
 - Tiny Hands
 
Hardware Setup
Wiring
| Adafruit Bluefruit LE UART | mbed | 
|---|---|
| Gnd | Gnd | 
| Vin | Vu | 
| CTS | Gnd | 
| TXO | p28 | 
| RXI | p27 | 
| SDCard Reader | mbed | 
|---|---|
| Gnd | Gnd | 
| Vcc | Vout | 
| CS | p14 | 
| SCK | p13 | 
| DI | p11 | 
| DO | p12 | 
| H Bridge | mbed | Car Lock Actuator Motor | 
|---|---|---|
| PWMA/PWMB/STBY | Vout (3.3 V) | |
| Gnd | Gnd | |
| Vcc | Vout | |
| Vmot | 9v | |
| AIN1 | p5 | |
| AIN2 | p6 | |
| BIN1 | p29 | |
| BIN2 | p30 | |
| AO1 | Pole A2 | |
| AO2 | Pole A1 | |
| BO1 | Pole B1 | |
| BO2 | Pole B2 | 
| N-channel power MOSFET | mbed | RGB LED | |
|---|---|---|---|
| MOSFET 1 | pin 1 | p25 | |
| pin 2 | R | ||
| pin 3 | gnd | ||
| MOSFET 2 | pin 1 | p24 | |
| pin 2 | G | ||
| pin 3 | gnd | ||
| MOSFET 3 | pin 1 | p23 | |
| pin 2 | B | ||
| pin 3 | gnd | 

Creating Motor Casings
The motor mounts were laser cut from the plywood according to the design below. Then assembled by attacking the motors between the two brackets using the 6-32 x 2" nuts and bolts.

Next holes were drilled 8 3/8" from the tip of the drum stick for the smaller drum and 8 3/4" from the tip for the larger drum using a 5/32" drill bit. These are then attached to the motor casings using the 1-1/2" binding posts.
Since the wires on the motors are too short to be attached to the breadboard we extended the wires by soldering them to generic 22 AWG threaded core wires using heat shrink to encase the wire connections. To ensure the threaded core wire can be used with the breadboard we soldered the ends of the wires to jumper wires.
LEDs
We used two RGB LED strips controlled by PWM pins which light up blue when each drum is hit individually and red when both are hit. To control voltage and current flow to each RGB pin we used an N-Channel MOSFET to control each of the RGB pins. To power the strip we used the 5V Vu pin on the Mbed. Since the Vu pin of the Mbed does not function when the device is not powered by the USB port we kept the device connected when running.
Assembling Bot
These motor casing are attached the the bongo drums using the steel hanger strap, 6-32 x 1" nuts and bolts, and some washers for added stability. Next we attached the drum sticks to the casings using the 1-1/2" Binding Posts aligned such that when the motors fire they push the drum stick down.



After attaching the drumsticks, we put counterweights on the back of them so that after they hit the drum they will go back to their up position. Since the drum sticks have holes at different positions due to the different size of the drum sticks they did not need the same counterweight to achieve this function. We put a D battery on the larger drum's stick and a AA on the smaller one's stick as counterweights.

Since we used two smaller breadboards because they were the correct size to mount easily to the bongos, we had to jump the connections across the breadboards.
Software
Mbed Code
Import programECE4180_BangoBot
Bangobot code
Bongo Bot Main.cpp
#include "mbed.h"
#include "Motor.h"
#include "SDFileSystem.h" 
#include "rtos.h"
 
#define SONG_SIZE 60
 
//Class to control an RGB LED using three PWM pins
class RGBLed
{
public:
    RGBLed(PinName redpin, PinName greenpin, PinName bluepin);
    void write(float red,float green, float blue);
private:
    PwmOut _redpin;
    PwmOut _greenpin;
    PwmOut _bluepin;
};
 
RGBLed::RGBLed (PinName redpin, PinName greenpin, PinName bluepin)
    : _redpin(redpin), _greenpin(greenpin), _bluepin(bluepin)
{
    //50Hz PWM clock default a bit too low, go to 2000Hz (less flicker)
    _redpin.period(0.0005);
}
 
void RGBLed::write(float red,float green, float blue)
{
    _redpin = red;
    _greenpin = green;
    _bluepin = blue;
}
 
//Setup RGB led using PWM pins and class
microphone mymicrophone(p16);
RGBLed myRGBled(p25,p24,p23); //RGB PWM pins
Motor small(p21, p30, p29); // pins for large drum // pwm, fwd, rev
Motor largex`(p22, p6, p5); // pins for small drum
Serial blue(p28,p27); // pins for bluetooth module serial
Serial pc(USBTX,USBRX);
SDFileSystem sd(p11, p12, p13, p14, "sd");
Timer timer; // Timer to  for when recorded song is playing
Thread led_thread;
char buffer[SONG_SIZE];
 
DigitalOut smallLED(LED1);
DigitalOut largeLED(LED2);
DigitalOut bothLED(LED3);
DigitalOut led4(LED4);
int toggle = 0;
 
FILE *nfp = fopen("/sd/sdtest1.txt", "r");
 
 
float strikelength = .15; // amount of time in seconds that the motor will run when striking
float bpm = 240.0; // bpm of recorded song, 240 bpm is nearing the fastest that we can go
float intensity = 0.0;
volatile int color = 0;
float recover = .15;
float initial = .01;
 
// Hit the small drum
void small_hit(void) {
    myRGBled.write(1,1,0);
    smallLED = 1;
    wait(.1);
    smallLED = 0;
    color = 2;
    small.speed(1.0);
    wait(strikelength);
    small.speed(0);
    wait(initial);
    small.speed(-1.0);
    wait(recover);
    small.speed(0);
    myRGBled.write(0,0,0);
}
 
// Hit large drum
void large_hit(void) {
    myRGBled.write(1,1,0);
    largeLED = 1;
    wait(.1);
    largeLED = 0;
    color = 1;
    large.speed(1.0);
    wait(strikelength);
    large.speed(0);
    wait(initial);
    large.speed(-1.0);
    wait(recover);
    large.speed(0);
    myRGBled.write(0,0,0);
}
 
// Hit both drums
void both_hit(void) {
    myRGBled.write(0,0,1);
    bothLED = 1;
    wait(.1);
    bothLED = 0;
    color = 3;
    small.speed(1.0);
    large.speed(1.0);
    wait(strikelength);
    small.speed(0);
    large.speed(0);
    wait(initial);
    small.speed(-1.0);
    large.speed(-1.0);
    wait(recover);
    small.speed(0);
    large.speed(0);
    myRGBled.write(0,0,0);
}
 
int main() {
    if(nfp == NULL) {
        error("Could not open file for read\n");
    }
    led4 = 1;
    
    while(true) {
        led4 = toggle;
        toggle = (toggle == 0) ? 1 : 0;
        if (blue.getc() == '!') { // The message should just be '!' and then a number 0-3. Ex. '!0','!1',...
            //pc.printf("Bluetooth message recieved");
            // '!0' and any other number corresponds to no action
            if (blue.getc() == 'B') {
                char c = blue.getc();
                if (blue.getc() == '1') {
                    if (c == '1') large_hit();
                    else if (c == '2') small_hit();
                    else if (c == '3') both_hit();
                    else {
                        fgets(buffer,SONG_SIZE,nfp);
                        for (int i = 0; i < sizeof(buffer); i++) {
                            timer.reset();
                            if (buffer[i] == NULL) break;
                            switch(buffer[i]) {
                                case '1': large_hit();
                                case '2': small_hit();
                                case '3': both_hit();
                            }
                            // Play the notes or rests at 240 bpm
                            //while (timer < 60.0/bpm);
                        }
                    }
                }
                
            }
        }    
    }
    
    return 0;
}
Mobile App Code
We edited the Bluetooth UART iOS and Android applications to reflect the project goals. To do this we changed the button scheme to have a bongo which can be pressed to play the large or smaller drum, a button which plays both drums, and a button which plays the song from the SD card.
iOS
To make the iOS app reflect the new controller button layout we changed the ControllerPadViewController.swift file to the following code and edited the corresponding view in the Main.storyboard file as shown below.

Updates to ViewController
import UIKit
protocol ControllerPadViewControllerDelegate: class {
    func onSendControllerPadButtonStatus(tag: Int, isPressed: Bool)
}
class ControllerPadViewController: UIViewController {
    // UI
    @IBOutlet weak var uartTextView: UITextView!
    @IBOutlet weak var uartView: UIView!
    @IBOutlet weak var button1: UIButton!
    @IBOutlet weak var button2: UIButton!
    @IBOutlet weak var button3: UIButton!
    @IBOutlet weak var button4: UIButton!
    
    // Data
    weak var delegate: ControllerPadViewControllerDelegate?
    override func viewDidLoad() {
        super.viewDidLoad()
        // UI
        uartView.layer.cornerRadius = 4
        uartView.layer.masksToBounds = true
        // Setup buttons targets
        setupButton(button1)
        setupButton(button2)
        setupButton(button3)
        setupButton(button4)
        
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Fix: remove the UINavigationController pop gesture to avoid problems with the arrows left button
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.navigationController?.interactivePopGestureRecognizer?.delaysTouchesBegan = false
            self.navigationController?.interactivePopGestureRecognizer?.delaysTouchesEnded = false
            self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
        }
    }
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
    }
    // MARK: - UI
    fileprivate func setupButton(_ button: UIButton) {
        button.addTarget(self, action: #selector(onTouchDown(_:)), for: .touchDown)
        button.addTarget(self, action: #selector(onTouchUp(_:)), for: .touchUpInside)
        button.addTarget(self, action: #selector(onTouchUp(_:)), for: .touchDragExit)
        button.addTarget(self, action: #selector(onTouchUp(_:)), for: .touchCancel)
    }
    func setUartText(_ text: String) {
        // Remove the last character if is a newline character
        let lastCharacter = text.last
        let shouldRemoveTrailingNewline = lastCharacter == "\n" || lastCharacter == "\r" 
        let formattedText = shouldRemoveTrailingNewline ? String(text[..<text.index(before: text.endIndex)]) : text
        
        //
        uartTextView.text = formattedText
        // Scroll to bottom
        let bottom = max(0, uartTextView.contentSize.height - uartTextView.bounds.size.height)
        uartTextView.setContentOffset(CGPoint(x: 0, y: bottom), animated: true)
    }
    // MARK: - Actions
    @objc func onTouchDown(_ sender: UIButton) {
        sendTouchEvent(tag: sender.tag, isPressed: true)
    }
    @objc func onTouchUp(_ sender: UIButton) {
        sendTouchEvent(tag: sender.tag, isPressed: false)
    }
    private func sendTouchEvent(tag: Int, isPressed: Bool) {
        if let delegate = delegate {
            delegate.onSendControllerPadButtonStatus(tag: tag, isPressed: isPressed)
        }
    }
    @IBAction func onClickHelp(_ sender: UIBarButtonItem) {
        let localizationManager = LocalizationManager.shared
        let helpViewController = storyboard!.instantiateViewController(withIdentifier: "HelpViewController") as! HelpViewController
        helpViewController.setHelp(localizationManager.localizedString("controlpad_help_text"), title: localizationManager.localizedString("controlpad_help_title"))
        let helpNavigationController = UINavigationController(rootViewController: helpViewController)
        helpNavigationController.modalPresentationStyle = .popover
        helpNavigationController.popoverPresentationController?.barButtonItem = sender
        present(helpNavigationController, animated: true, completion: nil)
    }
}
Android
To make the Android app reflect the new controller button layout we changed the ControllerPadFragment.java file to the following code and edited the corresponding view in the fragment_controller_pad.xml file as shown below.

Updates to ControllerPadFragment
package com.adafruit.bluefruit.le.connect.app;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Button;
import com.adafruit.bluefruit.le.connect.BluefruitApplication;
import com.adafruit.bluefruit.le.connect.BuildConfig;
import com.adafruit.bluefruit.le.connect.R;
import com.squareup.leakcanary.RefWatcher;
public class ControllerPadFragment extends Fragment {
    // Log
    @SuppressWarnings("unused")
    private final static String TAG = ControllerPadFragment.class.getSimpleName();
    // Config
    private final static float kMinAspectRatio = 1.8f;
    // UI TextBuffer (refreshing the text buffer is managed with a timer because a lot of changes can arrive really fast and could stall the main thread)
    private Handler mUIRefreshTimerHandler = new Handler();
    private Runnable mUIRefreshTimerRunnable = new Runnable() {
        @Override
        public void run() {
            if (isUITimerRunning) {
                updateTextDataUI();
                // Log.d(TAG, "updateDataUI");
                mUIRefreshTimerHandler.postDelayed(this, 200);
            }
        }
    };
    private boolean isUITimerRunning = false;
    // UI
    private ViewGroup mContentView;
    private EditText mBufferTextView;
    private ViewGroup mRootLayout;
    private View mTopSpacerView;
    private View mBottomSpacerView;
    // Data
    private ControllerPadFragmentListener mListener;
    private volatile StringBuilder mDataBuffer = new StringBuilder();
    private volatile StringBuilder mTextSpanBuffer = new StringBuilder();
    private int maxPacketsToPaintAsText;
    View.OnTouchListener mPadButtonTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            final int tag = Integer.valueOf((String) view.getTag());
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                view.setPressed(true);
                mListener.onSendControllerPadButtonStatus(tag, true);
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_UP) {
                view.setPressed(false);
                mListener.onSendControllerPadButtonStatus(tag, false);
                view.performClick();
                return true;
            }
            return false;
        }
    };
    // region Lifecycle
    @SuppressWarnings("UnnecessaryLocalVariable")
    public static ControllerPadFragment newInstance() {
        ControllerPadFragment fragment = new ControllerPadFragment();
        return fragment;
    }
    public ControllerPadFragment() {
        // Required empty public constructor
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Retain this fragment across configuration changes
        setRetainInstance(true);
    }
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_controller_pad, container, false);
    }
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // Set title
        AppCompatActivity activity = (AppCompatActivity) getActivity();
        if (activity != null) {
            ActionBar actionBar = activity.getSupportActionBar();
            if (actionBar != null) {
                actionBar.setTitle(R.string.controlpad_title);
                actionBar.setDisplayHomeAsUpEnabled(true);
            }
        }
        // UI
        mRootLayout = view.findViewById(R.id.rootLayout);
        mTopSpacerView = view.findViewById(R.id.topSpacerView);
        mBottomSpacerView = view.findViewById(R.id.bottomSpacerView);
        mContentView = view.findViewById(R.id.contentView);
        mBufferTextView = view.findViewById(R.id.bufferTextView);
        if (mBufferTextView != null) {
            mBufferTextView.setKeyListener(null);     // make it not editable
        }
        Button button1ImageButton = view.findViewById(R.id.button1ImageButton);
        button1ImageButton.setOnTouchListener(mPadButtonTouchListener);
        Button button2ImageButton = view.findViewById(R.id.button2ImageButton);
        button2ImageButton.setOnTouchListener(mPadButtonTouchListener);
        Button button3ImageButton = view.findViewById(R.id.button3ImageButton);
        button3ImageButton.setOnTouchListener(mPadButtonTouchListener);
        Button button4ImageButton = view.findViewById(R.id.button4ImageButton);
        button4ImageButton.setOnTouchListener(mPadButtonTouchListener);
        // Read shared preferences
        maxPacketsToPaintAsText = UartBaseFragment.kDefaultMaxPacketsToPaintAsText; //PreferencesFragment.getUartTextMaxPackets(this);
    }
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof ControllerPadFragmentListener) {
            mListener = (ControllerPadFragmentListener) context;
        } else if (getTargetFragment() instanceof ControllerPadFragmentListener) {
            mListener = (ControllerPadFragmentListener) getTargetFragment();
        } else {
            throw new RuntimeException(context.toString() + " must implement ControllerPadFragmentListener");
        }
    }
    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }
    @Override
    public void onResume() {
        super.onResume();
        ViewTreeObserver observer = mRootLayout.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                adjustAspectRatio();
                mRootLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
        // Refresh timer
        isUITimerRunning = true;
        mUIRefreshTimerHandler.postDelayed(mUIRefreshTimerRunnable, 0);
    }
    @Override
    public void onPause() {
        isUITimerRunning = false;
        mUIRefreshTimerHandler.removeCallbacksAndMessages(mUIRefreshTimerRunnable);
        super.onPause();
    }
    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.menu_help, menu);
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        FragmentActivity activity = getActivity();
        switch (item.getItemId()) {
            case R.id.action_help:
                if (activity != null) {
                    FragmentManager fragmentManager = activity.getSupportFragmentManager();
                    if (fragmentManager != null) {
                        CommonHelpFragment helpFragment = CommonHelpFragment.newInstance(getString(R.string.controlpad_help_title), getString(R.string.controlpad_help_text));
                        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
                                .replace(R.id.contentLayout, helpFragment, "Help");
                        fragmentTransaction.addToBackStack(null);
                        fragmentTransaction.commit();
                    }
                }
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
    // endregion
    // region UI
    private void adjustAspectRatio() {
        ViewGroup rootLayout = mContentView;
        final int mainWidth = rootLayout.getWidth();
        if (mainWidth > 0) {
            final int mainHeight = rootLayout.getHeight() - mTopSpacerView.getLayoutParams().height - mBottomSpacerView.getLayoutParams().height;
            if (mainHeight > 0) {
                // Add black bars if aspect ratio is below min
                final float aspectRatio = mainWidth / (float) mainHeight;
                if (aspectRatio < kMinAspectRatio) {
                    final int spacerHeight = Math.round(mainHeight - mainWidth / kMinAspectRatio);
                    ViewGroup.LayoutParams topLayoutParams = mTopSpacerView.getLayoutParams();
                    topLayoutParams.height = spacerHeight / 2;
                    mTopSpacerView.setLayoutParams(topLayoutParams);
                    ViewGroup.LayoutParams bottomLayoutParams = mBottomSpacerView.getLayoutParams();
                    bottomLayoutParams.height = spacerHeight / 2;
                    mBottomSpacerView.setLayoutParams(bottomLayoutParams);
                }
            }
        }
    }
    public synchronized void addText(String text) {
        mDataBuffer.append(text);
    }
    private int mDataBufferLastSize = 0;
    private synchronized void updateTextDataUI() {
        final int bufferSize = mDataBuffer.length();
        if (mDataBufferLastSize != bufferSize) {
            if (bufferSize > maxPacketsToPaintAsText) {
                mDataBufferLastSize = bufferSize - maxPacketsToPaintAsText;
                mTextSpanBuffer.setLength(0);
                mTextSpanBuffer.append(getString(R.string.uart_text_dataomitted)).append("\n");
                mDataBuffer.replace(0, mDataBufferLastSize, "");
                mTextSpanBuffer.append(mDataBuffer);
            } else {
                mTextSpanBuffer.append(mDataBuffer.substring(mDataBufferLastSize, bufferSize));
            }
            mDataBufferLastSize = mDataBuffer.length();
            mBufferTextView.setText(mTextSpanBuffer);
            mBufferTextView.setSelection(0, mTextSpanBuffer.length());        // to automatically scroll to the end
        }
    }
    // endregion
    // region ControllerPadFragmentListener
    public interface ControllerPadFragmentListener {
        void onSendControllerPadButtonStatus(int tag, boolean isPressed);
    }
    // endregion
}
Finished Product
Future Work
Some improvement which could be made are to allow for song uploads from the mobile device rather than reading from the SD card. This could be implemented by an audio processing library which turn the mp3 song data into usable information before sending it to the bluetooth controller. Additionally adding a microphone to create a raise/dimming effect which lights the LEDs based on how loud the drum hit is another possible addition to this project.
Please log in to post comments.
