Lab 1: The Artemis Board and Bluetooth

Lab 1a

In section 1a, I set up the Arduino IDE and Artemis board. I installed the latest version of the Arduino IDE and the Sparkfun Apollo boards manager using the json link provided. After that, I ran the following four tasks to get comfortable with the IDE and board.

Blink

/File/Examples/01.Basics

The blue LED on the board turns on and off in 1 second intervals.

Serial

/File/Examples/Apollo3/Example04_Serial

This file tests that string can be printed in the Serial Monitor.

Serial monitor output for Serial example

Analog Read

/File/Examples/Apollo3/Example02_AnalogRead

In the video, as I touch the temperature sensor, the LED indicator gets brighter.

Microphone Output

/File/Examples/PDM/Example1_MicrophoneOutput

When I snap my fingers, the frequency gets higher.


Lab 1b

In section 1b, I establish a connection between my laptop and the board with the Bluetooth Stack. To prepare for this section, I read this explanation of Bluetooth Low Energy (BLE).

A Quick Summary of BLE

Bluetooth Low Energy (BLE) is optimized for low power and low data rate. A BLE radio acts like a “community bulletin board”. Any computer that connects to the board is like a community member reading the bulletin board.

Radios can act as the bulletin board or community members/readers. A “bulletin board” radio is formally called a peripheral device, and a “reader” radio is a central device.

Peripheral devices present information with services, which are further divided into characteristics. The analogy is this: think of the services as notices on the bulletin, and characteristics as the paragraphs in those notices. Services are identified by unique numbers called UUIDs.

4 Things a Central Device can do with a Characteristic:


Computer Setup

Board Setup


Configurations

In order to get the connection with the board working, I needed to make some configurations.

Generate UUID Shell
python3 -c "import uuid; print(uuid.uuid4())"

I also updated the UUID in ble_arduino.ino on ArduinoIDE. After finishing the configuration on ble_arduino.ino, I upload it onto the board, and run through demo.ipynb.


Tasks

The following tasks are to ensure that we can receive timestamped messages from the board. In order to complete these tasks, I made a lab1.ipynb. I copied the setup code from demo.ipynb which imports command types and the controller set up functions.

Setup Code Python
# Enable autoreload for smoother notebook iterations
%load_ext autoreload
%autoreload 2

from ble import get_ble_controller
from base_ble import LOG
from cmd_types import CMD
import time
import numpy as np

LOG.propagate = False

Important Functions:

Task 1: ECHO Command

For this task, I implemented the ECHO command on the arduino code and sent the command through the Jupyter notebook.

Arduino C
case ECHO:

    // Extract the next value from the command string as a character array
    char char_arr[MAX_MSG_SIZE];

    success = robot_cmd.get_next_value(char_arr);
    if (!success)
        return;

    tx_estring_value.clear();
    tx_estring_value.append("ECHO < ");
    tx_estring_value.append(char_arr);
    tx_estring_value.append(" >");
    tx_characteristic_string.writeValue(tx_estring_value.c_str());

    Serial.print("Sent back: ");
    Serial.print(char_arr);
    
    break;
Jupyter Notebook Python
ble.send_command(CMD.ECHO, "Go Big Red!")
s = ble.receive_string(ble.uuid['RX_STRING'])
print(s)

Serial Monitor Output:

ECHO serial monitor output

Jupyter Notebook Output:

ECHO notebook output

Task 2: SEND_THREE_FLOATS

For this task, I implemented the SEND_THREE_FLOATS command on the arduino code, and sent the command through the Jupyter notebook.

Arduino C
case SEND_THREE_FLOATS:
    float float_a, float_b, float_c;
    // Extract the next value from the command string as an integer
    success = robot_cmd.get_next_value(float_a);
    if (!success)
        return;

    success = robot_cmd.get_next_value(float_b);
    if (!success)
        return;

    success = robot_cmd.get_next_value(float_c);
    if (!success)
        return;

    Serial.print("Three Floats: ");
    Serial.print(float_a);
    Serial.print(", ");
    Serial.print(float_b);
    Serial.print(", ");
    Serial.print(float_c);


    break;
Jupyter Notebook Python
ble.send_command(CMD.SEND_THREE_FLOATS, ".5|.3|.2")

Serial Monitor Output:

Three floats serial output

Task 3: GET_TIME_MILLIS

For this task, I needed to add a new command type in cmd_types.py on the Jupyter server side. I also added the enum to the CommandTypes. Finally, I implemented the command on ble_arduino.ino and uploaded it to the board.

Arduino C
case GET_TIME_MILLIS:            
    tx_estring_value.clear();
    tx_estring_value.append("T:");
    tx_estring_value.append((int)millis());
    tx_characteristic_string.writeValue(tx_estring_value.c_str());

    break;
Jupyter Notebook Python
ble.send_command(CMD.GET_TIME_MILLIS, "")
s = ble.receive_string(ble.uuid['RX_STRING'])
print(s)

Jupyter Notebook Output:

Get time millis notebook output

Task 4: Notification Handler for GET_TIME_MILLIS

This task builds upon task 3. A notification handler has two arguments, the UUID of the sender, which in this case is ble.uuid['RX_STRING'], and the data in the characteristic, which is a bytearray. The notification handler function converts the data from a byte array to a string. We then extract the string with simple python string slicing.

Jupyter Notebook Python
def notification_handler(sender_uuid, data):
    decoded = ble.bytearray_to_string(data)
    print(decoded[2:])

ble.start_notify(ble.uuid['RX_STRING'], notification_handler)
print("Notifications for GET_TIME_MILLIS:")

ble.send_command(CMD.GET_TIME_MILLIS, "")

Jupyter Notebook Output:

GET_TIME_MILLIS notifications output

Task 5: GET_TIME_MILLIS Loop

For this task, I called the GET_TIME_MILLIS command 100 times. Then, I noted the initial and final timestamp. As seen in the notebook output, the initial time stamp was 1416412, and the final time stamp was 1422340. Each message contains 9 characters, including the "T:". Each character is 1 byte, so each message is 9 bytes. Therefore, there were 900 bytes of data sent over 1422340 - 1416412 = 5928ms. With a simple ratio calculation, the rate is ~150 bytes/sec.

Jupyter Notebook Python
def notification_handler(sender_uuid, data):
    decoded = ble.bytearray_to_string(data)
    print(decoded)


ble.start_notify(ble.uuid['RX_STRING'], notification_handler)
print("Starting notifications and loop...")

for i in range(100):
    if i == 0:
        print("Initial timestamp: ")
    if i == 99:
        print("Final timestamp: ")
    ble.send_command(CMD.GET_TIME_MILLIS, "")

Jupyter Notebook Output:

Initial timestamp output from notebook
Final timestamp output from notebook

Task 6: SEND_TIME_DATA

First, I added the SEND_TIME_DATA command to cmd_types.py in the Jupyter server and in the enum in the arduino code. Then, I implemented the SEND_TIME_DATA case in the arduino code. There is a global timestamp_arr variable of size 100 at the top of ble_arduino.ino which I use in the implementation. On the Jupyter code side, I wrote a new notification handler that prints the timestamp. After I call the start_notify and send the SEND_TIME_DATA command to the board, we count the number of timestamps returned to ensure it is 100, and it is.

Arduino C
case SEND_TIME_DATA:

    for (int i = 0; i < 100; i++) {
        timestamp_arr[i] = int(millis());
    }

    for (int j = 0; j < 100; j++) {
        tx_estring_value.clear();
        tx_estring_value.append("T:");
        tx_estring_value.append(timestamp_arr[j]);
        tx_characteristic_string.writeValue(tx_estring_value.c_str());
    }

    break;
Jupyter Notebook Python
def time_notification_handler(sender_uuid, data):
    decoded = ble.bytearray_to_string(data)
    print(decoded)

ble.start_notify(ble.uuid['RX_STRING'], time_notification_handler)
ble.send_command(CMD.SEND_TIME_DATA, "");

Jupyter Notebook Output:

There were 100 total messages in the output, but it was too long to paste all of it.

SEND_TIME_DATA initial timestamp output

The final timestamp was:

SEND_TIME_DATA final timestamp output

Task 7: GET_TEMP_READINGS

For this task, I introduced a new array of the same size as timestamp_arr, 100. Similarly to before, I added the command to cmd_types.py and the enum in the arduino code. I added a global variable, temps_arr to reference in the command’s implementation. This command is very similar to SEND_TIME_STAMPS, but I add additional temperature information, which I get using the getTempDegF() function. I found this function looking in the Apollo3 AnalogRead example on ArduinoIDE. After burning the updated code onto the board, I wrote a notification handler that prints timestamp and temperature info. On the Jupyter code, I call start_notify, then call the command 100 times in a loop.

Arduino C
case GET_TEMP_READINGS:

    for (int i = 0; i < 100; i++) {
        timestamp_arr[i] = int(millis());
        temps_arr[i] = getTempDegF();
    }

    for (int j = 0; j < 100; j++) {
        tx_estring_value.clear();
        tx_estring_value.append("T:");
        tx_estring_value.append(timestamp_arr[j]);
        tx_estring_value.append(" F:");   
        tx_estring_value.append(temps_arr[j]);
        tx_characteristic_string.writeValue(tx_estring_value.c_str()); 
    }
    break;
Jupyter Notebook Python
def time_and_temps_notification_handler(sender_uuid, data):
    decoded = ble.bytearray_to_string(data)
    print(decoded)

ble.start_notify(ble.uuid['RX_STRING'], time_and_temps_notification_handler)
ble.send_command(CMD.GET_TEMP_READINGS, "")

Jupyter Notebook Output:

Note, the complete output was cutoff!

GET_TEMP_READINGS notebook output

Task 8: Analysis of Methods

During this lab, I used two different methods of sending and receiving data from the board. The first method continuously calls the GET_TIME_MILLIS command in a loop. As we saw in task 5, we were able to send 100 timestamps over ~6000 ms, or 6 seconds. The other method was collecting data on the board, then sending it by looping through the stored data in an array. This method used no loop on the client side. Looking at the Task 6 images, the initial timestamp was 189033 ms, and the final timestamp was 189035 ms. We were able to receive 100 messages in 2 ms. This is substantially more messages per second received than the first method. Method 1 would be a better option for streaming in data over a longer period of time. Method 2 would be better for precise information, which would be better suited for controlling a robot through feedback, since more information from sensors will be received over less time.

The Artemis board has 384 kB of RAM. The ArduinoIDE shows that global variables take up 30952 bytes of memory, which leaves us with 353048 bytes. If we are considering a data point to simply be a timestamp and temperature point (getTempDegF() call), both as ints, a single data point is 4 + 4 = 8 bytes. 353048 / 8 = 44131. Therefore, we can store a total of 44,131 data points before the board runs out of memory.


Discussion

During this lab, I worked on setting up my workspace and connecting my laptop and the Artemis board through bluetooth. I learned about how BLE works, especially how data is sent and accessed through different characteristics. I got comfortable uploading code to the board through the ArduinoIDE, interacting with the code through Jupyter Lab, and debugging with the Serial Monitor.

The most challenging part of the lab was understanding how to write the notification handler functions used by the start_notify() function. It also took me some time to figure out how I was going to calculate the rate of messages sent.

During debugging, I ran into several issues but was able to resolve them. I didn’t order my command cases the same as my enums, which caused problems. The stop_notify() function had to be called every time I called a start_notify(), or else I’d get errors.


Collaboration

For this lab, I referenced two student pages, Lucca Correia and Rachel Arena, for help with SEND_TIME_DATA and specific parts of the code. I also used ChatGPT to help style my entire website, making sure my code and explanations were displayed properly.