So, I just bought a brand spanking new blood pressure monitor. My wife is a nurse and we have been talking about getting one for some time. After browsing the market, we settled on a Beurer BM 65. It is a very nice piece of kit and it comes with a USB plug for PC connectivity. Exciting, right? Unfortunately, the software is Windows-only. Bummer..

Beurer BM 65

Beurer BM 65

Well, then it’s obviously my duty to reverse-engineer it. Let’s get started then! A quick Google search tells me that others have had success with the Beurer PM70 (a heart rate monitor) and success with the Beurer BG64 (a diagnostic scale). They seem to use different protocols, though.

Reverse engineering the protocol

When the BM 65 is plugged in, it enumerates as 067B:2303, which is a Prolific Technology PL-2303 USB-to-serial controller. Interesting.. The problem is therefore reduced to guessing the serial protocol it uses.

Output from dmesg

Output from dmesg

On my Linux box it gets mapped to /dev/ttyUSB0 with no issues, as this chip is supported in the kernel. But how to communicate with it? We need to sniff the protocol… Beurer provides a free Windows-only tool called Health Manager for communicating with the device, as well as a subset of their other products. Luckily, it’s possible to eavesdrop on serial ports in Windows, and my gaming rig runs Windows 7. There is a SysInternals tool for this called Portmon, but it seems to work very poorly on Win7 x64. Next, I tried a tool called API Monitor v2. As the communication with the device is through a fake serial port, we should be able to sniff the relevant Windows API calls.

Lo and behold! It works! It seems that SetCommState in Kernel32.dll is used to configure the COM port (4800 baud 8N1).

4800baud

The SetCommState call where the serial connection is set up to 4800 baud 8N1.

Let’s then see if we can deduce the actual communication.. After some digging around I successfully limited the captured API calls to just the file I/O stuff in Kernel32.dll. The Health Manager tool tries all available COM ports until it gets a correct response. After that, we just have to follow the yellow brick road of WriteFile and ReadFile API calls. It only writes 1 or 2 bytes per call (depending on the command) and all reads are single byte reads.

Serial data transfer

Serial data transfer

This API Monitor tool is a bit tedious for this, so I tried Serial Port Monitor by Eltima Software instead. It is a shareware program with a 14 day trial, but that’s enough for this purpose. A serial dump of a sequence of 3 measurements looks something like this:

Captured serial communication

Captured serial communication

In table form, transferring a set of three measurements goes like this:

Sent to deviceReceived from deviceMy interpretation
0xAAPing?
0x55Pong!
0xA4Get description
"Andon Blood Pressure Meter KD001"Device description
0xA2How many measurements?
0x033 measurements!
0xA3 0x01Get measurement 1
0xAC 0x66 0x37 0x4E 0x0A 0x11 0x16 0x2A 0x0DMeasurement 1!
0xA3 0x02Get measurement 2
0xAC 0x62 0x35 0x5F 0x0A 0x0E 0x12 0x0C 0x0DMeasurement 2!
0xA3 0x03Get measurement 3
0xAC 0x64 0x3D 0x55 0x0A 0x0C 0x0E 0x09 0x0DMeasurement 3!

After the last byte, the connection is terminated. Apparently, this device is made by a company called Andon. And it seems that it only transmits data about a single user at a time. Let’s have a look at a single measurement:

Byte valueMy interpretationDescription
0xAC0b10101100Status bits? Magic number?
0x66102 + 25 = 127 mmHgSystolic blood pressure (offset by 25)
0x3755 + 25 = 80 mmHgDiastolic blood pressure (offset by 25)
0x4E78 BPMPulse
0x0A10 = OctoberMonth
0x1117Day of month
0x1622Hours
0x2A42Minutes
0x0D13 = 2013Year

And Bob’s your uncle! We have now successfully reverse engineered the protocol. Well.. Almost.. I haven’t got a clue about the first byte of each measurement. It might be a magic number, but it’s probably some status bits. Besides blood pressure and pulse, the device also registers cardiac arrhythmia. If this information is recorded and if a measurement is always 9 bytes, it would have to be stored in these bits.

I mentioned that the device seemed to be made by Andon. After a bit of digging, I found some evidence for this. The document from dabl Educational Trust says:

“Andon is an OEM manufacturer for the BM 65. Despite the different designs, the BM 65 is functionally the same as the Andon KD-5915 with added dual user, averaging and uploading features but without the voiced results.”

It seems that some of the other Andon devices also support USB. I wonder if the protocol is the same as for the BM 65?

Implementing a Python module for the Beurer BM 65

Let’s make a rudimentary data downloader in Python using our newly acquired knowledge about the protocol. I’m using Python 2.7 on a reasonably new Linux Mint installation. The code is reasonably basic, omitting any kind of error handling.

For those who don’t want to copy code from here, you can also pull a copy from GitHub.

Disclaimer:
The code is free to use, but do so at your own risk.
If you brick your device, it’s not my problem.

[code language=”python”]
import sys, serial

class Measurement(object):
def __init__(self, data):
self.header = data[0]
self.systolic = data[1] + 25
self.diastolic = data[2] + 25
self.pulse = data[3]
self.month = data[4]
self.day = data[5]
self.hours = data[6]
self.minutes = data[7]
self.year = data[8] + 2000
self.time = “{0}-{1:02}-{2:02} {3:02}:{4:02}”.format(self.year,
self.month,
self.day,
self.hours,
self.minutes)

def getBytes(self):
return [self.header,
self.systolic – 25,
self.diastolic – 25,
self.pulse,
self.month,
self.day,
self.hours,
self.minutes,
self.year – 2000]

def __repr__(self):
hexBytes = [‘0x{0:02X}’.format(byte) for byte in self.getBytes()]
return “Measurement([{0}])”.format(‘, ‘.join(hexBytes))

def __str__(self):
return “\n”.join([“Header byte : 0x{0:02X}”,
“Time : {1}”,
“Systolic pressure : {2} mmHg”,
“Diastolic pressure : {3} mmHg”,
“Pulse : {4} BPM”]).format(self.header,
self.time,
self.systolic,
self.diastolic,
self.pulse)

class BeurerBM65(object):
def __init__(self, port):
self.port = port

def sendBytes(self, connection, byteList, responseLength = 1):
connection.write(”.join([chr(byte) for byte in byteList]))
response = connection.read(responseLength)
return [ord(char) for char in response]

def bytesToString(self, bytes):
return “”.join([chr(byte) for byte in bytes])

def getMeasurements(self):
ser = serial.Serial(
port = self.port,
baudrate = 4800,
parity = serial.PARITY_NONE,
stopbits = serial.STOPBITS_ONE,
bytesize = serial.EIGHTBITS,
timeout = 1)

pong = self.sendBytes(ser, [0xAA])
print “Sent ping. Expected 0x55, got {0}”.format(hex(pong[0]))

description = self.bytesToString(self.sendBytes(ser, [0xA4], 32))
print “Requested device description. Got ‘{0}'”.format(description)

measurementCount = self.sendBytes(ser, [0xA2])[0]
print “Found {0} measurement(s)…”.format(measurementCount)

for idx in range(measurementCount):
yield Measurement(self.sendBytes(ser, [0xA3, idx + 1], 9))

print “Done. Closing connection…”
ser.close()

if __name__ == “__main__”:
conn = BeurerBM65(sys.argv[1])
for idx, measurement in enumerate(conn.getMeasurements()):
print “”
print “MEASUREMENT {0}”.format(idx + 1)
print measurement
[/code]


29 Comments

Bloody Mary · 2013-11-30 at 18:15

Great job!
How can i use the python script? What should be sys.argv[1] when I run the python script?
Thanks in advance!

    atbrask · 2013-11-30 at 21:09

    Thanks!
    The script takes a single command line argument as sys.argv[1]. This argument is the name of the serial port used by the blood pressure monitor. The value to put in depends on which operating system you use and exactly how the port is mapped into the system. On my Linux box its /dev/ttyUSB0, on my Mac it’s /dev/tty.usbserial, and on my Windows box its COM4.

Bloody Mary · 2013-12-01 at 20:31

Ahh okey, should have known that…Anyways, it works like a charm, just it has to be run with sudo:
sudo python scriptfile /dev/ttyUSB0 . Thanks again!
Do you plan to extend the python script with functionalities like
-keep all data in a (csv) file
-plotting functionality with matplotlib (e.g. the Beurer win software can do nice pie charts)?
This could be the linux alternative for the Beurer Health Manager then…

    atbrask · 2013-12-01 at 21:24

    Nice to hear that you got it working! 🙂
    No, I currently don’t have any further plans for it. In my spare time I do all sorts of small projects and this one was a very interesting Sunday afternoon (except for the additional time needed for blogging). My contribution will be limited to this code, which is open source and totally free to use.
    It should be relatively easy to implement your suggestions, but in order to be a true alternative to Beurer’s Windows software, a lot of other devices would need to be supported too. And that’s a fairly big task, but not impossibly big.

hacker · 2014-01-20 at 01:22

Worked well for me on a BM58 + Mac.
Thanks very much for the clear documentation and clean code!

    atbrask · 2014-01-20 at 10:44

    Cool! I never tested this code with other devices than my own BM65. The various Beurer devices are actually very different from one another. But from reading though some disassembled Health Manager code I figured this code would probably work with the BM58 too. Nice to hear that’s the case. 🙂

Ilynikh Denis · 2015-12-02 at 15:23

Hello i have a Beurer BM58, and it work like USB HID and in my Raspberry Pi it not create a ttyUSB for comunicate. Can you help me ?

    atbrask · 2015-12-02 at 20:58

    I’m afraid I probably can’t help you.. An earlier comment here above suggested that the BM58 worked fine with this script. It’s a bit surprising if it appears as a HID device. What does dmesg and lsusb say when you plug it in?

M · 2016-01-12 at 00:42

Hi, I too have an Bm58 device. I’ve bought it with the intent of implementing the data transfer on a microcontroller, but as mentioned before it does seems to be different. The USB interface is not done via a USB-to-serial chip (PL-2303), but using an Sonix SN8F2271B 8-bit microcontroller, which appears as HID-compliant on my PC. I’ve poked the data lines between the main IC (which is a potted chip-on-board, so no markings) and the Sonix chip, and it appears to use 5 pins – SDI, SDO, SCK (for bidirectional synchronous serial communication), INT1 (hardware intrerrupt pin) and 3.3V power out (from the Sonix). I’ve haven’t got the chance to decode the protocol, but it appears to be very similar to USART (not UART), which a clock period of 4us (200kbps?). Can you help me by reverse-enginneering the software, please? Thank you!

    atbrask · 2016-01-12 at 08:15

    I guess the BM58 isn’t the same as the BM65 after all. Bummer.. Let me guess.. It enumerates with VID 0C45 and PID 7406, right? As I don’t have a BM58 myself, I can’t really help reverse engineering the protocol. But I can give you a couple of ideas how to proceed, I think.. As I see it, you have two options. An easy one and a hard one.

    The hard way would be to use an application like API Monitor v2 (the one I mentioned in the blog entry above). Using that would enable you to observe exactly what happens when you connect the device.

    The easy way is to use a tool like ILSpy to decompile the Beurer HealthManager application and have a look inside. It’s written in pure .NET code with no obfuscation whatsoever, so the resulting decompied C# code is actually very nice and readable with all symbol names intact. In that case you may want to have a look in the Beurer.DeviceCommunicator.SupportLib.dll assembly. Inside, the Beurer.DeviceCommunicator.SupportLib.Devices.BloodPressure namespace contains a class called BM58 that handles the communication with this particular device.

      M · 2016-01-12 at 20:18

      Thank you, yes it enumerates with those IDs. I’ve tried the API Monitor software, but it didn’t result any conclusive data. I will have a look with the decompilation.

      M · 2016-01-13 at 20:49

      Thank you again, the disassembly helped a lot regarding how each measurement’s pulse, pressure rates, date and time are transmitted. The interface itself between the Sonix and the main IC is via a SPI-like protocol, MSB-first, 250KHz , falling-edge clock, 8-bit per word, active-high data lines. Unfortunately, it requires hard reverse-engineering, i.e. using an oscilloscope/logic analyzer. I’ll return with the details when I’m finished.

        atbrask · 2016-01-13 at 22:09

        No problem! 🙂
        It sounds like a great reverse-engineering project you have there. So you want to bypass the Sonix chip and collect the data yourself using a MCU? Wireless data transfer?

          M · 2016-01-24 at 17:46

          Hi, Sorry for the long delayed response. I was busy with the protocol reverse-engineering, finally managing in implementing the reading of a measurement on a FPGA board (in VHDL). I’ve documented the protocol, but I’m not sure if it is easily readable for others except myself, as it is not professionally noted. But if anyone is interested, I’m more than happy to share.

          atbrask · 2016-01-24 at 20:38

          Very cool! I have never worked with FPGAs before, but would like to try it out some day when the need arises. If you some day decide to do a write-up about your findings, I would very much like a link.

          M · 2016-01-24 at 21:50

          Once I’ll make an nice, visual representation of the protocol (as it has multiple bits, bytes, words, with different time delays between them, different requests and responses) with clear explanations, I’ll add a link, but currently I’m working on a project which incorporates this blood pressure meter.

        Kiran Ghanta · 2017-06-01 at 09:45

        Hi M, We also bought Beurer BC 58 (similar to BM58), which uses the same VID and PID mentioned above and uses Sonix to transfer the data. We also stuck to get the data into our java application. As the device is not broadcasting the data using the com ports, we are looking for the other options. I like the idea of decoding the “Beurer HealthManager” by atbrask. But it is much appreciated, if you could share your experience. As you have done all the ground work for the same.

        Thanks in Advance.

Muling · 2016-02-14 at 18:26

M, I’d be very interested in your protocol docs for the BM58. I’ve got that exact meter and I’d love read out the data, then pipe it into Graphite.

Even if you think that it’s unreadable for everybody but you I’m sure it’ll help me a lot :).

Muling · 2016-02-16 at 04:44

I’ve had a look at the Beurer BM 58 USB protocol and wrote an ugly Python script to read out the data.

Apparently there are two versions of the BM 58 out there, one registering as USB/Serial converter and one as HID (0c45:7406). I wrote POC code for the latter one. It’s on Github: https://github.com/muling-tt/beurer_bm58

    Kiran Ghanta · 2017-06-05 at 07:36

    Hi Muling, We have the device BC 58, for this we used your py code to fetch the data (https://github.com/muling-tt/beurer_bm58). But unfortunately, we are getting error : ” usb.core.USBError: [Errno 13] Access denied (insufficient permissions) “, which translates to missing drivers in our Raspberry Pi OS. You did your research on the device BM 58, will the same work with BC 58 (may be with minor tweaks here and there)? If your code works with this, then what are the places that we need to tweak on?

    hafor · 2021-01-31 at 11:26

    Hi Muling,
    thanks a lot for your beurer_bm58 script, really cool!
    And also thanks to yelmd for fixing the termination bug.
    I could figure out how to handle the measurement values for user1 and user2.
    It’s an offset of 128 to the HOUR value indicating user2.
    And a similar logic applies to the YEAR value, where an offset of 128 indicates cardiac dysrhythmia.
    That means you could modify the get_records code by adding 2 more data fields (USER, DYSRH) like this:

    def get_records(self, count):
    “””read the records from the device

    :param count int: number of records to read
    “””
    getrecord_b = [0xa3]
    records = {}
    for i in range(count):
    self._send_to_device(getrecord_b + [i + 1])

    # Put everything in a nested dict
    dataset = self._read_from_device(8)
    records[i] = {}
    records[i][‘systole’] = dataset[0] + 25
    records[i][‘diastole’] = dataset[1] + 25
    records[i][‘pulse’] = dataset[2]
    records[i][‘month’] = dataset[3]
    records[i][‘day’] = dataset[4]
    if records[i][‘day’] < 128:
    records[i]['user'] = 'U1'
    else:
    records[i]['user'] = 'U2'
    records[i]['day'] -= 128 # U2: day has offset 128
    records[i]['hour'] = dataset[5]
    records[i]['minute'] = dataset[6]
    records[i]['year'] = dataset[7]+2000
    if records[i]['year'] < 2128:
    records[i]['dysrh'] = 0
    else:
    records[i]['dysrh'] = 1
    records[i]['year'] -= 128 # dysrhythmia: year has offset 128
    i += 1

    return records

      hafor · 2021-01-31 at 11:36

      Sorry, there was a mistake in my comment, the offset for user2 is not in hour, but in DAY value.
      It’s an offset of 128 to the DAY value indicating user2.

John · 2017-03-23 at 18:59

Hi,

Do you perhaps know where I can get a schematic diagram of the BM 15 ?

Or else, do you know whetherit is possible to build a pc interface for it ?

Alex · 2017-12-24 at 23:42

I have a MediSana® CardioCompact device, which also replies with “Andon Blood Pressure Meter KD001” but follows a slightly different protocol – it uses two bytes for the record count reply and for the index when retrieving individual records. I have patched it for my device and published the code at https://github.com/alech/BM65DataDownloader.

Philip · 2019-08-22 at 00:09

I found this Python script, and wondered if it might also work for the BM55. My Google searches got a hit on this software package: https://github.com/rprobinson/MediPi , which includes explicit support for the BM55 and some other Beurer products. Inspection of ./MediPiPatient/MediPi/src/main/java/org/medipi/devices/drivers/BeurerBM55.java and ./MediPiPatient/MediPi/src/main/java/org/medipi/devices/drivers/service/BM55USBService.java showed that the protocol is the same as the BM65, and also revealed an additional code, 0xF7 for terminating the communication. Hope this is of help to others.

jelmd · 2020-08-08 at 00:43

For BM55 https://github.com/muling-tt/beurer_bm58.git works as well – thanks a lot! To get both collections, I applied the PR#2 from the same repo. Finally, to get a proper termination, I modified the bytes sequence a little bit:

— a/bm58.py
+++ b/bm58.py
@@ -124,7 +124,7 @@ class BeurerBM58(object):
def terminate(self):
“””disconnect from the device”””
LOGGER.debug(‘terminating connection’)
– term_bytes = [0xf7, 0xf6]
+ term_bytes = [0xf7]
for i in term_bytes:
self._send_to_device([i])
self.dev.reset()

Have fun!

era · 2020-10-08 at 06:00

thanks for sharing. infromation its good

LazyT · 2021-08-05 at 16:37

You can download the protocol descriptions for all devices directly from Beurer ( https://connect.beurer.com/developer) after registration.

I’ve added 4 usb-serial devices to my Universal Blood Pressure Manager project: https://codeberg.org/LazyT/ubpm

Maybe someone will test or contact me for usb-hid… 😉

LazyT · 2021-11-05 at 16:02

Also added the usb-hid variants now to my Universal Blood Pressure Manager project: https://codeberg.org/LazyT/ubpm

Maybe someone will test and give feedback, thanks…

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *