Lab 1B: Bluetooth

During this lab, I setup bluetooth low energy (BLE) communication with the Artemis Nano via python.

Prelab

I had Python along with Anaconda installed, so I created a new environment and installed the requisite packages

conda create -n fastrobotsble python=3.10
pip install numpy pyyaml colorama nest_asyncio bleak
conda install jupyter # easier than pip

BLE Setup

To enable communication between the two devices, I had to obtain the Artemis’s MAC address and setup uuid’s for the bluetooth service and the characteristics. Using this BLE repo, I uploaded ble_arduino.ino to the Artemis which printed the MAC. Next, I generated the uuids, and updated connections.yaml with:

artemis_address: 'c0:81:60:26:29:64'

ble_service: '699025ae-a586-4521-b1c9-32d90b7cd3db'

characteristics:
  TX_CMD_STRING: '9750f60b-9c9c-4158-b620-02ec9521cd99'

  RX_FLOAT: '27616294-3063-4ecc-b60b-3470ddef2938'
  RX_STRING: 'f235a225-6735-4d73-94cb-ee5dfce9ba83'

Similarly, I update BLE_UUID_TEST_SERVICE in ble_arduino.ino. I then used the jupyter notebook demo.ipynb to initiate a BLE connection with the Artemis.

Task 1: Echo

The echo command is received by the Artemis along with the associated string, and it sends back "Sent Robot says -> {string} :)" via the string characteristic. The arduino implementation of this looks like

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;
    
snprintf(send, MAX_MSG_SIZE, "Sent Robot says -> %s :)", char_arr);
tx_characteristic_string.writeValue(send);

The corresponding code to send and received the echo in python

>>> ble.send_command(CMD.ECHO, "Hello!")
>>> print(ble.receive_string(ble.uuid['RX_STRING']))
Sent Robot says -> Hello! :)

Task 2: Get Time

As a warmup to sending actual sensor values, I used the onboard timer to send the current time after receiving GET_TIME_MILLIS. This was again sent via the string characteristic by encoding the time as a string.

snprintf(send, MAX_MSG_SIZE, "T:%lu", millis());
tx_characteristic_string.writeValue(send);
>>> ble.send_command(CMD.GET_TIME_MILLIS, "")
>>> print(ble.receive_string(ble.uuid['RX_STRING']))
T:220300

Task 3: Notification Handler

The notify interface from BLE is extremely useful when reading streaming data because it doesn’t require constantly reading on the characteristic, instead it just executes a callback when it receives a command. The python code to setup this handler looks like

def readtime(uuid, bytearray):
    print(ble.bytearray_to_string(bytearray)[2:])

ble.start_notify(ble.uuid["RX_STRING"], readtime )

The output is an integer like 230400 representing the ms elapsed.

Task 4: Data Transfer Rate

The same notify service from above can be used to record the effective data transfer rate by just measuring the average difference between consecutive received timestamps.

arr = []

def readtime(uuid, bytearray):
    
    arr.append(int(ble.bytearray_to_string(bytearray)[2:]))
    if len(arr) > 100:
        print("Mean time", np.mean(np.array(arr[1:]) - np.array(arr[:-1])))

ble.start_notify(ble.uuid["RX_STRING"], readtime )

The rate I saw was roughly 1 message every 24 ms. So roughly 42 messages per second.

Task 5: Client Service Model

The notification interface above represents a publisher-subscriber model where the Artemis, the publisher, constantly publishes and it is upto you, the subscriber, to received the data. Another approach is for the Artemis to buffer data and send it to the you upon request. To enable this, I implemented a new command SEND_TIME_DATA where upon receipt the Artemis sends back the buffered time values as above. Also, I recorded the timestamp every epoch.

void update() // called in loop()
{
  timestamps[cnt] = millis();
  cnt = (cnt + 1) % ARRAY_LEN;
}


for (int i = 0; i < ARRAY_LEN; i++){
    char send[MAX_MSG_SIZE];
    snprintf(send, MAX_MSG_SIZE, "%lu", timestamps[i]);
    tx_characteristic_string.writeValue(send);
}

Then, on your computer you just record the timestamps received in an array similar to Task 3.

outs = []

def readtime(uuid, bytearray):
    
    outs.append((ble.bytearray_to_string(bytearray)))

ble.start_notify(ble.uuid["RX_STRING"], readtime )

Task 6: Time and Temperature

Moving on to actual temperature readings, using the same procedure as in Task 5, but implementing SEND_TEMP_DATA that sends time and temperature pairs upon receipt. Again, the temperature buffer is updated every epoch.

void update()
{
  timestamps[cnt] = millis();
  temps[cnt] = getTempDegF();
  cnt = (cnt + 1) % ARRAY_LEN;
}

for (int i = 0; i < ARRAY_LEN; i++){
    char send[MAX_MSG_SIZE];
    snprintf(send, MAX_MSG_SIZE, "%lu ", timestamps[i]);
    tx_estring_value.clear();
    
    tx_estring_value.append(send);
    tx_estring_value.append(temps[i]);
    tx_characteristic_string.writeValue(tx_estring_value.c_str());
}

The python code is similar,

outs = []

def readtemp(uuid, bytearray):
    
    outs.append(tuple(map(int,ble.bytearray_to_string(bytearray).split())))

ble.start_notify(ble.uuid["RX_STRING"], readtemp)

Task 7: Comparing Models

You would want to use the publisher-subscriber model (notify) when quick live-streaming communication is required because it can send an up to date message every epoch which allows the computer to more effectively control it. However, this means that each epoch will be much slower which could potentially hinder the control frequency of the robot.

On the other hand, if you want to get as many sensor readings as possible (high data rate), it makes sense to buffer them because then the time between sensor readings is much lower as your not sending bluetooth messages. I saw a recording rate of roughly 3000 readings per second. However, this method would increase the lag between a measurement occuring and the computer receiving the data. Another tradeoff is that this requires using up precious RAM of which the Artemis has only 384kB. At 4 bytes per timestamp, you could buffer at most 98304 (assuming all memory is just used by the array). However, if you want to store both timestamp and temprature this capacity is halved. At 3000 readings per second it would be able to store 16s of data which is likely unnecessary.