Part 1
Setup for Artemis board and UART communication
By installing the Arduino IDE ands Arduino Core for Apollo3 following the setup instructions, I was able to program and burn into the Artemis nano.
Lab section
Task 1
We were asked to connect the board to computer and burn "Example: Blink it Up" to the board, and after burning it, we could see the built-in LEDs on the board blinking.
Task 2
We were asked to test the serial example. After burning it into the board, some sentences are printed out to the serial monitor. And it echos my input by printing out it in the serial monitor, as shown in the video.
Task 3
We were asked to test the analogRead example. As shown in the video, the temperature readings are printed to the serial monitor in the format {temp, vcc/3, vss, time, temp_f}. I modified the original 'serial.printf' statement, which you can see in the top half of the video, to print the desired format. When I touch the chip, the values of temp, vcc/3, temp_f increase.
Task 4
We were asked to test PDM/MicrophoneOutput example, We were asked to test the PDM/MicrophoneOutput example and as shown in the video, the highest frequency was printed to the serial monitor. When I whistled into the microphone, the output in the serial monitor increased because the whistle frequency was higher than the frequency of the ambient noise.
Task 5
We are asked to program the board to light up the LED on board when playing musical "A" note. I added following code in the loop function:
if (myPDM.available()) | |
{ | |
myPDM.getData(pdmDataBuffer, pdmDataBufferSize); | |
if (ui32GlobalLoudestFrequency >= 430 && ui32GlobalLoudestFrequency <= 450) | |
{ | |
Serial.printf("in range"); | |
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) | |
delay(300); | |
} else { | |
Serial.printf("not in range"); | |
digitalWrite(LED_BUILTIN, LOW); // turn the LED on (HIGH is the voltage level) | |
delay(300); | |
} | |
printLoudest(); | |
} |
Part 2
Setup for BLE communication
By burning the ble_arduino.ino into the board, the MAC address 'c0:89:90:6d:d:4b' was printed out to the serial output, as shown in figure 1. By running uuid4() in python script, I can get a generated UUID '3f297411-561b-4f87-a592-c14a7c3c8112' for ble_service, as shown in figure 2. Finally, I checked that the UUIDs used in the Arduino sketch are the same as those connection.yaml in python, and that the command types defined in "enum CommandTypes" in the Arduino sketch are the same as the command types defined in cmd_types.py.


The codebase includes Arduino and Python components. In the Arduino part, 'ble_arduino.ino' initialize BLE service, defining characteristics, handling BLE events, and executing robot commands received over BLE.In the Python component, 'demo.ipynb' initializes the BLE connection, connects to, and sends commands to and receives data back from the robot via BLE communication. Other files manage BLE operations, define command structures, and contain BLE connection parameters.
First, Artemis initializes the BLE service and advertises its presence. Then, the Python script discovers available BLE devices and recognizes the board. Once connected, the Python script can send commands and receive data from the board by writing or reading specific BLE characteristics. Artemis can also send data and receive data by updating or reading characteristics values.
Lab section
Task 1
We were asked to write a ECHO command in ble_arduino.ino. As shown in video, when I sent ECHO command with string 'HiHello' from computer, an augmented string 'Robot says -> HiHello :)' was sent from the board and was received by computer.
case ECHO: | |
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_estring_value.append(" :)"); | |
tx_characteristic_string.writeValue(tx_estring_value.c_str()); | |
Serial.print("Sent back: "); | |
Serial.println(tx_estring_value.c_str()); | |
break; |
Task 2
We were asked to write a GET_TIME_MILLIS command in ble_arduino.ino. As shown in video, when I sent GET_TIME_MILLIS command from computer, a time stamp string 'T:49603' was sent from the board and was received by computer.
case GET_TIME_MILLIS: { | |
const int n = snprintf(NULL, 0, "%lu", currentMillis); | |
char single_time_millis_buf[n+1]; | |
snprintf(single_time_millis_buf, n+1, "%lu", currentMillis); | |
Serial.print("The string is : "); | |
Serial.println(single_time_millis_buf); | |
tx_estring_value.clear(); | |
tx_estring_value.append("T:"); | |
tx_estring_value.append(single_time_millis_buf); | |
tx_characteristic_string.writeValue(tx_estring_value.c_str()); | |
Serial.print("Sent back: "); | |
Serial.println(tx_estring_value.c_str()); | |
break; | |
} |
Task 3
We were asked to write a notification handler in Python to receive a string value from a board. The callback function extracts the time from the timestamp string. As shown in the video, the cell in the python script started notifying RX_STRING for 3 seconds. During these 3 seconds, if the board sent RX_STRING, the script wound try to extract the time (i.e. 2473256) from the string (if in timestamp format) (i.e. T:2473256).
# 3 handler | |
import time | |
global received_messages | |
received_messages = [] | |
def time_stamp_handler(sender, time_stamp_received): | |
global received_messages | |
string_time_stamp_received = time_stamp_received.decode("utf-8") | |
# Extract time | |
if "T:" in string_time_stamp_received: | |
time_from_time_stamp = string_time_stamp_received.split("T:")[1].strip() | |
print(f"received: {string_time_stamp_received}") | |
print(f"processed: {time_from_time_stamp}") | |
received_messages.append(time_from_time_stamp) | |
# test handler | |
start_notify_time = time.time() * 1000 | |
ble.start_notify(ble.uuid['RX_STRING'], time_stamp_handler) | |
ble.send_command(CMD.GET_TIME_MILLIS, "") | |
while (time.time() * 1000 - start_notify_time < 3000): | |
pass # notify 3 seconds | |
ble.stop_notify(ble.uuid['RX_STRING']) |
Task 4
We were asked to write a loop for the board to send timestamp messages to the computer. In the lab1_task4_loop_arduino.ino below is the loop that I added to the write_data() function. Because write_data() is called by loop(), this causes the board to send timestamp messages continuously between 10 and 20 seconds after connecting to the computer.
I wrote a code in Python based on lab1_task3_handler_with_test.py to analyze the received data as shown in lab1_task4_loop_based_on_3.py. As shown in the video, the unit in the Python script started notifying RX_STRING for 3 seconds. During these 3 seconds, if the board sends RX_STRING, the script tries to extract the time from the timestamp string and store it in received_messages. Then, it calculated the total time of sending, the number of message that were sent, and the size of these messages. Finally, I got 19 messages in 0.615 second. Thus, the effective data transfer rate is 30.894 messages per second and 1235.772 bits per second.
// loop for lab1 part 2 task 4 | |
if (currentMillis > 1000 * 10 && currentMillis < 1000 * 20) { | |
const int cur_time_ori = snprintf(NULL, 0, "%lu", currentMillis); | |
char buf_cur_time[cur_time_ori+1]; | |
snprintf(buf_cur_time, cur_time_ori+1, "%lu", currentMillis); | |
tx_estring_value.clear(); | |
tx_estring_value.append("T:"); | |
tx_estring_value.append(buf_cur_time); | |
tx_characteristic_string.writeValue(tx_estring_value.c_str()); | |
} else if (currentMillis == 1000 * 20){ | |
Serial.print("stop sending back continuous timestamp"); | |
} |
def count_chars_in_array(strings): | |
total_chars = sum(len(s) for s in strings) | |
return total_chars | |
if len(received_messages) > 1: | |
print(len(received_messages)) | |
start_time_in_artemis = received_messages[0] | |
end_time_in_artemis = received_messages[-1] | |
total_time_in_artemis = (int(end_time_in_artemis) - int(start_time_in_artemis)) / 1000 | |
print(total_time_in_artemis) | |
message_count = len(received_messages) | |
bit_count = 8 * count_chars_in_array(received_messages) | |
rate_msg_in_artemis = message_count / total_time_in_artemis | |
rate_bit_in_artemis = bit_count / total_time_in_artemis | |
print(f"Effective data transfer rate in artemis: {rate_msg_in_artemis} messages per second and {rate_bit_in_artemis} bits per second") |
Task 5
We were asked to write a program for the board to record a certain number of timestamps into an array and send this array to the computer when command SEND_TIME_DATA is received. lab1_task5_send_time_data_cmd_arduino.ino below contains the code added to the write_data() function and the SEND_TIME_DATA command added to handle_command(). Since loop() calls write_data(), this causes the board to record a timestamp between 10 and 20 seconds after connecting to the computer.
I wrote a code in Python to send the SEND_TIME_DATA command and print the received data as shown in lab1_task5_send_time_data_cmd_py.py. As shown in the video, the Python script sends the command and starts notifying RX_STRING for 60 seconds. During these 60 seconds, the board sends elements of the array, which the script accepts and stores in received_messages_about_time_array_ele. Finally we can see that the board shows 400 messages sent and the computer receives the same number of messages.
// in handle_command() | |
case SEND_TIME_DATA: { | |
Serial.println("start sending back time stamps array"); | |
int count_for_sending_back_time_msg = 0; | |
for (int i = 0; i < MAX_STRINGS_TIME_STAMP; i++){ | |
if (timeStampArray[i][0] != '\0') { // Check if the string is not empty | |
// Serial.print("The string is : "); | |
// Serial.println(timeStampArray[i]); | |
tx_estring_value.clear(); | |
tx_estring_value.append(timeStampArray[i]); | |
tx_characteristic_string.writeValue(tx_estring_value.c_str()); | |
Serial.print("Sent back: "); | |
Serial.println(tx_estring_value.c_str()); | |
count_for_sending_back_time_msg++; | |
} | |
} | |
Serial.print("Sent back "); | |
Serial.print(count_for_sending_back_time_msg); | |
Serial.println(" messages"); | |
break; | |
} | |
// in write_data() | |
// task 5 | |
if (currentMillis > 1000 * 10 && currentMillis < 1000 * 20) { | |
char timeStampBaseString[] = "T:"; | |
char timeStampString[10]; | |
const int cur_time_ori = snprintf(NULL, 0, "%lu", currentMillis); | |
char buf_cur_time[cur_time_ori+1]; | |
snprintf(buf_cur_time, cur_time_ori+1, "%lu", currentMillis); | |
strcpy(timeStampString, timeStampBaseString); | |
strcat(timeStampString, buf_cur_time); | |
addTimeStamp(timeStampString); | |
} else if (currentMillis == 1000 * 30){ | |
Serial.println("stop storing timestamp and temperature"); | |
} |
# task 5 | |
global received_messages_about_time_array_ele | |
received_messages_about_time_array_ele = [] | |
def time_stamp_array_element_handler(sender, time_stamp_array_ele_received): | |
global received_messages_about_time_array_ele | |
string_time_stamp_received = time_stamp_array_ele_received.decode("utf-8") | |
received_messages_about_time_array_ele.append(string_time_stamp_received) | |
start_notify_time = time.time() * 1000 | |
ble.start_notify(ble.uuid['RX_STRING'], time_stamp_array_element_handler) | |
ble.send_command(CMD.SEND_TIME_DATA, "") | |
time.sleep(60) | |
ble.stop_notify(ble.uuid['RX_STRING']) | |
print(received_messages_about_time_array_ele) | |
print("receive ", len(received_messages_about_time_array_ele), " messages") |
Task 6
We were asked to write a program for the board to record a certain number of synchronized temperature readings and timestamps to two arrays and send them to the computer when GET_TEMP_READINGS are received. The following 'lab1_task6_send_temp_data_cmd_arduino.ino' contains the code added to the write_data() function and the GET_TEMP_READINGS command added to the handle_command(). Since loop() calls write_data(), this causes the board to record temperature readings and timestamps between 10 and 20 seconds after connecting to the computer.
I wrote a code in Python to send the GET_TEMP_READINGS instruction and to accept the data and store it in the temperature and timestamp arrays, respectively, and print the received data, as 'lab1_task6_send_temp_data_cmd_ py.py' shown. As shown in the video, the Python script sends the command and starts notifying RX_STRING for 60 seconds. During these 60 seconds, the board sends the elements of the array and the script receives, processes, and stores the data. We can see that the board shows 400 messages sent and the computer receives the same number of messages.
case GET_TEMP_READINGS: { | |
Serial.println("start sending back temp + time stamps array"); | |
int count_for_sending_back_time_temp_msg = 0; | |
for (int i = 0; i < MAX_STRINGS_TIME_STAMP; i++){ | |
if (timeStampArray[i][0] != '\0' && tempArray[i][0] != '\0') { // Check if the string is not empty | |
tx_estring_value.clear(); | |
tx_estring_value.append(tempArray[i]); | |
tx_estring_value.append(","); | |
tx_estring_value.append(timeStampArray[i]); | |
tx_characteristic_string.writeValue(tx_estring_value.c_str()); | |
Serial.print("Sent back: "); | |
Serial.println(tx_estring_value.c_str()); | |
count_for_sending_back_time_temp_msg++; | |
} | |
} | |
Serial.print("Sent back "); | |
Serial.print(count_for_sending_back_time_temp_msg); | |
Serial.println(" messages"); | |
break; | |
} | |
#ifdef ADCPIN | |
int external = analogRead(EXTERNAL_ADC_PIN); // reads the analog voltage on the selected analog pin | |
analogWrite(LED_BUILTIN, external); | |
#endif | |
// task 6 | |
if (currentMillis < 1000 * 20 && currentMillis >= 1000 * 10) { | |
char timeStampBaseString[] = "T:"; | |
char timeStampString[10]; | |
char tempBaseString[] = "degF:"; | |
char tempString[20]; | |
const int cur_time_ori = snprintf(NULL, 0, "%lu", currentMillis); | |
char buf_cur_time[cur_time_ori+1]; | |
snprintf(buf_cur_time, cur_time_ori+1, "%lu", currentMillis); | |
strcpy(timeStampString, timeStampBaseString); | |
strcat(timeStampString, buf_cur_time); | |
addTimeStamp(timeStampString); | |
char temp_buffer[10]; | |
floatToTwoDecimalStr(temp_buffer, temp_f); | |
strcpy(tempString, tempBaseString); | |
strcat(tempString, temp_buffer); | |
addTemperature(tempString); | |
} else if (currentMillis == 1000 * 20){ | |
Serial.println("stop storing timestamp and temperature"); | |
} |
# 6 | |
global received_messages_about_time_temp_array_ele, temperature_list, timestamp_list | |
received_messages_about_time_temp_array_ele = [] | |
temperature_list = [] | |
timestamp_list = [] | |
def temp_time_array_element_handler(sender, temp_time_array_ele_received): | |
global received_messages_about_time_temp_array_ele, temperature_list, timestamp_list | |
string_temp_time_received = temp_time_array_ele_received.decode("utf-8") | |
elements = string_temp_time_received.split(',') | |
for element in elements: | |
label, value = element.split(':') | |
if label == 'degF': | |
temperature_list.append(float(value)) | |
elif label == 'T': | |
timestamp_list.append(int(value)) | |
received_messages_about_time_temp_array_ele.append(string_temp_time_received) | |
start_notify_time = time.time() * 1000 | |
ble.start_notify(ble.uuid['RX_STRING'], temp_time_array_element_handler) | |
ble.send_command(CMD.GET_TEMP_READINGS, "") | |
time.sleep(60) | |
ble.stop_notify(ble.uuid['RX_STRING']) | |
print(received_messages_about_time_temp_array_ele) | |
print("receive ", len(received_messages_about_time_temp_array_ele), " messages") | |
print("temperature_list") | |
print(temperature_list) | |
print("timestamp_list") | |
print(timestamp_list) |
Task 7
Method 1 is real-time data transmission. Whenever the board gets a piece of data, it sends it to the receiver immediately. The advantage is that this allows the receiver to give immediate feedback based on the received data. And there is no need for complex data management and storage on the board. The disadvantage is that the effective data transfer rate is low because every BLE packet sent needs a overhead. It is suitable for monitoring application that needs real-time data.
Method 2 is batch data transmission. A certain amount of data is stored on the board and then uniformly sent to the receiver. The advantage is that it increases the effective data transfer rate by reducing the overhead with sending each piece of data individually. It also has less risk of data loss because storing data into arrays is faster than sending data individually. The disadvantage is that this requires additional data processing on the board and uses limited storage space. Also this causes a delay between data collection and data transmission. This method is suitable for applications that value data throughput and where the board will experience scenarios with bursty data generation.
According to output of task 5, the computer receive an array contains 400 messages in 12504-10001=2503ms=2.503seconds. So, the second method can record data in the rate of 159.8 messages per second or 8949.261 bits per second.
In the time stamp array, each time stamp is about 7 bytes + 1 byte('\0') = 8bytes. Array overhead is 4 bytes(pointer size) * num of string. Approximately, each time stamp uses 12 bytes. So, 384kB of RAM can store 1024 * 384 / 12 = 32768 time stamps.
Task 8
Short packets introduce larger overhead This is because the fixed costs associated with initiating and terminating communications are a larger percentage of the total transmission time when the data is small. The larger replies help to reduce overheadthe because the fixed costs are amortized over a larger amount of data. As you can see in the following plot, when the reply sizes increases, the effective data rate increases. However, since EString has limitation on string size, I did not test reply size that is greater than 150 bytes. The following code shows how I send the command and calculate the round-trip time.

case GET_5_BYTE: { | |
tx_estring_value.clear(); | |
tx_estring_value.append("ABCDE"); | |
tx_characteristic_string.writeValue(tx_estring_value.c_str()); | |
break; | |
} | |
case GET_120_BYTE: { | |
tx_estring_value.clear(); | |
tx_estring_value.append("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); | |
tx_characteristic_string.writeValue(tx_estring_value.c_str()); | |
break; | |
} | |
case GET_140_BYTE: { | |
tx_estring_value.clear(); | |
tx_estring_value.append("12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); | |
tx_characteristic_string.writeValue(tx_estring_value.c_str()); | |
break; | |
} |
global start_time_task8_5B | |
global rtt_5B | |
def task_8_handler(sender, msg_received): | |
global start_time_task8_5B, rtt_5B | |
start_time = time.time() # Record the current time before sending the message | |
string_received = msg_received.decode("utf-8") | |
end_time = time.time() | |
print(string_received) | |
rtt_5B = end_time - start_time_task8_5B | |
print('Round-Trip Time is: ', rtt_5B) | |
rtt_5B = 0 | |
ble.start_notify(ble.uuid['RX_STRING'], task_8_handler) | |
start_time_task8_5B = time.time() | |
ble.send_command(CMD.GET_5_BYTE, "") | |
ble.stop_notify(ble.uuid['RX_STRING']) | |
# output | |
# ABCDE | |
# Round-Trip Time is: 0.11514091491699219 | |
global start_time_task8_120B | |
global rtt_120B | |
def task_8_handler_120B(sender, msg_received): | |
global start_time_task8_120B, rtt_120B | |
start_time = time.time() # Record the current time before sending the message | |
string_received = msg_received.decode("utf-8") | |
end_time = time.time() | |
print(string_received) | |
rtt_120B = end_time - start_time_task8_120B | |
print('Round-Trip Time is: ', rtt_120B) | |
rtt_120B = 0 | |
ble.start_notify(ble.uuid['RX_STRING'], task_8_handler_120B) | |
start_time_task8_120B = time.time() | |
ble.send_command(CMD.GET_120_BYTE, "") | |
ble.stop_notify(ble.uuid['RX_STRING']) | |
# output | |
# 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 | |
# Round-Trip Time is: 0.23892855644226074 | |
global start_time_task8_140B | |
global rtt_140B | |
def task_8_handler_140B(sender, msg_received): | |
global start_time_task8_140B, rtt_140B | |
start_time = time.time() # Record the current time before sending the message | |
string_received = msg_received.decode("utf-8") | |
end_time = time.time() | |
print(string_received) | |
rtt_140B = end_time - start_time_task8_140B | |
print('Round-Trip Time is: ', rtt_140B) | |
rtt_140B = 0 | |
ble.start_notify(ble.uuid['RX_STRING'], task_8_handler_140B) | |
start_time_task8_140B = time.time() | |
ble.send_command(CMD.GET_140_BYTE, "") | |
ble.stop_notify(ble.uuid['RX_STRING']) | |
# output | |
# 2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 | |
# Round-Trip Time is: 0.2393791675567627 | |
# Effective Data Rate | |
byte_5_edr = 5 / rtt_5B | |
byte_120_edr = 120 / rtt_120B | |
byte_140_edr = 140 / rtt_140B | |
print("Effective Data Rate for tranfering 5 bytes: ", byte_5_edr) | |
print("Effective Data Rate for tranfering 120 bytes: ", byte_120_edr) | |
print("Effective Data Rate for tranfering 140 bytes: ", byte_140_edr) | |
# Effective Data Rate for tranfering 5 bytes: 43.975684069039126 | |
# Effective Data Rate for tranfering 120 bytes: 499.2050273943799 | |
# Effective Data Rate for tranfering 140 bytes: 584.8462146013711 |
Task 9
When I programmed the board to send a fixed string 1000 times at fixed baud rate. I expected that my computer may not receive 1000 strings from the board. However, there is no packet loss. Computer read all the data published from the board. I also tried to use a higher baud rate, but there's still no packet loss. However, if I make the board to send more data, in different type or in different speed, there may have packet loss occur.
Discussion
In this lab, I learned about configuring a new board and the way Bluetooth communication works between the computer and the board. Understanding and using some of the fixed functions needed to communicate with BLE was a challenge. But programming according to the given case can solve this problem.