How to Send Bluetooth Packets and Write Requests to a BLE Device via the Command Line

At revWhiteShadow, we understand the intricacies of modern device communication and the desire to control your Bluetooth Low Energy (BLE) devices programmatically, directly from the command line. This is particularly relevant when you aim to automate tasks, integrate BLE devices into custom workflows, or simply gain a deeper understanding of their operational parameters. You’ve identified a common challenge: interacting with BLE devices without relying on proprietary mobile applications or graphical interfaces. This guide is meticulously crafted to provide you with the most comprehensive and actionable insights, drawing from real-world scenarios and offering robust solutions that aim to outrank existing resources on this critical topic.

You’ve embarked on a journey to control a BLE light, and through diligent packet sniffing, you’ve unearthed crucial details: the device’s MAC address, the specific handle for controlling the light’s status, and the hexadecimal value that dictates the light’s state. Your initial attempts with gatttool, a utility often encountered in BLE development, have encountered predictable roadblocks. The “Connection refused (111)” error when your phone is connected, and the “Device or resource busy (16)” error when your phone is disconnected, are classic symptoms of how BLE devices manage simultaneous connections and resource allocation. The deprecation of gatttool further complicates matters, signaling the need for more modern and supported approaches.

We will delve into the reasons behind these errors and, more importantly, provide you with alternative, effective methods to achieve your goal. Our focus will be on empowering you with the knowledge to interact with BLE devices using reliable command-line tools, ensuring your programmatic control is both successful and sustainable.

Understanding BLE Device Interaction and Connection Challenges

Bluetooth Low Energy (BLE) operates on a fundamentally different paradigm than its classic Bluetooth counterpart. It is designed for low power consumption and intermittent communication, making its connection management and data transfer protocols distinct. When you attempt to connect to a BLE device with multiple clients simultaneously, or when a device is already engaged in an active connection, you will inevitably encounter errors.

The Nature of BLE Connections

A BLE device typically supports a limited number of concurrent connections. When a BLE peripheral (like your light) establishes a connection with a central device (like your smartphone), it allocates resources to maintain that connection. Attempting to initiate another connection while one is active can lead to:

  • Connection Refused (111): This error often signifies that the BLE peripheral has reached its maximum connection limit, or it is configured to deny new connections when an active one exists. The device prioritizes the existing connection.
  • Device or Resource Busy (16): This error typically occurs when you try to access a resource or initiate an operation on a device that is already being utilized by another active process or connection. In your case, when your phone is connected, the device’s BLE stack is busy managing that communication. When you disconnect your phone, if the underlying Bluetooth adapter on your computer is still attempting to manage a previous, perhaps incomplete, connection attempt, it can also manifest as a “busy” state.

The Legacy of gatttool

gatttool was a valuable tool for interacting with BLE devices, particularly for debugging and basic command-line control. It leverages the Generic Attribute Profile (GATT), which defines how BLE devices structure their data and services. The gatttool --char-write-req command you used is designed to write a value to a specific characteristic (identified by its handle) using a “write request” procedure. This procedure involves a confirmation from the peripheral, ensuring the write operation was successful.

However, the deprecation of gatttool is not without reason. Newer Bluetooth specifications and evolving security measures often render older tools incompatible or less reliable. The errors you’re experiencing, especially the “Device or resource busy” after disconnecting your phone, can be attributed to gatttool’s inability to cleanly manage the BLE connection state or its underlying dependencies on older BlueZ versions. BlueZ is the official Linux Bluetooth protocol stack.

Leveraging Modern Command-Line Tools for BLE Interaction

Given the limitations of gatttool, we must turn to more current and actively maintained tools within the Linux ecosystem. The most prominent and powerful among these is bluetoothctl, a comprehensive command-line utility for managing Bluetooth devices. While you’ve encountered difficulties with bluetoothctl thus far, we will guide you through the correct procedures to establish a connection and perform write operations.

Mastering bluetoothctl for BLE Device Control

bluetoothctl is an interactive tool that provides a wide range of functionalities, from scanning for devices to pairing, connecting, and interacting with their GATT services.

1. Initiating a bluetoothctl Session

To begin, open your terminal and launch bluetoothctl:

sudo bluetoothctl

You will enter an interactive bluetoothctl prompt.

2. Scanning for Your BLE Device

Before you can connect, you need to ensure your computer can “see” the BLE device.

[bluetooth]# scan on

This command will start scanning for nearby Bluetooth devices. You should see output similar to this, listing discovered devices by their MAC address and name (if available):

Discovery started
[CHG] Controller XX:XX:XX:XX:XX:XX Discovering: yes
[NEW] Device XX:XX:XX:XX:XX:XX YourLightName

Locate your light’s MAC address (XX:XX:XX:XX:XX:XX) in the scan results. Once you have identified it, you can stop the scan to reduce clutter:

[bluetooth]# scan off

3. Connecting to the BLE Device

Establishing a stable connection is paramount.

[bluetooth]# connect XX:XX:XX:XX:XX:XX

Replace XX:XX:XX:XX:XX:XX with your device’s MAC address. If the connection is successful, you will see output indicating this:

Attempting to connect to XX:XX:XX:XX:XX:XX
Connection successful

If you encounter connection issues, ensure your device is discoverable and not already connected to another primary device. You might need to power cycle your BLE light or ensure no other application is actively controlling it.

4. Discovering GATT Services and Characteristics

Once connected, you need to identify the specific GATT services and characteristics that control your light. You already know the handle (0x0009) for controlling the light status. However, in a broader context, you would typically discover these.

Within the bluetoothctl prompt, after a successful connection, you can use the show command to see details about the connected device:

[bluetooth]# show XX:XX:XX:XX:XX:XX

This will provide information about the device, including its services. To specifically list characteristics, you might need to use other tools or re-examine packet captures. However, since you have the handle, we can proceed directly.

5. Performing a GATT Write Operation

Now, we move to the core task: writing the value to turn on your light. bluetoothctl uses the gatttool’s syntax for writing characteristics, but it’s integrated directly and works with the active connection managed by bluetoothctl.

To write a long write (a characteristic that accepts more than 20 bytes, though your value is shorter, this command structure is generally used for writing arbitrary values):

[bluetooth]# char-write-req XX:XX:XX:XX:XX:XX 0x0009 c7e3f68520e8d5ae5acd17760a01459d

Let’s break down this command:

  • char-write-req: This specifies that we are performing a GATT characteristic write with a request. This means the peripheral will acknowledge the write operation.
  • XX:XX:XX:XX:XX:XX: The MAC address of your BLE device.
  • 0x0009: The handle of the characteristic you want to write to.
  • c7e3f68520e8d5ae5acd17760a01459d: The hexadecimal value you want to write. This is the payload that instructs the light to turn on.

Crucial Considerations for char-write-req:

  • Connection State: Ensure your device is connected in bluetoothctl before executing this command.
  • Value Format: The value needs to be provided as a hexadecimal string.
  • Characteristic Properties: The characteristic at handle 0x0009 must support the “Write Request” property. If it only supports “Write Without Response,” you would use char-write instead. However, given your initial gatttool command used write-req, it’s highly likely this is the correct method.

If you need to write a value that is longer than 20 bytes (which your current value is not, but for future reference):

You might need to use the char-write command, which implies “Write Without Response,” or handle the long write in chunks. However, for your specific case, char-write-req is the appropriate command.

Example of a successful write:

[bluetooth]# char-write-req XX:XX:XX:XX:XX:XX 0x0009 c7e3f68520e8d5ae5acd17760a01459d

A successful write might not return explicit success confirmation in bluetoothctl for every operation, but the absence of an error message, combined with the observed behavior of your light (it should turn on), indicates success.

6. Disconnecting from the Device

Once you are finished, it’s good practice to disconnect cleanly.

[bluetooth]# disconnect XX:XX:XX:XX:XX:XX

And then exit bluetoothctl:

[bluetooth]# exit

Troubleshooting bluetoothctl Connection Issues

If you still face difficulties connecting or writing with bluetoothctl:

  • Pairing: Sometimes, pairing is necessary before connecting. Use pair XX:XX:XX:XX:XX:XX. You might be prompted for a PIN.
  • Trusting: Trusting the device can also help establish more stable connections. Use trust XX:XX:XX:XX:XX:XX.
  • Adapter Status: Ensure your Bluetooth adapter is powered on. Use show to see the adapter status and power on.
  • BlueZ Version: Ensure you have a reasonably recent version of BlueZ installed. This can usually be updated via your distribution’s package manager.
  • Device State: Try toggling the power on your BLE light. Sometimes, a simple reset can resolve persistent connection issues.

Exploring Alternative Libraries and SDKs

While bluetoothctl is powerful for direct command-line interaction, for more complex or script-driven automation, you might consider using programming libraries that abstract away the lower-level Bluetooth interactions. These offer greater flexibility and integration capabilities.

Python and the bleak Library

Python is a popular choice for scripting and automation, and the bleak library provides a robust and cross-platform way to interact with BLE devices.

1. Installing bleak

You can install bleak using pip:

pip install bleak

2. Writing a Python Script for Your BLE Light

Here’s a conceptual Python script that accomplishes what you’re aiming for:

import asyncio
from bleak import BleakClient

# Device details
DEVICE_ADDRESS = "XX:XX:XX:XX:XX:XX"  # Replace with your device's MAC address
HANDLE_LIGHT_STATUS = 0x0009
VALUE_LIGHT_ON = bytes.fromhex("c7e3f68520e8d5ae5acd17760a01459d")

async def send_ble_command():
    print(f"Connecting to {DEVICE_ADDRESS}...")
    async with BleakClient(DEVICE_ADDRESS) as client:
        if client.is_connected:
            print("Connected successfully.")

            try:
                print(f"Writing value {VALUE_LIGHT_ON.hex()} to handle {HANDLE_LIGHT_STATUS}...")
                # Using write_gatt_char for characteristic writes.
                # For a write request, you might need to ensure the characteristic supports it.
                # 'write_gatt_char' typically handles both write-req and write-without-response
                # based on the characteristic's properties.
                await client.write_gatt_char(
                    char_specifier=HANDLE_LIGHT_STATUS,
                    data=VALUE_LIGHT_ON,
                    response=True # This ensures it's a write request
                )
                print("Command sent.")
            except Exception as e:
                print(f"Error writing characteristic: {e}")
        else:
            print("Failed to connect.")

if __name__ == "__main__":
    asyncio.run(send_ble_command())

Explanation of the Python Script:

  • async def send_ble_command():: Defines an asynchronous function to handle the BLE operations.
  • async with BleakClient(DEVICE_ADDRESS) as client:: This is the core of the connection. BleakClient manages the connection to the BLE device. The async with statement ensures the client is properly initialized and disconnected when the block is exited.
  • await client.write_gatt_char(char_specifier=HANDLE_LIGHT_STATUS, data=VALUE_LIGHT_ON, response=True): This is the equivalent of your gatttool --char-write-req.
    • char_specifier: Can be the handle (as you have) or the UUID of the characteristic.
    • data: The payload for the write, provided as a byte string. bytes.fromhex() converts your hex string to bytes.
    • response=True: This crucial parameter tells bleak to use a “Write Request” operation, which requires an acknowledgment from the peripheral. If the characteristic supported “Write Without Response,” you would set response=False.

3. Running the Python Script

Save the script (e.g., as control_light.py) and run it from your terminal:

sudo python control_light.py

Important Notes for Python Scripting:

  • Permissions: Running BLE operations often requires root privileges, hence the sudo.
  • Event Loop: bleak relies on an asyncio event loop, which is why asyncio.run() is used.
  • Error Handling: The script includes basic error handling, which you can expand upon for production-ready applications.
  • Characteristic Properties: The success of write_gatt_char with response=True depends on the GATT characteristic’s properties. If it doesn’t support write requests, you might get an error.

Node.js and bleno or noble

For JavaScript developers, libraries like noble (for the central role) and bleno (for the peripheral role) are available. noble is particularly relevant if you want to act as a central device controlling a peripheral.

1. Installing noble

npm install noble

2. Conceptual Node.js Script

const noble = require('noble');

const DEVICE_ADDRESS = 'xx:xx:xx:xx:xx:xx'; // Replace with your device's MAC address
const SERVICE_UUID = 'YOUR_SERVICE_UUID'; // You'll need to find this if not using handle directly
const CHARACTERISTIC_UUID = 'YOUR_CHARACTERISTIC_UUID'; // You'll need to find this if not using handle directly

// If you know the handle and can map it to UUIDs, that's ideal.
// Otherwise, you'll need to discover services and characteristics first.
// For this example, let's assume you discover them.

const VALUE_LIGHT_ON = Buffer.from('c7e3f68520e8d5ae5acd17760a01459d', 'hex');

noble.on('stateChange', function(state) {
  if (state === 'poweredOn') {
    console.log('Scanning...');
    noble.startScanning([SERVICE_UUID]); // Scan for devices with specific service UUIDs
  } else {
    noble.stopScanning();
  }
});

noble.on('discover', function(peripheral) {
  if (peripheral.id === DEVICE_ADDRESS) {
    console.log('Found device:', peripheral.id, peripheral.advertisement);
    noble.stopScanning();

    peripheral.connect(function(error) {
      if (error) {
        console.error('Connection error:', error);
        return;
      }
      console.log('Connected to', peripheral.id);

      // Discover services and characteristics to find the correct UUID for handle 0x0009
      // This part requires more intricate discovery logic.
      // For simplicity, if you knew the UUIDs corresponding to handle 0x0009:
      // peripheral.discoverSomeServicesAndCharacteristics([SERVICE_UUID], [CHARACTERISTIC_UUID], function(error, services, characteristics) {
      //    ...
      // });

      // Assuming you have found the characteristic for handle 0x0009 and it's 'yourCharacteristic'
      // This is a placeholder, as you need the actual UUID for the characteristic
      peripheral.discoverServices(function(error, services) {
        if (error) {
          console.error('Error discovering services:', error);
          return;
        }
        let targetCharacteristic = null;
        for (const service of services) {
          for (const characteristic of service.characteristics) {
            // This is a simplified check. You'd typically compare UUIDs.
            // If you only have the handle, you'd need a way to map it.
            // A common GATT structure might expose handle 9 as a specific characteristic.
            // For demonstration, let's assume a characteristic with UUID '00002a00-0000-1000-8000-00805f9b34fb' (Device Name)
            // is NOT what we want. You'd need to find the correct UUID.

            // For a proper mapping from handle to UUID, you'd need to iterate through all characteristics
            // and examine their properties and UUIDs.
            // The specific UUID for your light status characteristic is critical here.
            // If handle 0x0009 corresponds to a standard characteristic or a custom one, its UUID is needed.
            // For now, let's assume we've identified it.

            // Example: If you found the characteristic with a specific UUID:
            // if (characteristic.uuid === 'some-custom-uuid-for-light-control') {
            //    targetCharacteristic = characteristic;
            //    break;
            // }
          }
          if (targetCharacteristic) break;
        }

        // Once you have the correct characteristic object:
        // For example, if targetCharacteristic is found:
        // targetCharacteristic.write(VALUE_LIGHT_ON, true, function(error) {
        //    if (error) {
        //       console.error('Error writing characteristic:', error);
        //       return;
        //    }
        //    console.log('Light turned on.');
        //    peripheral.disconnect();
        // });
      });
    });
  }
});

Key Points for Node.js:

  • UUIDs: Unlike bluetoothctl which can often use handles directly, Node.js libraries typically require Service UUIDs and Characteristic UUIDs. You will need to perform GATT discovery to find these if they are not readily available from your packet sniffing.
  • Asynchronous Nature: Node.js is heavily asynchronous. You’ll be working with callbacks and Promises.
  • Permissions: Similar to Python, BLE operations often require elevated privileges.

Addressing the Device or Resource Busy Error Systematically

The “Device or resource busy” error, especially after disconnecting your phone, often indicates that the Bluetooth adapter on your computer is still in an unexpected state. This can happen if the previous connection attempt or the shutdown sequence wasn’t clean.

1. Managing Bluetooth Adapter State

  • Toggle Bluetooth: The simplest approach is to turn your computer’s Bluetooth off and then back on. This forces a reset of the adapter and its associated states.

    sudo systemctl stop bluetooth
    sudo systemctl start bluetooth
    

    Or, depending on your system:

    sudo rfkill block bluetooth
    sudo rfkill unblock bluetooth
    
  • Restart Bluetooth Service: A more forceful reset involves restarting the Bluetooth service.

    sudo systemctl restart bluetooth
    

2. Ensuring Exclusive Access

When using command-line tools like bluetoothctl or libraries like bleak, ensure no other application (including your phone if it’s still in proximity and attempting to connect) is actively interfering.

  • Disable Bluetooth on Other Devices: Temporarily disable Bluetooth on your smartphone or any other device that might attempt to connect to your BLE light.
  • Cleanly Disconnect/Exit: Always ensure you cleanly disconnect from the device in bluetoothctl and exit the program if you’re using a script.

3. Resetting BLE Connections

Sometimes, the BLE stack on the host computer can get into a bad state. A full system reboot can resolve these deeper issues.

Advanced Techniques and Considerations

For highly specific or robust control, you might delve into lower-level BlueZ tools or even compile your own tools.

Using hcitool (Legacy but Sometimes Useful)

While hcitool is also considered legacy, certain commands can still be useful for direct HCI (Host Controller Interface) level interactions. However, it’s generally recommended to stick with bluetoothctl for GATT operations.

  • hcitool lescan: Similar to scan on in bluetoothctl, but a different interface.
  • hcitool lecc <BD_ADDR>: Attempts a BLE connection.

However, hcitool does not directly provide a mechanism for GATT characteristic writes in the way you need.

Understanding GATT Attributes and Handles

You’ve identified handle 0x0009. In the GATT hierarchy:

  • Services: A collection of characteristics. Each service has a UUID.
  • Characteristics: The actual data points or command interfaces. Each characteristic has a UUID and a handle. You write to characteristics.
  • Descriptors: Provide additional information about characteristics.

The fact that you have a handle 0x0009 is valuable. In many BLE implementations, handles are sequential. When you use bluetoothctl or bleak, they internally map these handles to the correct GATT attributes to perform operations. The key is that the characteristic at this handle must support the WRITE attribute, and specifically, the WRITE_COMMAND (Write Without Response) or WRITE_REQUEST (Write with Response) property, depending on what your device expects. Your initial command used write-req, implying the latter.

The Importance of UUIDs

While handles are direct memory addresses for attributes on a specific device connection, UUIDs (Universally Unique Identifiers) are the standardized way to identify services and characteristics across different BLE devices. If you were building a more generalized application, you would focus on discovering the Service UUID and then the Characteristic UUID that corresponds to your light control.

Your specific value c7e3f68520e8d5ae5acd17760a01459d is a 16-byte value. It’s not a standard UUID format (which is 128 bits or 16 bytes, but with a specific structure). This is likely your device’s custom command payload for turning the light on.

Concluding Thoughts and Best Practices

Successfully controlling your BLE light via the command line boils down to understanding BLE connection management and utilizing the correct, modern tools.

  • Prioritize bluetoothctl: For direct command-line interaction, bluetoothctl is the recommended and most powerful tool on Linux. Master its scanning, connecting, and char-write-req commands.
  • Embrace Programming Libraries: For automation, integration, or more complex control logic, libraries like Python’s bleak offer a superior and more maintainable solution.
  • Troubleshoot Systematically: If you encounter connection errors, focus on resetting your Bluetooth adapter and ensuring exclusive access to the device.
  • Understand GATT: Familiarize yourself with GATT services and characteristics, even if you are primarily working with handles, to better understand the underlying structure of BLE communication.

By following these detailed steps and insights, you are well-equipped to establish reliable command-line control over your BLE light. This approach ensures you bypass the limitations of deprecated tools and leverage the robust capabilities of modern Bluetooth management utilities and libraries. The goal is to provide you with a definitive pathway to achieving your programmatic control objectives, enabling you to interact with your BLE devices with confidence and precision.