Wednesday, 14 July 2021

Get data from a tilt hydrometer on an Arduino

A tilt hydrometer is a submersible thermometer and hydrometer for monitoring fermentation. It’s a Bluetooth LE device that reports data via the iBeacon protocol. Specifically, it broadcasts a major and minor value, which represent the temperature and gravity of the liquid its submersed in, via the manufacturer data. These values can be extracted from the manufacturer data and converted into decimal values.

The manufacturer data it broadcasts is a hex string, such as the following:


This data breaks down as follows:

Apple beacon Type Length Device UUID Major (temperature) Minor (gravity) ???
4c 00 02 15 a4 95 bb 30 c5 b1 4b 44 b5 12 13 70 f0 2d 74 de 00 47 04 2b 00

The Device UUID is shared between devices of a specific colour (it’s unimportant what this means, other than to know that the Device UUID above identifies it as a black tilt).

The temperature is in degrees Fahrenheit, and is a 16 bit unsigned integer in big endian format. The gravity is a 16 bit unsigned integer in big endian format, that must be divided by 1000 to obtain the correct value. For the message above, the temperature is 71F (21.67 C) and the gravity is 1.067.

Normally I monitor the data the tilt returns via an app on my phone, but for various reasons I decided to build my own device to display the data.

My microcontroller of choice is Arduino. They are fantastic, cheap, reliable, and powerful devices and I’ve used them in several projects. The Arduino IDE is a bit basic, but I’m still constantly surprised that Arduino’s “just work”, particularly when I can’t say that about many other technology stacks.

My Arduino of choice, when I require connectivity, is the Arduino Nano 33 IoT. It’s perfect for small devices that require WiFi and Bluetooth functionality. The process for using the Arduino to get data from the tilt hydrometer is as follows:

  • Start bluetooth.
  • Scan for your tilt hydrometer. Once the tilt is found, stop scanning.
  • Retrieve the manufacturer data from the tilt, and extract the temperature and gravity.
  • Stop bluetooth.

Note that because an iBeacon device broadcasts its data, there’s no need to connect to the device.

The ArduinoBLE library can be used to manage bluetooth connectivity. If you’re interested in how the library works, see its GitHub repo. The problem with the library is that it doesn’t support reading manufacturer data. However, an unmerged PR has added that functionality. This version of the library must be installed to your Arduino IDE’s library directory for the sketch below to work (clone the ArduinoBLE repo, switch to the branch containing the PR, zip the repo contents, place the zip in the library directory for the Arduino IDE).

My sketch that gets the manufacturer data from the tilt, and decodes/extracts the temperature and gravity is shown below:

#include <ArduinoBLE.h>
char tiltMacAddress[] = "your tilt MAC address goes here e.g. aa:bb:cc:dd:ee:ff";
float temperature = 0;
float gravity = 0;
void setup()
    while (!Serial); 
    BLE.setEventHandler(BLEDiscovered, OnBLEDiscovered);
void loop()
    if (temperature == 0 || gravity == 0)
void StartBluetooth()
    if (!BLE.begin())
        Serial.println("Can't start BLE");
    Serial.println("Started bluetooth");
    Serial.println("Started scan");
void OnBLEDiscovered(BLEDevice peripheral)
    if (peripheral.hasManufacturerData())
        Serial.println("Tilt detected");
void StopScan()
    Serial.println("Stopped scan");
void StopBluetooth()
    Serial.println("Stopped bluetooth");
void GetTiltData(BLEDevice peripheral)
    Serial.println("Address: " + peripheral.address());
    Serial.println("RSSI: " + String(peripheral.rssi()));
    String tiltData = peripheral.manufacturerData();
    Serial.println("Data: " + tiltData);
    String tempHex = tiltData.substring(40, 44);
    String gravityHex = tiltData.substring(44, 48);
    char tempHexChar[5];
    tempHex.toCharArray(tempHexChar, 5);
    float tempF = strtol(tempHexChar, NULL, 16);
    temperature = (tempF - 32) * .5556;
    Serial.println("Temp: " + String(temperature));
    char gravChar[5]; 
    gravityHex.toCharArray(gravChar, 5);
    long grav = strtol(gravChar, NULL, 16);
    gravity = grav / 1000.0f;
    Serial.println("Gravity: " + String(gravity, 3));

After starting bluetooth on the Arduino, a tilt hydrometer can be scanned for using BLE.scanForAddress(tiltMacAddress). I’d recommend using the scanForAddress method over the scan method, as it will take less time to find your tilt. Obviously, this requires knowing the MAC address of your tilt, which can easily be obtained by free bluetooth scanners on most platforms.

Once the tilt with the specified MAC address is discovered, the BLEDiscovered event fires, which in turn executes the OnBLEDiscovered handler. This handler retrieves the manufacturer data with the BLEDevice.manufacturerData method, which returns a hex string. The major and minor values can then be extracted from the hex string, and converted into decimal-based temperature and gravity values.

Outputting the data to the serial port shows that it’s been successfully retrieved:

Having successfully retrieved the tilt data, it’s then possible to output it to Nixie tubes. This involved some refactoring of the above code to make it more robust to the appearance and disappearance of the tilt, and to only retrieve data every hour, rather than continuously (the data only changes very slowly).

So here it is - a one of a kind Nixie device (using 4x IN12A and 2x IN12B tubes) that displays the time (set on startup from an NTP server, and resynchronised every 24 hours):

When a tilt hydrometer is detected, it also displays the fermentation data. Temperature:


Provided that a tilt hydrometer is detected, the device displays the time for a minute, followed by temperature for 30 seconds, and gravity for 30 seconds. If there’s no tilt detected, the time is displayed permanently. The device also includes programmable RGB leds, which act as backlights to each Nixie tube.