home · posts · about · github

Arpan Dhatt / Mesh Networking with Bluetooth Low Energy

Tue Nov 23, 2021 · 1898 words · 10 min

Introduction

ForestFlock started as a project four friends and I did for a hackathon. After spending a grueling 24 hours writing code for hours on end with very little sleep, we were fortunate enough to win first place. To explain what it is, here's a quick blurb from our project submission page:

ForestFlock is a novel software and hardware system that gives agencies that monitor wilderness areas powerful insights into what is happening in their area. ForestFlock consists of various sensor nodes that are connected in a wireless Bluetooth mesh network that collect and analyze sensor inputs and then transmit relevant information over large distances without an Internet connection. The sensor nodes would be self-sufficient with solar panels for power and the mesh network for connectivity.

There were many components in this project, from hardware sensors that detected humidity to AI-enhanced microphones that detected specific sounds. We even built an iPad app to interface with the network. However, I mainly worked on the Bluetooth mesh network to communicate events without an internet connection. This is what I'll focus on for the rest of this post.

Why Bluetooth Low Energy?

I gravitated towards BLE because it consumed very little power compared to serial Bluetooth which things like audio headphones use. Also, for our particular project, the network wouldn't need to support a large amount of bandwidth. It was only intended to send signals with small amounts of identifying information.

Now Bluetooth 4.2 already has a mesh network protocol built which could have been utilized instead. It also supports larger amounts of data than my implementation supports. However, I decided not to use it for a couple of reasons.

For one, setting it up on the Raspberry Pi was nigh-impossible, especially since the vanishingly small amount of documentation I was following was outdated. Even installing the required meshctl took a lot of time and it didn't even work properly when it was installed. Overall, it was more trouble than it would probably be worth, considering I put in a lot of work in an attempt to just activate it rather than actually interface with it.

Another, more minor reason I decided to keep away from meshctl was because it wasn't updated to use Coded PHY. Bluetooth 5 essentially introduced it as a way to allow extremely long-range BLE communication. If I implemented my own mesh protocol, it would be able to benefit from Coded PHY since the Raspberry Pi 4 implements Bluetooth 5. I did realize at a later point that Coded PHY is an optional feature for Bluetooth 5 implementations. Being a pretty niche tool, it's not implemented on the Raspberry Pi's chip, so I wouldn't have been able to use it, even if I wanted to.

Deciding on a Protocol

It took some time to find an appropriate way to communicate over BLE. In short, BLE works using advertisements and services. A BLE device's services essentially include its features. For a blood glucose monitor, it would have a service where other devices can read some floating point value. The blood glucose monitor advertises that it has some readable value. In its particular case, the value would be the blood gluecose level detected. This is likely the most common case for BLE since it's pretty versatile. You can even allow devices to write small amounts of data through services for things like smart home devices.

However, I ended up doing something more similar to another application of BLE: beacons. Briefly, beacons allow mobile devices like phones to know they're at some location. This is done simply be encoding all the necessary information in the advertisement. No services are exposed by the BLE beacon. The two most common beacon protocols are iBeacon, by Apple, and Eddystone, by Google.

I ended up choosing Eddystone since I found a lot better documentation on it and even some instructions to get a Raspberry Pi to emit a some given data. Eddystone is generally meant to encode some URI and allow nearby devices to open it. For example, it could be the website URL of whatever store a user is at. Instead of sending URI-encoded data, I could just send any bytes of my choosing.

Interfacing with Bluetooth

There were a couple of ways I could interact with Bluetooth on the Raspberry Pi, namely hcitool and bluetoothctl. At this point in time, hcitool and its accompanying tools, like hcidump were deprecated and replaced with bluetoothctl. However, I ended up going with hcitool since there was significantly more documentation on the tool compared to the latter. Also, programmatically using hcitool seemed much easier since it had a few command line options that returned values rather than bluetoothctl which nearly offered a full terminal user interface.

Controlling the Bluetooth chip involved the hcitool cmd command and hcitool lescan command. The first of these had a few subcommands I used to set advertisiment frequency, advertisiment mode, and advertisiment data. The following command activated advertisiment and sets the device's mode to non-connectable. This means that no device can form a connection with this one. It may only receive broadcasted advertisements.

hcitool cmd 0x08 0x000a 01

The 0x08 means that this command is for the Bluetooth controller on the RPi. The 0x000a specifies that the following data is meant to modify the advertising mode of this device. Finally, the 01 is one byte in hexadecimal specifying that the advertising mode should be set to true and be non-connectable. Now that the advertising mode is active, we can specify the frequency as to which Bluetooth advertisements should be sent. The following commands sets the frequency to 10 Hz.

hcitool cmd 0x08 0x0006 A0 00 A0 00 03 00 00 00 00 00 00 00 00 07 00

This command is slighlty more complex. First of all, the 0x0006 is to signify that the advertising frequency is being set. The first two bytes A0 00 set the minimum interval for the advertisement and the second two bytes A0 00 set the maximum interval. The decimal value of A0 is 160 in decimal. The Bluetooth chip will set the interval to 0.625ms * A0 = 100ms. This is once every 10 seconds. You can read some more about this from the stack overflow answer I used to interpret the bytes . I also used a similar process for writing the advertising data but I used the 0x0008 subcommand code.

Reading the incoming data makes use of two command line tools: hcidump and hcitool lescan. The first of these tools simply writes to the standard output the raw bytes of incoming packets. The second tool is once again hcitool. This time I use lescan which is just that: Bluetooth Low Energy scan. The complete command is as follows:

hcitool lescan --duplicates --passive

The optional flags change the behavior of the scanning to allow duplicate advertising data to be stored and the passive flag is to ensure the device doesn't attempt to connect to the transimitting device for more information. Although this command enables the scanning function of the Bluetooth chip, this won't actually output what is received. Instead, the hcidump tool is needed.

hcidump --raw

After the command is run, it will continously output any incoming BLE advertising packets in hexadecimal byte form directly into the standard output. This can be read and parsed by a separate program.

Program Structure

The program ran four separate threads doing the following four tasks:

  1. lescan_thread: Running the hcitool lescan command and keeping that child process open indefinitely. This was needed since as soon as this process was interrupted the device would stop scanning.
  2. packet_thread: This ran the hcidump command line utility and parsed the advertisement bytes.
  3. graph_ingress_thread: This allowed me to connect to any of the Raspberry Pi's through TCP for testing purposes.
  4. advertiser_thread: This final thread would set the output advertisement data with the purpose of echoing whatever data was received by the packet_thread.

The implementations of each of these threads are in this main.rs file.

The packet_thread and graph_ingress_thread needed to communicate with the advertiser_thread to set new advertisement data depending on what was either received from nearby nodes to directly inputted to the program through a TCP socket. This was done through a multiple-producer single-consumer (mpsc) channel. The packet_thread would use the blocking iterator from the hci crate to read data and send it into the channel.

fn packet_thread_fn(tx: Sender<[u8; 40]>) {
    let scanner = hci::packet_reader();
    for packet in scanner { // blocks while new data is being received
        if is_eddystone_packet(&packet) {
            if let Some(data) = extract_data(&packet) {
                tx.send(data).unwrap();
            }
        }
    }
}

On the advertisement thread the data is received and echoed to any nearby listening nodes. If this message has been received before it won't be sent again for a minute. This is so that new messages can make it through the network if they're already transmitting a different message. I didn't really have time to write a proper protocol since the one below has several problems. For one, if there is a "choke point" in the network, one message could get sent through while the other is supressed. Another issue is that the network has a very limited bandwidth. It can only send one message at a time. Originally, I wanted to allow messages to reside for a certain amount of time on the RPi and the advertisment thread would cycle through them. That way multiple messages could get through the network at the same time. Once again, I didn't have the time to modify the code below to make it more optimal.

fn periodic_advertisement_controller(rx: Receiver<[u8; 40]>) {
    let mut first_seen: HashMap<[u8; 40], time::Instant> = HashMap::new(); // WARNING: UNBOUNDED SIZE HASHMAP

    for new_data in rx {
        println!("{}", String::from_utf8_lossy(&new_data));
        if first_seen.contains_key(&new_data) { // this is a repeat message
            if first_seen.get(&new_data).unwrap().elapsed().as_millis() > 60000 { // it's been long enough so let's resend this
                first_seen.insert(new_data.clone(), time::Instant::now());
                hci::set_advertising_data(&new_data).unwrap();
            }
        } else { // completely new message so it should be set immediately
            first_seen.insert(new_data.clone(), time::Instant::now());
            hci::set_advertising_data(&new_data).unwrap();
        }
    }
}

The rest of the code is much better documented and is in the repository arpan-dhatt/forestflock.

Conclusion

If I were to do this project again, I would have changed several things. For one, the way I was encoding the floating point values into the advertisement was very inefficient. I could have instead just been using something like f32.to_be_bytes(). Also, extending this to work with more than floating point values was very painful since the code I had written didn't lend itself to flexible design. In fact, I pretty recently learned about a communication protocol for embedded devices called Controller-Area Network (CAN). That would have been very useful in the implementation for this since it allows me to specify encoding/decoding schemas that gave a lot of flexibility on the amount of space each value would take. More importantly, it even had C code generation which I could have leveraged to simplify the encoding and decoding.


home · posts · about · github