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
(supportschar *
,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
withbleSerial.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.
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
:
- You must obtain a HardwareBLESerial instance by calling
HardwareBLESerial::getInstance
. - You must initialize your HardwareBLESerial instance by calling
HardwareBLESerial::beginAndSetupBLE
, which is analogous toSerial.begin
. - You must call
HardwareBLESerial::poll
regularly in order to synchronize state with BLE hardware, unlikeSerial
which does not need polling by the user. - You can use line-oriented methods to read/peek/check input for entire lines at a time.
Serial
has something similar withSerial.readBytesUntil
, but it is harder to use and also blocks control flow.
More examples:
- Echo each received line back to the sender
- Bridge between BLE UART and hardware serial port
- Using CommandParser to make a BLE commandline-like interface
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.
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
.
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.
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
.
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.
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.
Returns the number of bytes available for reading (bytes that were already received and are waiting in the receive buffer).
Returns the next byte available for reading as an int
. If no bytes are available for reading, then returns -1 instead.
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.
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.
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.
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.
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.
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).
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).
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).
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).
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).
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
}
This library would not be possible without these excellent resources:
- Official Nordic Semiconductor page for their UART protocol. This had a lot of information about rationale and design considerations.
- Kevin Townsend's BLE UART tutorial. This was a much more digestible version of the Nordic page, making implementation a lot easier.
- BLE UART example from the Arduino-BLEPeripheral library. This was useful in debugging the resulting library, especially the idea to use a regularly-flushed transmission ring buffer.