Xiaomi Mi Smart is a digital scale with BLE interface. This allows it to broadcast your weight to any device like your smart phone and incorporate the measurements to an application like Samsung Health.
Two years ago I was trying to read BLE messages from an ESP32, but Arduino BLE libraries didn’t work very well at that moment and all I got were some headaches. I wasn’t even able to match the Weight Measurement remote characteristic, so once the ESP32 connected to the scale it wasn’t able to implement the callback functions.
Today I’ve tried again, and I made it work!!!
Calculate the UUIDs
I have used the BLE_Client example (under ESP32 dev board samples). In order to connect and read values from any device you need two unique identifiers (16 bit UUID alias):
- the service: Weight Scale (uuid alias 181D)
- the characteristic: Weight Measurement (uuid alias 2A9D)
Now, with this two short uuids, we will calculate the long (128 bits) uuid by using the next formula:
128bit_base_uuid = 16bit_uuid_alias * 2^96 + Bluetooth_Base_UUID
where the Bluetooth_Base_UUID is 1000800000805F9B34FB (00000000-0000-1000-8000-00805F9B34FB)
In our case we will be using the string version of the 128bit uuid and that means all we have to do to calculate the long uuid for the service and the characteristic is concatenating the uuids (aliases and Bluetooth Base one) in the following way:
- Service UUID: 0000181D-0000-1000-8000-00805F9B34FB
- Characteristic UUID: 00002A9D-0000-1000-8000-00805F9B34FB
Finding our weight scale
Bring your Xiaomi scale next to your ESP32 MCU and load the next sketch using Arduino IDE:
/** * An ESP32 BLE client to find Xiaomi Mi Smart weight scale * Author Pangodream * Date 2020.05.31 */ #include "BLEDevice.h" //Base UUIDs //Weight Scale service static BLEUUID srvUUID("0000181d-0000-1000-8000-00805f9b34fb"); /** * Callback class for each advertised device during scan */ class deviceCB: public BLEAdvertisedDeviceCallbacks { //Called on each advertised device void onResult(BLEAdvertisedDevice advertisedDevice) { // We have found a device, let us now see if it contains the service we are looking for. if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(srvUUID)) { if(advertisedDevice.getName() != "MI_SCALE"){ Serial.print("Weight scale (no MI_SCALE) device found with address "); Serial.print(advertisedDevice.getAddress().toString().c_str()); Serial.print(" and name "); Serial.println(advertisedDevice.getName().c_str()); } else { Serial.print("Xiaomi Mi Smart weight scale found with address "); Serial.println(advertisedDevice.getAddress().toString().c_str()); BLEDevice::getScan()->stop(); Serial.println("End of scan"); } } else { Serial.print("Non weight scale device found with address "); Serial.println(advertisedDevice.getAddress().toString().c_str()); } } }; void setup() { Serial.begin(115200); Serial.println("Devices scan"); BLEDevice::init(""); BLEScan* pBLEScan = BLEDevice::getScan(); pBLEScan->setAdvertisedDeviceCallbacks(new deviceCB()); pBLEScan->setInterval(1349); pBLEScan->setWindow(449); //Set active scan pBLEScan->setActiveScan(true); //Scan during 5 seconds pBLEScan->start(5, false); } void loop() { delay(1000); }
The sketch starts an active scan to find all the BLE servers (devices) that are advertising themselves at that moment.
The scan results are shown at the Serial Monitor (115200 bauds) and if a Xiaomi Mi Smart scale is found (and its name is MI_SCALE) something like the next should appear in your monitor:
If your weight scale was present in the scan and you can read the message “Xiaomi Mi Smart weight scale found with address xx:xx:xx:xx:xx:xx” then you can continue with our next goal, which is to retrieve data from weight measurement.
Retrieving data
When the ESP32 is scanning for devices, it only reads the service beacons sent by each BLE server to advertise itself, but when we use the scale and it broadcasts the calculated weight, it doesn’t send the same type of message (it is not a service message, but a characteristic one).
Note that we haven’t used yet the characteristic UUID we calculated before and we’ve only used the service one.
Let’s load a new sketch into the ESP32 MCU to retrieve some data from the characteristic:
/** * An ESP32 BLE client to retrieve data from the Weight Measurement characteristic * of a Xiaomi Mi Smart weight scale * Author Pangodream * Date 2020.05.31 */ #include "BLEDevice.h" //Base UUIDs //Weight Scale service static BLEUUID srvUUID("0000181d-0000-1000-8000-00805f9b34fb"); //Weight Measurement characteristic static BLEUUID chrUUID("00002a9d-0000-1000-8000-00805f9b34fb"); static BLEAdvertisedDevice* scale; static BLERemoteCharacteristic* remoteChr; static boolean doConnect = false; static boolean connected = false; static int year = 0; /** * Callback function for characteristic notify / indication */ static void chrCB(BLERemoteCharacteristic* remoteChr, uint8_t* pData, size_t length, bool isNotify) { //Console debugging Serial.print("Received data. Length = "); Serial.print(length); Serial.print(". - Data bytes: "); for(int i =0; i< length; i++){ Serial.print(pData[i]); Serial.print(" "); } Serial.println(" "); //End of console debugging //Parsing the received data and calculate weight boolean temporary = true; int rcvdYear = pData[3]; //If we received a year for the first time, store it in the year variable //The first year we receive indicates a temporary measurement if(year == 0){ year = rcvdYear; }else{ //If year has been previously defined and the year we have received is //greater than it, then the measurement is not temporary, is the final one if(rcvdYear > year){ temporary = false; } } double weight = 0; weight = (pData[1] + pData[2] * 256) * 0.005; Serial.print("Weight: "); Serial.print(weight); Serial.print(" Kg - "); if(temporary){ Serial.println(" (Provisional)"); }else{ Serial.println(" (Definitive)"); } } /** * Callback class for each advertised device during scan */ class deviceCB: public BLEAdvertisedDeviceCallbacks { //Called on each advertised device void onResult(BLEAdvertisedDevice advertisedDevice) { // We have found a device, let us now see if it contains the service we are looking for. if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(srvUUID)) { if(advertisedDevice.getName() != "MI_SCALE"){ Serial.print("."); } else { Serial.println(" Found!"); BLEDevice::getScan()->stop(); Serial.println("Stopping scan and connecting to scale"); scale = new BLEAdvertisedDevice(advertisedDevice); doConnect = true; } } else { Serial.print("."); } } }; /** * Callback class for device events */ class ClientCB : public BLEClientCallbacks { void onConnect(BLEClient* pclient) { } void onDisconnect(BLEClient* pclient) { Serial.println("Disconnected. Reconnecting..."); connected = false; } }; bool connectToScale() { Serial.println("Stablishing communications with scale:"); BLEClient* pClient = BLEDevice::createClient(); Serial.println(" BLE client created"); pClient->setClientCallbacks(new ClientCB()); // Connect to the remove BLE Server. pClient->connect(scale); Serial.println(" Connected to scale"); // Obtain a reference to the service we are after in the remote BLE server. BLERemoteService* pRemoteService = pClient->getService(srvUUID); if (pRemoteService == nullptr) { Serial.println(" Error: Failed to find service"); pClient->disconnect(); return false; } Serial.println(" Service found"); remoteChr = pRemoteService->getCharacteristic(chrUUID); if (remoteChr == nullptr) { Serial.print(" Failed to find characteristic"); pClient->disconnect(); return false; } Serial.println(" Characteristic found"); Serial.println(" Setting callback for notify / indicate"); remoteChr->registerForNotify(chrCB); return true; } void setup() { Serial.begin(115200); Serial.println("Searching for MI_SCALE device"); BLEDevice::init(""); BLEScan* pBLEScan = BLEDevice::getScan(); pBLEScan->setAdvertisedDeviceCallbacks(new deviceCB()); pBLEScan->setInterval(1349); pBLEScan->setWindow(449); //Set active scan pBLEScan->setActiveScan(true); //Scan during 5 seconds pBLEScan->start(5, false); } void loop() { if(doConnect && !connected){ connected = connectToScale(); } delay(1000); }
Reboot the MCU and take a look to the serial monitor. If everything goes right, something similar to this should be shown:
The sketch performs these tasks:
- Scan devices to find the Xiomi scale
- Connects to the scale
- Search for the Weight Scale service
- Instantiates the service
- Search for the remote characteristic
- Instantiates the remote characteristic
- Sets the callBack function for the characteristic
In this way, when the scale begins to measure weight, our callback function will be invoked receiving the data.
At this point, the ESP32 is waiting to receive something in the callBack function. Put some weight over the scale and you should see the provisional and definitive measurements at the serial monitor.
The callback function always receive 10 bytes of information. These bytes can be parsed to get the weight and timestamp values in the following way:
The way we can differentiate between the provisional and the definitive measurements is looking at the year in the timestamp. While measurements are provisional, the scale sends a fake year and not the current one. So, all we hve to do is taking note of the value for the year in the first data packet we receive and then compare to the rest of the years we receive.
When the received year is greater than the first year we received that is the definitive measurement and we can get the calculated weight.
Why BLE_client sketch doesn’t work
If yoy try the sketch located at File / Examples / ESP32 BLE Arduino / BLE Client it won’t work and will not receive any chararteristic message even when you change the UUIDs.
The reason is this piece of code
if(pRemoteCharacteristic->canNotify()) pRemoteCharacteristic->registerForNotify(notifyCallback);
To set the callback function for the remote characteristic, first it checks the characteristic can be notifiable (method canNotify()) and our characteristic is not.
If you study the BLE connection using any app for that purpose (I use nRF Connect from Nordic Semiconductor) you will notice that the characteristic we are using is not notifiable but ‘indicate’ type:
So, if you comment out the condition to avoid checking if the characteristic is notifiable or not, and set the callback function anyway, you will received calls in the callback function with the parameter isNotify set to false.