Skip to content

srl-ethz/Arduino-HardwareBLESerial

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HardwareBLESerial

An Arduino library for Nordic Semiconductors' proprietary UART/Serial Port Emulation over BLE protocol, using ArduinoBLE.

Features:

  • Byte-oriented I/O: HardwareBLESerial::available, HardwareBLESerial::peek, HardwareBLESerial::read, HardwareBLESerial::write.
  • Line-oriented I/O: HardwareBLESerial::availableLines, HardwareBLESerial::peekLine, HardwareBLESerial::readLine.
  • Flexible printing: HardwareBLESerial::print, HardwareBLESerial::println (supports char *, char, int64_t, uint64_t, double).

Since the BLE standard doesn't include a standard for UART, there are multiple proprietary standards available. Nordic's protocol seems to be the most popular and seems to have the best software support. This library exposes a Serial-like interface, but without any of the blocking calls (e.g., Serial.readBytesUntil), and supports additional line-oriented features such as peekLine and readLine.

Generally speaking, BLE is not designed for UART-style communication, and this will consume more power than a design centered around individual BLE characteristics. However, you may find BLE UART useful for:

  • Wireless logging: Need to test your robot in the field, but cables getting in the way? View logs and diagnostics in realtime via apps such as Bluefruit LE! Just replace your existing Serial.println with bleSerial.println and you're 90% done.
  • Command-line interfaces: HardwareBLESerial works great with CommandParser, a commandline parser by yours truly. Check out Using CommandParser to make a BLE commandline-like interface in the examples section!
  • Transferring binary/text data: HardwareBLESerial papers over packet size limits and makes it easy to send large amounts of binary/text data back and forth between devices.

This library requires ArduinoBLE, and should work on all boards that ArduinoBLE works on, such as the Arduino Nano 33 BLE, Arduino Nano 33 IoT, or Arduino MKR WiFi 1010. Most of my testing was done on an Arduino Nano 33 BLE.

Quickstart

Search for "HardwareBLESerial" in the Arduino Library Manager, and install it. Now you can try a quick example sketch:

#include <HardwareBLESerial.h>

HardwareBLESerial &bleSerial = HardwareBLESerial::getInstance();

void setup() {
  if (!bleSerial.beginAndSetupBLE("Echo")) {
    Serial.begin(9600);
    while (true) {
      Serial.println("failed to initialize HardwareBLESerial!");
      delay(1000);
    }
  }
}

void loop() {
  // this must be called regularly to perform BLE updates
  bleSerial.poll();

  while (bleSerial.availableLines() > 0) {
    bleSerial.print("You said: ");
    char line[128]; bleSerial.readLine(line, 128);
    bleSerial.println(line);
  }
  delay(500);
}

Download Bluefruit LE Connect on your mobile phone, then connect to your Arduino.

This sketch demonstrates some differences between HardwareBLESerial and Serial:

  1. You must obtain a HardwareBLESerial instance by calling HardwareBLESerial::getInstance.
  2. You must initialize your HardwareBLESerial instance by calling HardwareBLESerial::beginAndSetupBLE, which is analogous to Serial.begin.
  3. You must call HardwareBLESerial::poll regularly in order to synchronize state with BLE hardware, unlike Serial which does not need polling by the user.
  4. You can use line-oriented methods to read/peek/check input for entire lines at a time. Serial has something similar with Serial.readBytesUntil, but it is harder to use and also blocks control flow.

More examples:

Apps for working with BLE UART

Usually you connect to BLE peripherals using a mobile phone or computer. In the BLE connection, the phone/computer is then the "BLE central device".

For BLE UART, you'll usually need a special app that supports interfacing over UART. For iOS devices, here's my review of various available options:

  • Bluefruit LE Connect: the best UART console I've tried. Scans quickly, connects quickly, and console is decently easy to use. However, it doesn't support setting up dedicated buttons that send common commands.
  • nRF Toolbox: another app with a UART console, but a bit harder to connect to a peripheral than Bluefruit LE Connect. Does support dedicated buttons for quick access to common commands, which is nice.
  • LightBlue Explorer: like BLE Hero, it also only supports managing BLE CHaracteristics, but it is the easiest to use option
  • nRF Connect: a good choice for managing BLE characteristics, the UI is a little bit unintuitive but it is quite a complete solution. However, it doesn't support UART that well, though in theory you can directly edit the RX/TX characteristics to interface with UART.

I also tried these but didn't find them as useful:

  • Blue - Bluetooth & developers: this does work for managing BLE characteristics, but doesn't have a UART console and overall has less features than nRF Connect, though it is easier to use. However, LightBlue Explorer has a better UI.
  • BLE Hero: like Blue, it also only supports managing BLE characteristics, but very buggy UI and quite sluggish - refresh often doesn't work. Would not recommend.
  • BLE Terminal HM-10: this actually only supports the Texas Instruments UART protocol, not the Nordic Semiconductor one. Does not work with this library!

NOTE: One problem with all of the above apps that offer a UART console is that they limit the length of the message you can write to 20 bytes, often silently truncating your message at 20 bytes. Usually, you would want ArduinoBLE to increase the MTU, to allow the board to receive messages up to 512 bytes in size. However, since ArduinoBLE doesn't allow increasing the MTU (even the internal ATTClass::setMaxMtu method doesn't seem to do anything), the other standard way to send longer messages is to send them in chunks with a short delay in between. However, the apps above do not seem to support this. As a workaround, I manually send long messages in chunks of 20 bytes within Bluefruit LE Connect.

Reference

HardwareBLESerial &bleSerial = HardwareBLESerial::getInstance()

The HardwareBLESerial class is a singleton, so there can be at most 1 instance of this class (because a device can only have 1 UART service).

The getInstance method returns a reference to this singular instance of HardwareBLESerial.

bool HardwareBLESerial::beginAndSetupBLE(const char *name)

This is exactly the same as HardwareBLESerial::begin, except it also initializes ArduinoBLE, sets the name of the device to name, and makes the Arduino visible to BLE central devices.

Returns true if initialization completed successfully, false otherwise.

Typically you would use this rather than HardwareBLESerial::begin, as it takes care of a small amount of ArduinoBLE setup for you - you won't need to think about ArduinoBLE at all! You would instead use HardwareBLESerial::begin if you need more flexibility, such as changing the ArduinoBLE initialization code.

void HardwareBLESerial::begin()

Registers the BLE UART service with ArduinoBLE, and sets it as the one service whose UUID is sent out with BLE advertising packets (this ensures that BLE central devices can see right off the bat that we support BLE UART, without needing to connect to us).

This requires ArduinoBLE to be initialized beforehand. Afterwards, you must also tell ArduinoBLE to start actually broadcasting, using BLE.advertise(). For a helper function that does everything ArduinoBLE-related for you, see HardwareBLESerial::beginAndSetupBLE.

void HardwareBLESerial::poll()

Performs internal BLE-related housekeeping tasks. Must be called regularly to ensure BLE state is kept up to date.

If not called often enough, we may miss some data packets being sent to us from the BLE central device - HardwareBLESerial::poll must be called more often than data packets are sent to us.

Exactly how often data packets are sent depends on the BLE central device. Usually for UART terminals such as Bluefruit LE Connect, each time you press "Send", it sends one data packet, and since you likely won't be sending messages more than twice a second, calling HardwareBLESerial::poll twice a second should be enough to ensure we don't miss anything.

void HardwareBLESerial::end()

Cleans up some of the resources used by HardwareBLESerial. ArduinoBLE doesn't supply enough functionality to fully clean up all of the resources we used, so this method is generally not that useful.

size_t HardwareBLESerial::available()

Returns the number of bytes available for reading (bytes that were already received and are waiting in the receive buffer).

int HardwareBLESerial::peek()

Returns the next byte available for reading as an int. If no bytes are available for reading, then returns -1 instead.

int HardwareBLESerial::read()

Returns the next byte available for reading as an int, and also consumes the byte from the receive buffer. If no bytes are available for reading, then returns -1 instead.

int HardwareBLESerial::write(uint8_t byte)

Writes byte to the transmit buffer, where it'll queue up until the buffer is flushed (usually within 100 milliseconds).

Returns the number of bytes that were sent to the BLE central device - that's 0 if there is no BLE central device connected, or 1 if there is.

void HardwareBLESerial::flush()

Manually flush the transmit buffer, allowing all data within it to be sent out to the connected BLE central device, if there is one.

This usually doesn't need to be called, since the transmit buffer should be automatically flushed by HardwareBLESerial::poll if it hasn't been flushed within the last 100 milliseconds, or automatically flushed when it's full.

size_t HardwareBLESerial::availableLines()

Returns the number of lines available for reading (lines that were already received and are waiting in the receive buffer).

A line is defined as a sequence of zero or more non-newline characters, followed by a newline character.

size_t HardwareBLESerial::peekLine(char *buffer, size_t bufferSize)

Copies the next line available for reading into buffer (or the first bufferSize - 1 characters of it, if the line doesn't fully fit). If no next line is available for reading, it simply null-terminates buffer so it contains an empty string. Afterwards, buffer is guaranteed to be null-terminated.

Returns the number of characters copied from the line, so between 0 and bufferSize - 1. If there was no next line, then consider that as 0 characters being copied.

size_t HardwareBLESerial::readLine(char *buffer, size_t bufferSize)

Copies the next line available for reading into buffer (or the first bufferSize - 1 characters of it, if the line doesn't fully fit), and also consumes that line from the receive buffer. If no next line is available for reading, it simply null-terminates buffer so it contains an empty string. Afterwards, buffer is guaranteed to be null-terminated.

Returns the number of characters copied from the line, so between 0 and bufferSize - 1. If there was no next line, then consider that as 0 characters being copied.

size_t HardwareBLESerial::print(const char *str), size_t HardwareBLESerial::println(const char *str)

Writes a null-terminated string str to the transmit buffer, where it'll queue up until the buffer is flushed (usually within 100 milliseconds).

Returns the number of bytes that were sent to the BLE central device (0 if there is no BLE central device connected).

size_t HardwareBLESerial::print(char value), size_t HardwareBLESerial::println(char value)

Writes a single char value to the transmit buffer, where it'll queue up until the buffer is flushed (usually within 100 milliseconds).

Returns the number of bytes that were sent to the BLE central device (0 if there is no BLE central device connected, 1 if there is a BLE central device connected).

size_t HardwareBLESerial::print(int64_t value), size_t HardwareBLESerial::println(int64_t value)

Writes a single signed 64-bit integer value to the transmit buffer, formatted as a decimal number, where it'll queue up until the buffer is flushed (usually within 100 milliseconds).

Returns the number of bytes that were sent to the BLE central device (0 if there is no BLE central device connected).

size_t HardwareBLESerial::print(uint64_t value), size_t HardwareBLESerial::println(uint64_t value)

Writes a single unsigned 64-bit integer value to the transmit buffer, formatted as a non-negative decimal number, where it'll queue up until the buffer is flushed (usually within 100 milliseconds).

Returns the number of bytes that were sent to the BLE central device (0 if there is no BLE central device connected).

size_t HardwareBLESerial::print(double value), size_t HardwareBLESerial::println(double value)

Writes a single double value to the transmit buffer, formatted as a decimal format floating point number (i.e., no exponents), where it'll queue up until the buffer is flushed (usually within 100 milliseconds).

Returns the number of bytes that were sent to the BLE central device (0 if there is no BLE central device connected).

if (HardwareBLESerial) { ... }

Instances of HardwareBLESerial can be used as boolean values - they are truthy when a BLE central device is connected, and falsy when no BLE central devices are connected. This works even without calling HardwareBLESerial::poll, so you can call this in a loop by itself without HardwareBLESerial::poll like in the code example below.

This is often used to wait for a device to connect:

HardwareBLESerial &bleSerial = HardwareBLESerial::getInstance();
while (!bleSerial) {
  delay(100); // on Mbed-based Arduino boards, delay() makes the board enter a low-power sleep mode
}

Resources

This library would not be possible without these excellent resources:

Packages

No packages published

Languages

  • C++ 100.0%