BT785: Difference between revisions

From RevSpace
Jump to navigation Jump to search
Line 117: Line 117:
|}
|}


Packet payload is typically one item per bluetooth message, prefixed with 00f00000008000
Measurement data is reported using code 0x8006, typically one item per bluetooth message.
The DP is prefixed with 00f00000008000


== Captures ==
== Captures ==

Revision as of 05:54, 16 October 2025

Project BT785
Connecting a BT785 EC meter to raspberry pi
Status In progress
Contact bertrik
Last Update 2025-10-16

Introduction

This page is about reverse engineering the BT-785, an inexpensive Tuya-compatible water electrical conductivity (EC) meter. It measures EC and temperature, and has a display and communicates its reading over Bluetooth Low-Energy (BLE) to the Tuya app on a phone. The goal is to understand the communication protocol so we can communicate with it from the raspberry pi in the Karaburan project.

Interesting info:

Analysis

BLE characteristics in NRF-connect

On the lowest level, it uses bluetooth low-level with the GATT profile on the right:

Most important seem two characteristics:

  • 00000002-0000-1001-8001-00805f9b07d0: module to phone, notifies of data from the EC meter
  • 00000001-0000-1001-8001-00805f9b07d0: phone to module, we write data to the EC meter

Advertisement manufacturer data:

  • item 2000 = 00000100449b6a4ea8ea8d8ecddb794250608546

Advertisement service data:

  • item 0000fd50-0000-1000-8000-00805f9b34fb=49000008255c3e3b440632b9

The device connects to the Tuya app on a phone. Data transfer is encrypted.

While the tuya app is running, we can see notifications coming in, for example:

00 31 46 05 36 46 B1 F5 36 55 B2 56 34 E1 B3 C9
13 33 0A D9 17 E8 FE 4F 5C 9A 70 CE DB 26 A6 17
22 CC DF 65 7D F7 0D 01 02 0A 33 45 B0 C1 A5 A8
42 F5 C1 B2

Composition of the data:

  • 0x31 is the length of the data
  • 0x46 probably some sequence number
  • 0x05 the "security flag" indicating the type of encryption
  • 36 46 B1 F5 36 55 B2 56 34 E1 B3 C9 13 33 0A D9: 16 bytes of initial vector
  • rest: encrypted data, padded to 16 bytes unit

Another example:

00-31-40-05-8A-8B-20-8F-95-46-06-C0-CF-B2-9C-98
45-F8-E5-0F-8F-43-86-79-CA-1A-11-4B-30-F5-CB-1C
3F-93-3C-41-CE-27-DD-23-82-8A-2A-95-AE-71-4E-BF
DC-50-F2-32

Sequence

What we see in the communication between phone and device:

  • phone discovers bluetooth characteristics, apparently one of them contains the uuid of the device
  • phone ups the MTU, to make communication easier
  • phone sends device info request:
    • send command 0x000 (request device info), encrypted using 6 bytes of the local key
    • receive response, this contains the srand value used in further communication
  • phone sends command 'pair' (0x0001) with payload of 0x2E bytes:
    • 16-byte uuid
    • 6 bytes of local key
    • 16 bytes device id
    • 8 bytes unknown (1 big endian)
    • + 2 bytes crc
    • + 8 bytes padding
  • device sends 'pair' response, contains one byte (2 means OK)
  • device indicates that it would like its time to be set (0x8011)
  • phone requests device status (0x0003), this takes no arguments
  • phone sends the time command (0x8011), payload is unix time (milliseconds) in ascii, with \0 terminator and 0xC8 trailer byte (?)
  • device responds with device status (0x0003) on earlier device status request, with data 0x00
  • device indicates code 0x8006 (?) with data 0x00f0000000800008020004000000c2
  • device indicates code 0x000b (?) with data 0x000100000000000000
  • phone sends command 0x0007 ('DP report'?)with data 0x0000
  • device sends response 0x0007 with data 0x0000000000000000000000000200a5390

Protocol

The Tuye BLE protocol consists of several layers, from bottom to top:

  • Framing layer, splits large packets up into smaller fragments, to work around MTU limitations:
    • packets are segmented into 20-byte units, each prefixed by a header that contains packet number, length byte and encrypted payload
  • Encrypted payload layer, encrypts data using AES
    • data is padded to 16-byte units, common method seems that the number of padding bytes is also the pad value
    • prefixed with a 1-byte key type (04 or 05), a 16-byte initial vector, data is encrypted with AES in CBC mode
  • Command/response packets, within each packet:
    • 4-byte sequence number of the sender side, each side keeps its own sequence number
    • 4-byte sequence number of the related packet (in a response, the sequence number of the command packet), or 0 if not applicable
    • 2-byte command code
    • 2-byte length L
    • L-byte payload, this is command specific data
    • 2-byte CRC
  • Data point data, in the case of a measurement report, these have the following encoding:
    • 1-byte DP id, e.g. 8 = temperature for BT785
    • 1-byte DP type (0 = raw, 1 = boolean, 2 = int, 3 = string, 4 = enum)
    • 2-byte length L (big-endian)
    • L-byte data, e.g. 4 bytes for an int, variable size for a string, etc, appears to be big-endian

BT875 data points

BT785 DPs
Id Type Meaning
08 02 (INT) Temperature (0.1 deg C)
6A 02 (INT) Temperature (0.1 deg F)
6F 02 (INT) TDS (ppm)
74 02 (INT) EC (?)
79 02 (INT) Salt (ppm)
7E 02 (INT) Proportion (0.001 SG)

Measurement data is reported using code 0x8006, typically one item per bluetooth message. The DP is prefixed with 00f00000008000

Captures

The protocol has the following steps:

  • Discovery of the BLE characteristics, this is not Tuya specific
  • Setting the MTU
  • Initial pairing: Initially, when the EC meter is paired with the Tuya app (long press?), a key is generated. This key is stored inside the meter and stored in the app. You can register with the Tuya developer console, link your device to the console, and fetch this key. Example key: '<z)o}Ezmuw01.TxQ'. Let's call this the local key'.
  • Session start When the EC meter is powered on and communication starts, the session is started by the host sending a 'device info request'. The response contains a session key. This exchange is secured using part of the 'local key' (only first 6 bytes!?). This is used in another 'pairing' session (volatile, per power-on) to start the actual session.
  • Measurement, the device sends measurements as encrypted notifications.

The encrypted frames typically do no fit in basic GATT frames. A GATT notification can typically hold only about 23 bytes of data.

An encrypted frame contains of a 5-byte header, a 16-byte IV, so basically no room left for the actual data.

Pairing

I plan to not investigate this, just let the device and the app figure this out, then fetch the local key from the API

Session start

BLE MTU negotiation
BLE MTU negotiation

The very first step is probably upping the MTU, since by default GATT only allows very small blocks of data to be exchanged. I see in the app-device communication that the MTU is increased before session start:

Device info request

The phone sends a device info request: Encrypted, this looks like

002120
04 security flag
55304552fd7810cdb72cced8831dc514 iv
8b77f23fb840607c58d0beb91c463a98 encrypted data

Fields:

  • header: 0x00 = first packet, 0x21 = length, protocol_version = 0x20 = 2 << 4
  • security flag 0x04
  • iv: 55304552fd7810cdb72cced8831dc514
  • encrypted: 8b77f23fb840607c58d0beb91c463a98

Encoding/encryption:

  • key = md5(first 6 bytes of local key)
  • iv = first 16 bytes after header
  • encode using AES in CBC mode with key and iv

This decrypts to:

00000001 00000000 0000 0002 00f3 784e

Which decodes to:

  • [0-3]: cmd sn = 1
  • [4-7]: ack sn = 0
  • [8-9]: code = 0 = device info request?
  • [A-B]: length = 2
  • [C-D]: data = 0x00f3 = ???
  • [E-F]: crc = 0x784e, CRC-16/MODBUS with poly 0x8005

Device info response

The device responds with:

0081014004
65db80c3df1cde3aebe8bf8ceee18b39
f2be5ee93a59f09e0c65af2963440497
394317d56482a56e2ed32d05c727585b
980b73b3266e782acb9a7705233c5807
444cacccef3daa3e1eec12c271069a42
9ed448d8312d33212f0a886abaa5cecb
ed8ca761eea1daaef97cd5fc9033fb09
5f08353bf0ae6a7f231ee94df6100ce8

This can be decoded as follows:

  • header says this is a block of 0x81 bytes, sequency number 0x40, with security flag 0x04
  • determine iv = first 16 bytes after the header = 65db80c3df1cde3aebe8bf8ceee18b39
  • calculate key = md5(first 6 bytes of local key) = md5("<z)o}E") = 64ba62ad750c312d08e10aa343769fbf
  • decode remaining bytes using AES in CBC mode with the iv and key just determined

The decrypted response is:

000: 0000000100000001 0000006001020402
010: 1001c4984a2a590f 0100c14161b10880
020: 5adba92a221eb39d 6e2f000000000000
030: 0000000000000000 0000000102000100
040: 0001006266366138 346d66796b756262
050: 696a330000000000 0000000000000000
060: 0000000000e76d69 4f23dc00755d0202
Offset (hex) Size (bytes) Hex (example) Field name Meaning / comment
0x00 4 00 00 00 01 sn Sequence number = 1
0x04 4 00 00 00 01 sn_ack Acknowledged SN = 1
0x08 2 00 00 code 0x0000 = device_info response
0x0A 2 00 60 length 0x0060 = 96 bytes payload
0x0C 2 01 02 protocol_version major.minor = 1.2
0x0E 1 04 security_flag 4 = AES using login-key MD5
0x0F 1 02 capability_flags Device capability / flags (vendor)
0x12 6 c4 98 4a 2a 59 0f srand **6-byte session random** (used to derive session key)
0x18 2 01 00 hardware version major/minor
0x1A 16 c1 41 61 b1 08 80 5a db a9 2a 22 1e b3 9d 6e 2f auth/device key material ???
0x43 16 62 66 36 61 38 34 6d 66 79 6b 75 62 62 69 6a 33 device_id (ASCII) "bf6a84mfykubbij3" followed by padding
0x65 6 e7 6d 69 4f 23 dc reversed_mac Reversed BLE MAC bytes

Measurement reports

Measurements are sent on the notification characteristic.

start continuous report

The command that happens just before the device starts emitting continuous reports, is:

002120052c0d1f8c65d5a59044e251998885ac91a997ec85431faba6798e01b41f614791

decrypts to:

00000006 00000000 000a 0002 0000 488c

Interpreted:

  • [0-3]: cmd sn = 6
  • [4-7]: ack sn = 0
  • [8-9]: code = 0xA = ???
  • [A-B]: length = 2
  • [C-D]: data = 0x0000 = ???
  • [E-F]: crc = 0x488c

example 1

Example data, looks like response to 0x000a command.

00314705 09a25c5146e55def691d26bbf80cddc6 7375b68f9c835408df05a2c2301f5a7f 671f46c3be8bb16e3500cda30b1571b7

decrypts to

00000008 00000000 000a 0003 000000 8ea1 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
sn       ack_sn   cmd  len  data   crc  padding

Looks like this is padded with 0x0f somehow? -> looks like the padding is actually the number of bytes that was used for padding!

example 2

Data:

00314805
1d390b3225723a56c67c88ec8b69e788
b34aa66917000fe4cc5d92f01d308800
d160ae0e59175d83f88cfcad83deb8c4

decrypts to:

00000009 00000000 8006 000f 00f000000080006f02000400000147 e773 030303
sn       ack_sn   cmd  len  data                           crc  padding

example 3

Data:

00314905
4e021b00b6c21f8d8aa5b6381a10b7b1
c2e6d55ecefd28a9e16ffcb90bf314ff
b6c4c82a77172c72a4504e868ee6af86

decrypts to:

0000000a 00000000 8006 000f 00f00000008000740200040000028e 0117 030303
sn       ack_sn   cmd  len  data                           crc  padding

Note: the 0x28e value in the message is exactly twice the 0x147 value from the previous message

example 4

Data:

00314a056dcbf8b57350511a39bb655811f599cba815e559e3ac62f34d3b4179552c4168a464c3d421948d7d42976c8232ce6224

decrypts to:

0000000b 00000000 8006 000f 00f00000008000790200040000017d b8cb 030303
sn       ack_sn   cmd  len  data                           crc  padding

0x17d = 381 decimal

example 5

Data:

00314b05c6844918ad30e473232126c96d7426e6a47cf44ed9c73c4940dfb413f946ad2da297af5984e69474220fd2f7b0d48c01

decrypts to:

0000000c 00000000 8006 000f 00f000000080007e020004000003e7 7f9d 030303
sn       ack_sn   cmd  len  data                           crc  padding

0x3e7=999

example 6

Data:

00314c05c16ecb996e967ac9cf384249adfc014a26e1d812b802039fab7508a66f1fb90d1f734c69a1a0087f751761d0be185a55

decrypts to:

0000000d 00000000 8006 000f 00f000000080006a020004000002ae 7300 030303
sn       ack_sn   cmd  len  data                           crc  padding

example 7

Data (slighty longer packet):

00414e059c4d598e66979d48bc5996235e9fd25d641daac049a5da487cd662f8c0d8f8e3ae5b6a4c114e61dbfe223c27a599fd64a2ad17a32ec1167de6bafb55c5a38484

decrypts to:

0000000f 00000000 8006 0014 00f000000080006500(0009)010001010101000000 ac54 0e0e0e0e0e0e0e0e0e0e0e0e0e0e
sn       ack_sn   cmd  len  data               len?                    crc  padding

example 8

This is data from about 1 minute later:

00314005fcc95342ff6448a91273bff896f1570f77245f5c37810e6089941f80da847b5aeb0af98e914fbacb5da3cbab0dd37a10

decrypts to:

0000003e 00000000 8006 000f 00f000000080008302000400000000 8cfe 030303
sn       ack_sn   cmd  len  data                           crc  padding

example 9

003140054084767d4ed4119d21232112335e7b45744ef1205f5cd5976c88eacc78d0ccb246040fac0c622169d8dd0c04d08a10da

decrypts to:

00000028 00000000 8006 000f 00f00000008000(08)(02)(0004)000000c8 e3b0 030303
sn       ack_sn   cmd  len  data           id type  len  200 dec crc  padding

This contains a DP of id 8, type 2 (int), length 4, with value 200 (20.0 degrees celcius)