← Back to Dashboard

Lab 1: Artemis Board & Bluetooth 🏹

MAE 4190 • Spring 2026 • Rajarshi Das

Lab 1A: Artemis Board Setup

Objective

The purpose of Lab 1A is to setup and become familiar with the Arduino IDE and the Artemis board. After this lab, we should be comfortable programming the board, using the board LED, reading/writing serial messages over USB, and using the onboard temperature sensor and Pulse Density Microphone.

Prelab

Arduino IDE Installation

I installed the latest version of Arduino IDE and added the SparkFun Apollo3 boards manager using the JSON link from the official repository.

Important: The Artemis board is a 3V board, NOT 5V. Only 3V inputs!

Board Information

The SparkFun RedBoard Artemis Nano is a development board featuring:

Setup

Connected the Artemis board to my computer and selected the correct board and port in Arduino IDE:

Tasks

Task 1: Blink Test

Ran the Blink example from File → Examples → 01.Basics → Blink to test the onboard LED.

Code

I modified the delay to confirm the code was actually running and not some default OS LED blinking:

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(200);  // Reduced delay for faster blinking
  digitalWrite(LED_BUILTIN, LOW);
  delay(200);
}

Demo

Result: LED blinks at the modified rate, confirming the code is running properly!

Task 2: Serial Monitor Test

Tested serial communication using File → Examples → Apollo3 → Example04_Serial.

Demo

Testing the serial monitor to test bidirectional communication at 115200 baud:

Result: Serial communication working perfectly!

Task 3: Temperature Sensor Test

Tested the onboard temperature sensor using File → Examples → Apollo3 → Example02_analogRead.

Demo

Heating up the board by blowing air onto it:

Result: Temperature sensor is responsive and accurately detects temperature changes!

Task 4: Microphone Test

Tested the Pulse Density Microphone (PDM) using File → Examples → PDM → Example1_MicrophoneOutput.

Demo

Playing a 12000Hz audio recording to test if the microphone can detect high frequencies:

Result: Microphone successfully detects the 12000Hz frequency!

Lab 1B: Bluetooth Communication

Objective

The purpose of Lab 1B is to establish communication between your computer and the Artemis board through the Bluetooth stack. The computer will send Python commands from a Jupyter notebook to an Artemis board running the Arduino programming language. We will also establish a framework for sending data from the Artemis to the computer via Bluetooth that will be useful in future labs.

Config

Note: The Artemis address had to be temporarily changed when, in lab, a different board was used. Baud rate was set to 115200.

Python: connections.yaml

# connections.yaml
artemis_address: 'c0:81:a4:24:29:64'
ble_service: 'c041e110-cafe-cafe-cafe-c0ffeec0ffee'
characteristics:
  TX_CMD_STRING: '9750f60b-9c9c-4158-b620-02ec9521cd99'
  RX_FLOAT: '27616294-3063-4ecc-b60b-3470ddef2938'
  RX_STRING: 'f235a225-6735-4d73-94cb-ee5dfce9ba83'

Arduino IDE: ble_arduino.ino

// BLE Service UUID - must match connections.yaml
#define BLE_UUID_TEST_SERVICE "c041e110-cafe-cafe-cafe-c0ffeec0ffee"

// Characteristic UUIDs
#define BLE_UUID_RX_STRING "f235a225-6735-4d73-94cb-ee5dfce9ba83"
#define BLE_UUID_TX_CMD_STRING "9750f60b-9c9c-4158-b620-02ec9521cd99"
#define BLE_UUID_RX_FLOAT "27616294-3063-4ecc-b60b-3470ddef2938"

Successful Connection

Computer side - Python successfully connects to Artemis:

Successful BLE Connection from Computer

Artemis side - Serial monitor shows connection established:

12:16:15.840 -> Advertising BLE with MAC: c0:81:75:25:a:64
12:16:27.334 -> Connected to: 6c:7e:67:bd:52:58
Result: BLE connection established successfully on both sides!

Codebase: BLE Architecture

UUID Selection

The BLE service UUID is the ID that the Artemis board advertises. As long as it follows the UUID format (8-4-4-4-12 hex digits) and is unique, it can be anything. I tested this by creating a custom UUID (c041e110-cafe-cafe-cafe-c0ffeec0ffee) and confirmed it worked after updating both the Arduino sketch and Python config with matching UUIDs.

Communication Flow

The Artemis (peripheral) advertises a BLE service with multiple characteristics. The laptop (central) connects by searching for the UUID, writes commands to a string characteristic, and subscribes to notify-enabled characteristics for replies. Commands are encoded as strings with a numeric type and optional values. The Artemis tokenizes these, processes them, and sends back strings or floats asynchronously via notify.

Tasks

In order to test the robot's sensors more effectively, it is critical to have a working wireless debugging system. The following tasks ensure that we can receive timestamped messages from the Artemis board.

Task 1: ECHO Command

Send a string value from the computer to the Artemis board using the ECHO command. The computer should then receive and print an augmented string.

Python Code and Output

Task 1 Python ECHO command and output

Arduino Serial Output

12:16:15.840 -> Advertising BLE with MAC: c0:81:75:25:a:64
12:16:27.334 -> Connected to: 6c:7e:67:bd:52:58
12:24:04.114 -> Sent back: Robot says -> Wakey wakey, it's time for school!
Result: ECHO command working! Artemis receives string and sends back augmented version.

Task 2: SEND_THREE_FLOATS

Send three floats to the Artemis board using the SEND_THREE_FLOATS command and extract the three float values in the Arduino sketch.

Python Code

Task 2 Python SEND_THREE_FLOATS command

Arduino Serial Output

12:40:16.041 -> Three Floats: 1.00, 2.00, 3.00
Result: Successfully sent and extracted three float values!

Task 3: GET_TIME_MILLIS

Add a command GET_TIME_MILLIS which makes the robot reply with a string such as "T:123456" to the string characteristic.

Python Code and Output

Task 3 Python GET_TIME_MILLIS command

Arduino Serial Output

12:41:10.062 -> Sent back: T:193600
Result: Successfully implemented GET_TIME_MILLIS command and received timestamp!

Task 4: Notification Handler

Setup a notification handler in Python to receive the string value (the BLEStringCharacteristic in Arduino) from the Artemis board. In the callback function, extract the time from the string.

Python Code and Output

Task 4 Python notification handler

Arduino Serial Output

12:42:32.490 -> Sent back: T:275972
Result: Notification handler successfully extracts runtime from string (275972 ms)!

Task 5: Data Transfer Rate

Write a loop that gets the current time in milliseconds and sends it to your laptop to be received and processed by the notification handler. Collect these values for a few seconds and use the time stamps to determine how fast messages can be sent.

Python Handler Code

timestamps = []

def handler_task5(uuid, bytearray):
    indexT = bytearray.find(b"T:")
    runtime = int(bytearray[indexT+2:])
    timestamps.append(runtime)

ble.start_notify(ble.uuid['RX_STRING'], handler_task5)

repeat = 10
for i in range(repeat):
    ble.send_command(CMD.GET_TIME_MILLIS, "")

time.sleep(.2)
ble.stop_notify(ble.uuid['RX_STRING'])

Data Transfer Rate Analysis

Task 5 data transfer rate calculation

Arduino Serial Output

12:54:43.830 -> Sent back: T:31654
12:54:43.863 -> Sent back: T:31716
12:54:43.928 -> Sent back: T:31778
12:54:43.994 -> Sent back: T:31840
12:54:44.061 -> Sent back: T:31902
12:54:44.125 -> Sent back: T:31954
12:54:44.190 -> Sent back: T:32016
12:54:44.256 -> Sent back: T:32078
12:54:44.288 -> Sent back: T:32140
12:54:44.354 -> Sent back: T:32202
Result: Average time between messages: ~60.89 ms. Effective data transfer rate achieved!

Task 6: Array Storage

Create an array that can store time stamps. This array should be defined globally so that other functions can access it if need be. In the loop, rather than send each time stamp, place each time stamp into the array. Add a command SEND_TIME_DATA which loops the array and sends each data point as a string to your laptop to be processed.

The time array is defined globally with a maximum size of 100 elements. As timestamps are collected, they're stored in the array using a circular buffer approach. When the array is full, the SEND_TIME_DATA command sends all stored timestamps in chronological order.

Python Handler Code

def handler_task6(uuid, bytearray):
    data_str = ble.bytearray_to_string(bytearray)
    timestamp = int(data_str)
    timestamps.append(timestamp)
    print(f"Timestamp: {timestamp}")

#------------------------------------------------------

timestamps = []

ble.start_notify(ble.uuid['RX_STRING'], handler_task6)

ble.send_command(CMD.SEND_TIME_DATA, "")

time.sleep(2)  # Wait for 100 data points (reduction not recommended)
ble.stop_notify(ble.uuid['RX_STRING'])

print(f"\nTotal received: {len(timestamps)}")

Arduino Code (SEND_TIME_DATA)

case SEND_TIME_DATA:
    {
        int current_pos = array_index % MAX_ARRAY_SIZE;        

        // Send the "old" part | from current position to end of array
        for (int i = current_pos; i < MAX_ARRAY_SIZE; i++) {
            tx_estring_value.clear();
            tx_estring_value.append(time_array[i]);
            tx_characteristic_string.writeValue(tx_estring_value.c_str());
        }

        // Send the "new" part | from start of array to current position
        for (int i = 0; i < current_pos; i++) {
            tx_estring_value.clear();
            tx_estring_value.append(time_array[i]);
            tx_characteristic_string.writeValue(tx_estring_value.c_str());
        }

        Serial.print("Time data transfer finished | ");
        Serial.print(MAX_ARRAY_SIZE);
        Serial.println(" data points");
        break;
    }

Arduino Serial Output

12:58:22.458 -> Time data transfer finished | 100 data points

Python Output

Timestamp: 693733
Timestamp: 693743
Timestamp: 693753
...
Timestamp: 694713
Timestamp: 694723

Total received: 100
Result: Successfully stored 100 timestamps in array and sent all data in batch!

Task 7: Temperature Array

Add a second array that is the same size as the time stamp array. Use this array to store temperature readings. Each element in both arrays should correspond (i.e., the first time stamp was recorded at the same time as the first temperature reading). Add a command GET_TEMP_READINGS that loops through both arrays concurrently and sends each temperature reading with a time stamp.

Python Handler Code

timeKey = "T:"
tempKey = "|TEMP:"
timestamps, temperatures = [], []

#------------------------------------------------------

def handler_task7(uuid, bytearray):

    timeIndex = bytearray.find(timeKey.encode('utf-8'))
    tempIndex = bytearray.find(tempKey.encode('utf-8'))

    currentTime = int(bytearray[timeIndex+2:tempIndex])
    currentTemp = float(bytearray[tempIndex+6:])

    timestamps.append(currentTime)
    temperatures.append(currentTemp)

    print(f"Timestamp {len(timestamps)}: {currentTime} | Temperature: {currentTemp}")

#------------------------------------------------------
# ble.connect()

ble.start_notify(ble.uuid['RX_STRING'], handler_task7)

ble.send_command(CMD.GET_TEMP_READINGS, "")

time.sleep(2) # Wait for 100 data points (reduction not recommended)
ble.stop_notify(ble.uuid['RX_STRING'])

print(f"\nTotal received: {len(timestamps)}")

Arduino Code (GET_TEMP_READINGS)

case GET_TEMP_READINGS:
    {    
        int current_pos = array_index % MAX_ARRAY_SIZE;        

        // Send the "old" part | from current position to end of array
        for (int i = current_pos; i < MAX_ARRAY_SIZE; i++) {
            tx_estring_value.clear();
            tx_estring_value.append("T:");
            tx_estring_value.append(time_array[i]);
            tx_estring_value.append("|TEMP:");
            tx_estring_value.append(temp_array[i]);
            tx_characteristic_string.writeValue(tx_estring_value.c_str());
        }

        // Send the "new" part | from start of array to current position
        for (int i = 0; i < current_pos; i++) {
            tx_estring_value.clear();
            tx_estring_value.append("T:");
            tx_estring_value.append(time_array[i]);
            tx_estring_value.append("|TEMP:");
            tx_estring_value.append(temp_array[i]);
            tx_characteristic_string.writeValue(tx_estring_value.c_str());
        }

        Serial.print("Temp data transfer finished | ");
        Serial.print(MAX_ARRAY_SIZE);
        Serial.println(" data points");
        break;
    }

Arduino Serial Output

13:05:48.949 -> Time data transfer finished | 100 data points

Python Output

Timestamp 1: 678764 | Temperature: 78.28
Timestamp 2: 678774 | Temperature: 78.28
Timestamp 3: 678784 | Temperature: 79.302
...
Timestamp 99: 679744 | Temperature: 78.28
Timestamp 100: 679754 | Temperature: 78.28

Total received: 100
Result: Successfully stored and sent 100 paired timestamp/temperature readings!

Task 8: Discussion

Discuss the differences between these two methods (streaming vs. batching), the advantages and disadvantages of both and the potential scenarios that you might choose one method over the other.

Method 1: Live Streaming (Task 5)

Send data live. Python requests current timestamp individually and the microcontroller responds. Real time data with no memory constraints.

Tradeoff: Sampling rate is limited due to BLE (Bluetooth) round trip delays (60ms latency avg ~ 16.7Hz sampling rate)

Method 2: Batch Recording (Tasks 6-7)

Uses the microcontroller as a blackbox: continuously logs data on-board at very high speeds (up to 1000Hz), and then sends a large data packet when requested. This results in less BLE overhead and higher data rates.

Tradeoff: Limited RAM: with 362 kB available (after 31 kB used by global variables according to Arduino IDE post flashing the board), you can store roughly 45,000 timestamp-temperature pairs (4 bytes + 4 bytes each). At 100 Hz sampling, this gives ~7.5 minutes of recording before running out of memory.

Key Takeaway: Use Method 1 for real-time monitoring and Method 2 for high-speed data logging with post-processing.