The Artemis Nano supports Bluetooth connections, which will enable wireless communication when I build a robot in future labs.
In Part B of Lab 1, I configured the Artemis and my MacBook to exchange data over Bluetooth Low Energy (BLE).
Prelab
I already had Python and pip installed on my Mac. For Part B of the lab, I created a virtual Python environment and installed the necessary packages.
python3 -m venv ~/.virtualenvs/fastrobots_ble
source ~/.virtualenvs/fastrobots_ble/bin/activate
pip install numpy pyyaml colorama nest_asyncio bleak jupyterlab
Next, I downloaded the provided ble_robot-1.1 codebase and installed the ArduinoBLE library in Arduino IDE.
The codebase contains Arduino and Python utilities that handle connections, commands, and data types for Bluetooth BLE communication between the Artemis and my Mac. Unlike conventional Bluetooth connections, BLE is optimized for reduced power usage at the cost of lower data rates.
On the Artemis "peripheral" side, data is posted to a bulletin board of sorts, which the Python "client" side can then read at will or subscribe to asynchronously using a notify mechanism. Commands and messages can be sent by Python to the Artemis, and the Artemis can respond with messages of its own. Importantly, the codebase limits the size of BLE messages to 150 bytes.
Configuration
To establish a BLE connection, the Python client searches for the Artemis by its MAC hardware address. I found this by burning the provided ble_arduino.ino
sketch to the board, which prints the MAC address to the serial monitor.
I also needed to configure a UUID specific to my Artemis BLE service, which I generated in Python.
from uuid import uuid4
uuid4()
I then updated both the Artemis MAC address and BLE service UUID in the files connection.yaml
and ble_arduino.ino
.
On my Mac, I needed to allow processes like iTerm and Visual Studio Code to communicate via Bluetooth through a macOS privacy setting change.
After configuration, I was able to connect to the Artemis using the provided Jupyter notebook, demo.ipynb
.
Tasks
Task 1: Echo
To implement an ECHO
feature, I added logic to the Artemis that captures the command, appends a smiley face to the message, and sends it back.
// In handle_command():
case ECHO:
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("Robot says -> ");
tx_estring_value.append(char_arr);
tx_estring_value.append(" :)");
tx_characteristic_string.writeValue(tx_estring_value.c_str());
ble.send_command(CMD.ECHO, 'Hello')
break;
I then tested the new ECHO
command in Python.
Task 2: Single Time
The Artemis can report its current program time via the millis()
function. I added a GET_TIME_MILLIS
command that sends this time.
// In handle_command():
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;
I then called the GET_TIME_MILLIS
command in Python.
Task 3: Notification Handler
The purpose of a notification handler is to asynchronously process incoming data. I decided here that a Python class would be a good way to manage data storage and activation of callback functions.
class Artemis:
def __init__(self):
self.data = []
# Get ArtemisBLEController object
self.ble = get_ble_controller()
def start(self):
# Connect to the Artemis Device
self.ble.connect()
The benefit of this approach is that the "state" of the Python application is self-contained. In the future, the Artemis
class will be responsible for all my command and data handling.
To connect to the Artemis, I instantiated the class.
nano = Artemis()
nano.start()
I then added a string notification handler.
class Artemis:
...
def notify_time(self):
self.ble.start_notify(
self.ble.uuid['RX_STRING'], self._handle_time,
)
def _handle_time(self, uuid, bytes):
s = self.ble.bytearray_to_string(bytes)
LOG.debug(f'Time: {s}')
self.data.append(int(s[2:]))
Task 4: Time Loop
With the notification handler ready, I added a GET_TIME_MILLIS_MANY
command that tells the Artemis to send time data in a loop. To generate a decent number of messages, I sent times continuously for five seconds.
// In handle_command():
case GET_TIME_MILLIS_MANY: {
unsigned long startTime = millis();
unsigned long currentTime = startTime;
while (currentTime - startTime < 5000) {
tx_estring_value.clear();
tx_estring_value.append("T:");
tx_estring_value.append((int) currentTime);
tx_characteristic_string.writeValue(tx_estring_value.c_str());
// Update current time
currentTime = millis();
}
break;
}
I then called the GET_TIME_MILLIS_MANY
command in Python, which repeatedly triggered the notification handler and logged the data to the console.
The first time value was 119572
and the last was 124554
, while the total number of messages was 166. From this, I calculated that the BLE send rate was roughly 33.3 messages per second.
Because each message can be up to 150 bytes, the effective BLE data transfer rate from the Artemis to my Mac is therefore roughly 5 KiB per second.
Task 5: Time Storage
For this task, I defined a large global array for time data on the Artemis.
const int DATA_SIZE = 10000;
int time_data[DATA_SIZE] = {0};
I then added COLLECT_TIME_DATA
, which iterates for five seconds like the earlier GET_TIME_MILLIS_MANY
but stores the times rather than sending them.
// In handle_command():
case COLLECT_TIME_DATA: {
unsigned long startTime = millis();
unsigned long currentTime = startTime;
int i = 0;
while (currentTime - startTime < 5000) {
currentTime = millis();
time_data[i] = (int) currentTime;
// Prevent over-filling the storage array
if (i++ >= DATA_SIZE) break;
delay(10);
}
break;
}
To send the stored data, I added a SEND_TIME_DATA
command.
// In handle_command():
case SEND_TIME_DATA: {
for (int i = 0; i < DATA_SIZE; i++) {
// Don't send empty regions of the storage array
if (time_data[i] < 1) break;
tx_estring_value.clear();
tx_estring_value.append("T:");
tx_estring_value.append(time_data[i]);
tx_characteristic_string.writeValue(tx_estring_value.c_str());
}
break;
}
In Python, I first called COLLECT_TIME_DATA
, waited five seconds for the Artemis to store times, and then followed up with SEND_TIME_DATA
to retrieve the array.
Task 6: Temperature Storage
Now that I had a structure for collecting and later sending time data over BLE, I could add temperature readings. First, I initialized a second global array on the Artemis.
float temperature_data[DATA_SIZE] = {0};
Next, I created a COLLECT_TEMP_DATA
command to store times and temperatures.
// In handle_command():
case COLLECT_TEMP_DATA: {
unsigned long startTime = millis();
unsigned long currentTime = startTime;
int i = 0;
while (currentTime - startTime < 5000) {
currentTime = millis();
time_data[i] = (int) currentTime;
temperature_data[i] = getTempDegC();
// Prevent over-filling the storage array
if (i++ >= DATA_SIZE) break;
delay(10);
}
break;
}
I also created a GET_TEMP_READINGS
command that loops both time and temperature storage arrays concurrently.
// In handle_command():
case GET_TEMP_READINGS: {
for (int i = 0; i < DATA_SIZE; i++) {
// Don't send empty regions of the storage array
if (time_data[i] < 0.1) break;
tx_estring_value.clear();
tx_estring_value.append("T:");
tx_estring_value.append(time_data[i]);
tx_estring_value.append(",C:");
tx_estring_value.append(temperature_data[i]);
tx_characteristic_string.writeValue(tx_estring_value.c_str());
}
break;
}
In Python, I added a new notification handler to parse the data.
class Artemis:
...
def notify_time_temperature(self):
self.ble.start_notify(
self.ble.uuid['RX_STRING'], self._handle_time_temperature,
)
def _handle_time_temperature(self, uuid, bytes):
s = self.ble.bytearray_to_string(bytes)
LOG.debug(f'Time/Temp: {s}')
pairs = {}
for chunk in s.split(','):
key = chunk[0:1]
pairs[key] = int(chunk[2:]) if key == 'T' else float(chunk[2:])
self.data.append(pairs)
I then called COLLECT_TEMP_DATA
, waited five seconds, and retrieved the data with GET_TEMP_READINGS
.
Task 7: Transfer and Storage Limits
In the real-time transfer method, an advantage is that I can inspect the robot's state while it performs its tasks. This may sometimes be necessary for live data analysis and on-the-fly adjustments of the robot's parameters. Generally, however, the disadvantage of the significantly increased execution time outweighs this benefit. The robot should be fast, after all.
In the post-operational retrieval method, an advantage is that data storage and transfer are decoupled and the Artemis control loop can run as quickly and unhindered as possible. I anticipate favoring this approach throughout the semester because processing data when the robot isn't zipping around the room will allow for more detailed analysis. The disadvantages of storage are that the Artemis has a finite amount of memory and that I cannot see the data until after collection.
The speed increase between data transfer and data storage is significant. From the data collected in Task 6, the first time was 73984
, the last was 78984
, and the total number of data points was 13362. This yields a temperature data storage "rate" of roughly 2,672 operations per second, way faster than the BLE transfer rate of 33.3 messages per second. Without the slight delay caused by the temperature sensor, storing purely time data can be done even faster at around 35,000 operations per second, though this isn't terribly useful on its own.
The Artemis Nano has only 384 KiB (kibibytes) or 393,216 bytes of RAM. A byte contains 8 bits, so the Artemis can also be said to have 3,145,728 bits of memory. That amount of space allows me to store 393,216 8-bit chars
, 196,608 16-bit ints
, 98,304 32-bit floats
/longs
, or 49,152 64-bit doubles
.
Optional 5000-Level: Transfer Rate and Overhead
I'm enrolled in the 4000-level course but continued on. To test how message length affects the data transfer rate over BLE, I created a SEND_REPLIES
command that takes two arguments: the reply length in bytes and the number of messages to send.
// In handle_command():
case SEND_REPLIES: {
int length, count;
robot_cmd.get_next_value(length);
robot_cmd.get_next_value(count);
char char_arr[length];
for (int i = 0; i < length; i++) {
char_arr[i] = 'a';
}
for (int j = 0; j < count; j++) {
tx_estring_value.clear();
tx_estring_value.append(char_arr);
tx_characteristic_string.writeValue(tx_estring_value.c_str());
}
break;
}
I then called SEND_REPLIES
in Python while timing the data transfers.
total = 3600 # bytes
lengths = [5,10,20,30,40,50,60,80,90,100,120,150]
message_rates = []
byte_rates = []
for length in lengths:
# Calculate the message count to keep
# total BLE data transfer constant
count = int(total / length)
nano.command(CMD.SEND_REPLIES, f'{length}|{count}')
# Time the responses
duration = nano.collect(0.25)
message_rates.append(count / duration)
byte_rates.append(count * length / duration)
Lastly, I plotted the results and observed that, as message length increases, the message rate decreases but the data rate in bytes per second increases. This implies that short messages introduce considerable overhead and that fewer large messages are generally preferable.
Optional 5000-Level: Reliability
One idea I had to test for reliability in the BLE connection was to send an ascending sequence of integers as quickly as possible, then check the sum of that sequence against an expected value. I started by adding a TEST_RELIABILITY
command to the Artemis.
// In handle_command():
case TEST_RELIABILITY:
for (int i = 0; i < 1000; i++) {
tx_estring_value.clear();
tx_estring_value.append(i);
tx_characteristic_string.writeValue(tx_estring_value.c_str());
}
break;
Then, I called the command in Python and computed both the expected sum and the sum of the received values.
Because the two sums agreed, I can conclude that no messages were lost and that the BLE connection is indeed reliable.
Conclusion
This lab has been a valuable introduction to Bluetooth and BLE. The only major challenge I faced was during the initial configurations when the Python process kept crashing on my Mac. The fix was to adjust the Bluetooth privacy setting in macOS. To prepare for future work, wherein BLE will likely play a central role, I elected to create a Python class structure for wireless communication.