The MP730026 DMMMP730026 DMM supports BLE communication. However, I quickly learned that the software offered by Multicomp Pro has significant limitations. So I set out to find a cross-platform solution, which leads me to Python. In the MP730026 DMM BLE Tutorial, I covered how to install the Bleak Python module, find the meter's MAC address, and receive update messages from the DMM. In this post, I explain how the bytes being received are getting turned into a string value.
If you just want to know how to make make the MP730026's BLE function work, check this tutorial first.
The Raw Packet
On platforms that support the bluez toolset, you can run gattool to see a raw update packet from the MP730026 BLE DMM. The packet is the six bytes of the value shown below.
sudo gatttool -b A5:B3:C2:22:14:D2 -I [A5:B3:C2:22:14:D2][LE]> connect Notification handle = 0x001b value: 23 f0 04 00 e6 0c
The multimeter replicates everything on its LCD with those 6 bytes. For this example, the meter is in DC Volts displaying 3.302. If you cannot see how 0x23, 0xF0, etc turn into that value, do not worry. The first time I saw it, I could not either. Multicomp Pro does not offer any documentation for these BLE messages. After some reverse engineering, or more correctly, trial and error, I figured out what almost every byte and nibble of that sequence means.
Attached to this post is all of the code needed to communicate with the DMM. The following sections provide detailed explanations of the code.
Inside mp730026_decode_byearray.py
First, here are what each of the bytes from the raw packet contains.
0x23 0xf0 0x04 0x00 0xe6 0x0c [Byte0, Byte1, Byte2, Byte3, Byte4, Byte5]
Those individual bytes break down to this information.
Byte 0 and 1 | Mode and Range |
Byte 2 and 3 | Relative and Hold Indicators |
Byte 4 and 5 | Display Value |
Bleak provides a Python data type called a bytearray. Python has some functions which make dealing with this array straightforward. Using struct's unpack, the script converts that byte array into multiple tuples which are much easier to work with for the rest of the decoding process.
unpacked = struct.unpack('>HHBB', data)
The >HHBB format tells Python that the 6 bytes from Bleak are arranged as big-endian, two 16-bit integers, and two 8-bit integers. The cool thing about the unpack stuff is that now all of the information we need is in an easy-to-manipulate array.
We created functions to decode each of the lines above. Since those functions call the unpacked array "data", the sections below reference "data" instead of "unpacked." (If you look through the code, that will make more sense.) Let's breakdown what the MP730026's messages tell us.
data[0]: Mode and Range
It turns out the mode and range are not as straight forward as I would like. Or, I have not figured out the pattern. So I went the brute force method and decoded (almost) every state. There are still a few that I cannot decode correctly because I cannot catch the screen fast enough to know what the mode is supposed to be. That said, mp730026_value_table.py contains a dictionary of everything we decoded by hand. It gets used by a function named decode_mode_and_range that decodes the bytes and returns an array with the three values.
values[0x90F0] = ['dcc', 'uA', 4] # DC xxxx uA
The mode has two strings: a description and a unit. For the description, there is a dictionary with long-form strings that fully explain the mode. The range determines the position of the decimal point. There is a bug in the current code when dealing with Overload conditions. The decimal place when showing "OL" on the LCD does not match the decode. For example, if the screen shows "OL." the decoder may display "O.L".
data[1]: Relative and Hold Indicators
I am not sure why two bytes are used for these since each function only has 1 bit in the byte. In all of data[1] there are only two bits that matter, each indicates if the meter is in relative or hold mode. The function is called decode_hold_and_rel().
When the multimeter is in "hold mode," it stops sending BLE messages. That behavior gives some insight into how the meter works. It only sends BLE messages when something on the LCD changes.
data[2] and data[3] Display Value
When I first started working with the BLE messages, I struggled with how to decode the display. It seems like the value is little-endian but at a byte level. And then I had trouble getting Python to handle these values as signed numbers. And then I had to worry about the decimal point. Eventually, I decided to stop treating the display value as a number and instead convert it into a string for the polarity and decimal placement. (In retrospect, I should have realized this approach made sense sooner, because it is how the actual display works.)
So the first to happen is shifting the MSB in data[3] to the left and combining it with LSB in data[2]using a numPy int16. This creates a signed 16-bit value.
If the meter is displaying overload (OL) then this value is 0xFFFF. From here we determine if the number is negative by looking at the 8th bit of the MSB (data[3]) so that the correct polarity is shown. The last step is to use the decimal position from the mode and range decode that happened earlier and add the decimal in the correct position.
Example Decode
Here is an example of what the decoded strings look like:
pi@benchpi:~/ble-dmm $ python mp730026_ble_subscribe.py
Reading [DC Voltage]: -001.7 mV
Reading [DC Voltage]: -000.9 mV
Reading [DC Voltage]: 000.0 mV
When decode fails
Almost all of the range, modes, and conditions decode correctly. At the time of writing, when cycling through modes with the selection knob, the decoder occasionally fails. When it does fail, the decoder displays the raw mode and range code.
Reading [DC Voltage]: 000.0 mV Reading [0x20f1]: 0000 ?
Currently, mp730026_value_table.py decodes all of the corner cases I could create. So if you run into one that does not decode properly, I look forward to your pull request on GitHub.
Conclusion
Attached to these posts is the code that works today. There is also a GitHub repository, dmm-ble-mp730026. Any changes made to the code after this post goes live can be found there. (Plus you can provide pull requests if you make changes!) I need to give a big Thank You to tisboyo, who provided the mp730026_value_table.py module. His changes were a massive improvement over my original implementation.
I do want to say again that I am happy with the MP730026 Bluetooth DMMMP730026 Bluetooth DMM. My next steps are to work on a graphical front-end to this code. Follow this blog to know when I make progress on that part of the project.
For now, it is time for me to (finally) get back to making videos on my electronics workbench.