Skip to content

Instantly share code, notes, and snippets.

@fhunleth
Last active December 7, 2023 19:55
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fhunleth/fae46998609814ae4a8abd44f6f08188 to your computer and use it in GitHub Desktop.
Save fhunleth/fae46998609814ae4a8abd44f6f08188 to your computer and use it in GitHub Desktop.
Bluetooth with Nerves notes

UPDATE: THIS WAS WRITTEN BEFORE BlueHeron EXISTED

This is a braindump of my progress over the weekend to look into Bluetooth support on Nerves. While Bluetooth has a lot of specs, supporting a subset of BLE that makes it easy for Nerves devices to communicate with cell phones and back would be generally useful.

There are two options for BLE support on Nerves:

  1. Make a custom Nerves system that uses bluez
  2. Use Harald

bluez is a fairly complete stack that doesn't integrate well with Nerves and is hard to debug when things go wrong. Harald is all Elixir and simple, but incomplete and missing examples for doing anything except scanning for devices.

IMHO, it would be super useful if Nerves had a library that provided an API similar to Adafruit's Bluefruit, but using Elixir instead of AT commands. Even a subset of that library would be useful. I really hope to encourage someone to take steps toward such a library with this note.

BLE background

The Bluetooth spec is massive and overwhelming. Here's what I think is necessary for getting started with an Elixir BLE stack. This is going to be entirely incomplete.

  1. Bluetooth modules usually connect to processors via a UART
  2. The Bluetooth Host-controller Interface spec (HCI) defines nearly all of the commands. Harald handles sending and receiving HCI messages.
  3. There are some proprietary HCI commands that are documented by the module manufacturers. I don't think they're necessary to know except for initialization. And the RPi0W is currently set, so start there.
  4. The next layer up from HCI is L2CAP. The point of L2CAP is to be able to segment messages into smaller chunks and multiplex different kinds of data. BLE only uses a subset of L2CAP. The reset of L2CAP is for Bluetooth Classic.
  5. Then there's GAP which handles advertisements and connections. I'm pretty sure it doesn't need L2CAP.
  6. Finally there's GATT which lets you send data.

Goals

I'm not 100% sure on what the best order is, but I think that a reasonable goal would be to create a set of demos:

  1. BLE Beacon - This example would publish some information to the world. Basically a hello world style app that you could see on a cellphone (like in the LightBlue app)
  2. Counter monitor - This example would let you connect to it and it would share a counter
  3. HID keyboard (HID over GATT) - This example would advertise itself as a HID keyboard and after being paired, it would type
  4. UART - This example would be able to connect to Adafruit's BluefruitConnect app

Certainly, the more examples the better, so please add if there's something of interest to you.

Implementation strategy

Since the Bluetooth spec is large and overwhelming, the idea is to setup examples using the BlueKitchen stack, capture and study traces of it doing small things, and then replicating those in Elixir.

Currently Harald is unmaintained and Very has put it on their "labs" github organization to indicate that they're done with it. While it's possible that someone might put time into it again, I recommend forking it to avoid any frustration. Then at some later time when there's been progress, hopefully we can all get back together and rally around a common library.

Setting up a test environment

My test environment is a Raspberry Pi Zero W connected to my Mac via a USB cable. I've also tried this on a Linux desktop and it worked too.

The RPi0 W has a Cypress CYW43438 module in it. Note that the RPi 3 A+ and B+ have a CYW43455 and Beaglebones with BT use TI Wilink modules. There's a device-specific part of the BLE module setup that means that you should expect differences between parts. For the Raspberry Pi/Cypress case, firmware can be found at github/LibreELEC/brcmfmac_sdio-firmware-rpi. You won't need to download firmware to do what I did, but keep it in mind since a proper solution neeeds to be aware of this.

Set up the RPi Zero W

Download the firmware and use fwup to copy it to a MicroSD card.

This firmware boots the RPi Zero W and makes the UART going to the CYW43438 available over USB. The UART is set to 115200 baud. This is slower than it should be, but it worked for me, so I didn't bump it higher.

Test with Harald

Plug in the RPi0W and check that it shows up as a tty:

$ ls /dev/tty.usbmodem14601                                                                                                                                  
/dev/tty.usbmodem14601

Get the Harald source code, compile and run it:

$ iex -S mix                                                                                                                                                 
Erlang/OTP 22 [erts-10.7.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Interactive Elixir (1.10.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Harald.Transport.start_link(namespace: :bt, adapter: {Harald.Transport.UART, device: "/dev/tty.usbmodem14601", uart_opts: [speed: 115_200]})
{:ok, #PID<0.203.0>}
iex(2)> Harald.LE.scan(:bt)
%{
  5106530278845 => %Harald.HCI.Event.LEMeta.AdvertisingReport.Device{
    address: 5106530278845,
    address_type: 1,
    data: [
      {"Manufacturer Specific Data",
       <<6, 0, 1, 9, 32, 2, 76, 112, 87, 185, 24, 132, 221, 193, 5, 102, 252,
         105, 140, 179, 230, 17, 29, 123, 175, 14, 147, 21, 139>>}
    ],
    event_type: 3,
    rss: 158
  },
  ...
}

Test with BlueKitchen's BLE stack

Clone btstack

A couple modifications need to be made:

$ cd btstack/port/posix-h4

Edit main.c to turn off high speed mode and set the tty path:

$ git diff -w                                                                                                                                           
diff --git a/port/posix-h4/main.c b/port/posix-h4/main.c
index 2b09bf1e6..be2a27f0e 100644
--- a/port/posix-h4/main.c
+++ b/port/posix-h4/main.c
@@ -204,7 +204,7 @@ static void local_version_information_handler(uint8_t * packet){
         case BLUETOOTH_COMPANY_ID_BROADCOM_CORPORATION:
             printf("Broadcom/Cypress - using BCM driver.\n");
             hci_set_chipset(btstack_chipset_bcm_instance());
-            use_fast_uart();
+            //use_fast_uart();
             is_bcm = 1;
             break;
         case BLUETOOTH_COMPANY_ID_ST_MICROELECTRONICS:
@@ -246,7 +246,8 @@ int main(int argc, const char * argv[]){
     // config.device_name = "/dev/tty.usbserial-A900K2WS"; // DFROBOT
     // config.device_name = "/dev/tty.usbserial-A50285BI"; // BOOST-CC2564MODA New
     // config.device_name = "/dev/tty.usbserial-A9OVNX5P"; // RedBear IoT pHAT breakout board
-    config.device_name = "/dev/tty.usbserial-A900K0VK"; // CSR8811 breakout board
+    //config.device_name = "/dev/tty.usbserial-A900K0VK"; // CSR8811 breakout board
+    config.device_name = "/dev/tty.usbmodem14601"; // CSR8811 breakout board

     // accept path from command line
     if (argc >= 3 && strcmp(argv[1], "-u") == 0){

Then run make.

There are lots of examples. Here's one:

$ ./gatt_counter                                                                                                                                         
Packet Log: /tmp/hci_dump.pklg
H4 device: /dev/tty.usbmodem14601
BTstack counter 0001
Local version information:
- HCI Version    0x0007
- HCI Revision   0x0000
- LMP Version    0x0007
- LMP Subversion 0x2209
- Manufacturer 0x000f
Broadcom/Cypress - using BCM driver.
Local name: BCM43430A1
BTstack up and running at B8:27:EB:9A:47:33

Once it says BTstack up and running, go to device that supports BLE. I used the LightBlue app on my phone.

You should see a device called "LE Counter" show up in the app. It might be "Unnamed" at first. Click on it.

Then click on the UUID 0x0000FF11-0000-1000-8000-00805F9B34FB.

Click "Listen for notifications". This starts printing out on the console:

BTstack up and running at B8:27:EB:9A:47:33
BTstack counter 0002
BTstack counter 0003
BTstack counter 0004
BTstack counter 0005
BTstack counter 0006
BTstack counter 0007
BTstack counter 0008
BTstack counter 0009
BTstack counter 0010
BTstack counter 0011
BTstack counter 0012

In the LightBlue app, you should see data. Look at the end at the strings 30303032, etc. That's ASCII for 0002. I.e., the count comes over as ASCII data in this example.

Type CTRL-C to exit.

Test HID over GATT

Using the BlueKitchen demos, run:

$ ./hog_keyboard

Now, go to a device that supports pairing with keyboards and search for HID Keyboard. Pair with the keyboard and then go back to the demo app and start typing.

Read traces

The BlueKitchen stack logs HCI data to /tmp/hci_dump.pklg. This file can be read by Wireshark.

XCode also as program called PacketLogger that can read this file, but I found Wireshark to be easier.

The .pklg file has real HCI packets interleaved comments which I thought was cool. Assuming the file format is simple, this seems like something that would be good to support to make it easier to compare traces from Elixir and BlueKitchen.

Useful links

@fhunleth
Copy link
Author

@robmckinnon Try adding -L to the curl commandline to follow the redirect from GitHub.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment