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.