Lab 2 Bluetooth

The purpose of this lab was to setup bluetooth communication in order to communicate with the Artemis wirelessly in the future. This is especially important for debugging. In this lab, Jupyter Lab and Arduino were used in tandem to establish a bluetooth connection.

Prelab

To start the lab, I needed to create a virtual environment. As I already had the latest version of python, I just had to install pip and create my virtual environment from there. Then, I installed the codebase which would allow me to use ArduinoBLE. BLE, or "Bluetooth Low Energy" is a version of Bluetooth optimized for situations that don't require large sets of data transfer. The file ble_arduino.ino was the main file being used in this lab on the Artemis side. On the computer side, Jupyter Notebooks was used. For the virtual environment setup, I used the following code:

                            
                                python3 -m pip install --user virtualenv
                                python3 -m venv FastRobots_ble
                            
                        
From there, I created a folder in my computer where all my Fast Robots code, and the fast robots environment would be kept, and then activated it. After this, I opened Juputer Lab.
                            
                                > .\FastRobots_ble\Scripts\activate
                                > jupyter lab
                             
                        

Configuration

In order to establish a bluetooth connection, I needed to know my device's MAC address. I did this by having it print to the serial monitor. Alongside this, I generated a UUID, or Unique Universal Identifier for my specific board such that I wouldn't connect to another student's board by mistake. All the info was added to the connections.yaml file in the demo codebase.

                            
                                artemis_address: 'c0:89:94:6d:0e:4b'
    
                                #ble_service: '9a48ecba-2e92-082f-c079-9e75aae428b1'
                                ble_service: 'bc858ca3-5aad-4049-8c76-20d773b07cd9'
    
                                characteristics:
                                TX_CMD_STRING: '9750f60b-9c9c-4158-b620-02ec9521cd99'
    
                                RX_FLOAT: '27616294-3063-4ecc-b60b-3470ddef2938'
                                RX_STRING: 'f235a225-6735-4d73-94cb-ee5dfce9ba83'
                            
                        
Initially, I had a lot of trouble connecting to bluetooth. However, it was as simple as adding an extra character to my artemis_address. The serial monitor had printed c0:89:94:6d:e:6b. However, the python script needed 12 characters, so I added a 0 to get an equivalent 12 character MAC address.

Tasks

Echo

It is important for the Artemis Board to be able to send and modify strings. In order to configure this, I created a function ECHO. On the Arduino side, this consisted of adding a prefix to the message to say "Robot says ->" followed by the original message.

                            
                                char char_arr[MAX_MSG_SIZE];

                                // Extract the next value from the command string as a character array
                                success = robot_cmd.get_next_value(char_arr);
                                if (!success)
                                    return;
                                
                                tx_estring_value.clear();
                                tx_estring_value.append("Robot Says -> ");
                                tx_estring_value.append(char_arr);
                                tx_characteristic_string.writeValue(tx_estring_value.c_str());
                                
                                break;
                            
                        

On the python side, the results:

Echo Results

Get_Time_Millis

Next, I used the onboard timer to track and send time stamps through the bluetooth connection. This would be done a variety of ways throughout the lab, however to start I just sent a single timestamp by creating a command GET_TIME_MILLIS.

On the Arduino side:

                        
                            case GET_TIME_MILLIS:

                                tx_estring_value.clear();
                                tx_estring_value.append("T: ");
                                tx_estring_value.append((float) millis());
                                tx_characteristic_string.writeValue(tx_estring_value.c_str());

                                Serial.println("Sent time");

                                break;
                        
                    

On the python side:

Get Time Millis Results

Notification Handler

Next, I set up a notification handler in order to receive strings and extract necessary data from them. In its original form, the notification handler only consistent of the print line now contained within the else statement.

                        
                            def notif_handler(uuid, notif):
                                s = ble.bytearray_to_string(notif)

                                #Contains Temp Readings
                                if("|" in s):
                                    sep_notif = s.split(" | ")
                                    print(sep_notif[0] + ": " + sep_notif[1] + " is the current time and " + sep_notif[2] + " is the current temp in Fahrenheit.")
                                
                                #Does not contain temp readings
                                else:
                                    print(s + " is the current time.")
                        
                    

In later parts of the lab, the notification handler is used to handle large sets of data at once by storing them in an array on the Arduino side and then handling that array in the python notif_handler function.

Live-Sending Time Data

Before implementing an array to send data, I first wrote a loop which connects timestamps over 5 seconds, and sends them back over bluetooth immediately. I added a sample number to each timestamp in order to track the rate at which messages were sent. In the end, 155 timestamps were sent by the Arduino in 5 seconds, implying a rate of about 31 messages/second.

On the Arduino side:

                            
                                case TIME_DATA_LOOP:
                                    {
                                    int count = 0;
                                    unsigned long startT = millis();
                                    while (millis() - startT < 5000) {
                                        
                                        tx_estring_value.clear();
                                        tx_estring_value.append("Sample ");
                                        tx_estring_value.append(count);
                                        tx_estring_value.append(": ");
                                        tx_estring_value.append((float) millis());
                                        tx_characteristic_string.writeValue(tx_estring_value.c_str());
                                        count++;

                                    }

                                    Serial.println("Sent time many times");

                                    break;
                                    }
                            
                        

On the Python side: Time Data Loop

Sending Time Data with Arrays

Next, I established a global array which would be used to store timestamps over 5 seconds. Rather than send this data to over bluetooth live, instead a full dataset was collected for 5 seconds and essentially sent in bulk. Initially, I created an array with 5000 indexes, or one message per second. Upon actually running the loop, this array filled up very quickly, meaning messages could be stored much faster than that. Only 1 or 2 of the 5 seconds were even recorded. In future iterations, specifically when temperature and time were sent at the same time, I added a delay to remedy this.

On the Arduino side:

                        
                            case SEND_TIME_DATA:
                            {
                            int i = 0;
                            unsigned long startT = millis();

                            //Build the Array
                            while ((millis() - startT < 500) && (i < data_array_size)) {
                                
                                time_data[i] = (int) millis();
                                i++;

                                if(i == data_array_size-1)
                                Serial.println("Youch! Out of memory!");
                                
                            }

                            //Send back the array
                            for (int j = 0; j < data_array_size; j++) {

                                if(time_data[j] == 0)
                                break;

                                tx_estring_value.clear();
                                tx_estring_value.append("Sample ");
                                tx_estring_value.append(j);
                                tx_estring_value.append(": ");
                                tx_estring_value.append(time_data[j]);
                                tx_characteristic_string.writeValue(tx_estring_value.c_str());

                            }

                            Serial.println("Sent time many times");

                            break;
                            }
                        
                    

On the Python side: Time Data Loop Time Data Loop

Sending Time and Temperature Data with an Array

Similar to the previous task, I now was sending time and temperature data simultaneously. Each was stored in a separate global array. These global arrays were stored at the very top of the ble_arduino.ino function, such that any case would be able to access them. Global Arrays

In order to actually record data over 5 seconds, I needed to introduce a delay to my data collection. With no delay, 1000 data points were filled up in under a second, though the exact time is unknown. When I added a delay of 20 ms between every collected data point, only 250 data points were needed to capture 5 seconds, meaning 50 datapoints a second.

On the Arduino Side:

                            
                                case GET_TEMP_READINGS:
                                {

                                int i = 0;
                                unsigned long startT = millis();

                                //Build the Array
                                while ((millis() - startT < 5000) && (i < data_array_size)) {
                                    
                                    time_data[i] = (int) millis();
                                    temp_data[i] = (int) getTempDegF();

                                    //Add a buffer
                                    delay(20);

                                    if(i == data_array_size-1)
                                    Serial.println("Youch! Out of memory!");

                                    i++;
                                    
                                }

                                //Send back the array
                                for (int j = 0; j < data_array_size; j++) {

                                    if(time_data[j] == 0 || temp_data[j] == 0.0)
                                    break;

                                    tx_estring_value.clear();
                                    tx_estring_value.append(j);
                                    tx_estring_value.append(" | ");
                                    tx_estring_value.append(time_data[j]);
                                    tx_estring_value.append(" | ");
                                    tx_estring_value.append(temp_data[j]);
                                    tx_characteristic_string.writeValue(tx_estring_value.c_str());

                                }

                                Serial.println("Sent time many times");

                                break;
                                }
                            
                        
It is also worth noting that in all these commands, I had the serial monitor print when it was done.

On the Python side: Global Arrays

Advantages and Disadvantages of Each Method

Each method of sending data has its own pros and cons. The first method of sending datapoints live is very "quick" in the moment. For very short operations it may be beneficial to be able to watch the measurements live, especially in a case where debugging is needed. However, this method essentially clips the data, because the bluetooth connections can only send datapoints so fast. For steady state cases, say if the robot is just driving straight without turns, this does not matter much. However for complex motions this data clipping is a disadvantage.

In comparison, the array method of sending data was incredibly fast. I was able to store at a rate faster than one timestamp per second, implying a very fast data transfer rate. In addition to this, it was very fast to send and process the data array using the notification handler. It would be useful to collect data this way for something like a stunt, and be able to process the success of an operation after it is completed and be able to optimize it from there.

This begs the question of, when will the Artemis literally run out of data? The Artemis has 384 kB of RAM. If every float takes up 4 bytes of RAM, then 96,000 datapoints can be stored before memory runs out. If we are recording both time and temperature, this is 48,000 datapoints per measurement. However, each string also takes up a certain number of bytes per character. This means even less data can be sent to the notification handler before storage runs out. In comparison, sampling at a specific frequency could be much faster. If we transmit 16-bit pieces of data at a rate of 150 Hz, then over 6 seconds, 1500 bytes will be used. Dividing 384 kb by 1500 means 256 5 second intervals of data can be sent before storage runs out!

Discussion

Bluetooth communication will be extremely useful in future labs for communication between the bot and my computer. I imagine this will be useful for feedback loops, for example. This lab also made me more comfortable with Jupyter Labs.