A naïve attempt at decoding the pHin BLE GAP Interface¶
After inspecting the PCB, I decided to start looking at the BLE data. My intention was to simply look at the high-level behavior, such as the protocols implemented in GAP (Advertise) and GATT (Connected mode), as well as to get a general feel of how the data is structured. I ended up spending way too much time trying to decode the data without having sufficient knowledge of what is actually happening on the PCB itself. This is why I call this approach “naïve”.
Overview¶
The pHin sends BLE Advertisements periodically.
Here is an example capture using the CySmart Desktop application from Cypress, coupled with a CY5677 USB Dongle.

We can see that:
The BLE Advertisement contains the following data:
Flags (mandatory)
List of 16-bit services
Manufacturer Specific Data
The device also implements Scan Responses (which is not mandatory). A Scan response is sent by the BLE Peripheral directly after receiving a Scan request from the BLE Central (Scanner), allowing to add some complementary information in case a device wants to advertise more than the 31-byte limit of the Advertisement Packet. In the scan response, we have one optional field:
Shortened Local Name
The two middle bytes of the MAC Address are used to form the 4 digits in the Shortened Local Name. This is what allows the BLE Scanner to display the user-friendly name
pHinA087
. Note that using only two byte from the Bluetooth Address makes it non-unique.
Another easy way to decode BLE Advertisements is to use a BLE sniffer based on an nRF52840 development board with a specific firmware, coupled with the Wireshark network analyser.
To be complete, here is a screenshot of Wireshark decoding both the BLE Advertisement and the BLE Scan Response.
- BLE Advertise
- BLE Scan Response
We will look at the contents of each field in more detail, but first, we will discuss how the pHin monitor changes behavior after a 90s init period.
Advertisement Type during init period¶
For the first 90s after boot, the pHin is connectable. After the initial 90s period, the pHin becomes scannable only.

It’s not possible to connect to the pHin with a simple BLE debugging tool or application, as it seems we are missing a key. Here is the full log in CySmart Desktop when attempting to connect:
[13:24:23:923] : 'Resolve and Set Peer Device BD Address' request sent
[13:24:23:923] : BD Address Type: RANDOM_ADDRESS
[13:24:23:923] : BD Address: 79:69:87:A0:52:DC:00:00
[13:24:23:927] : 'Command Status' event received
[13:24:23:927] : Status: BLE_STATUS_OK
[13:24:23:927] : 'Resolve and Set Peer Device BD Address Response' event received
[13:24:23:927] : Status: 0x01
[13:24:23:927] : 'Command Complete' event received
[13:24:23:927] : Status: BLE_STATUS_OK
[13:24:23:927] : 'Establish Connection' request sent
[13:24:23:927] : BD Address Type: RANDOM_ADDRESS
[13:24:23:927] : BD Address: 79:69:87:A0:52:DC:00:00
[13:24:23:930] : 'Command Status' event received
[13:24:23:930] : Status: BLE_STATUS_OK
[13:24:23:945] : 'Establish Connection Response' event received
[13:24:24:792] : 'Enhanced connection complete' event received
[13:24:24:792] : Status: 0x00
[13:24:24:792] : Role: 0x00
[13:24:24:792] : BD Address Type: RANDOM_ADDRESS
[13:24:24:792] : BD Address: 79:69:87:A0:52:DC:00:00
[13:24:24:792] : Local resolvable address: 00:00:00:00:00:00:00:00
[13:24:24:792] : Peer resolvable address: 00:00:00:00:00:00:00:00
[13:24:24:792] : Connection Interval: 8.75 ms
[13:24:24:792] : Slave Latency: 0
[13:24:24:792] : Supervision Timeout: 100 ms
[13:24:24:794] : 'Command Complete' event received
[13:24:24:794] : Status: BLE_STATUS_OK
[13:24:24:794] : 'Connection Terminated Notification' event received
[13:24:24:794] : Reason: CONNECTION_FAILED_TO_BE_ESTABLISHED
[13:24:24:794] : 'Get local device security keys' request sent
[13:24:24:864] : 'Command Status' event received
[13:24:24:864] : Status: BLE_STATUS_OK
[13:24:24:864] : 'Get local device security keys response' event received
[13:24:24:864] : Key flags: INITIATOR_ENCRYPTION_INFORMATION, INITIATOR_IDENTITY_INFORMATION, INITIATOR_SIGNATURE_KEY, RESPONDER_ENCRYPTION_INFORMATION, RESPONDER_IDENTITY_INFORMATION, RESPONDER_SIGNATURE_KEY
[13:24:24:864] : Long Term Key (LTK): [D6:93:E8:A4:23:55:48:99:1D:77:61:E6:63:2B:10:8E]
[13:24:24:864] : Encrypted Diversifier (EDIV) and Random Number: [99:1F:26:1E:F6:09:97:2E:AD:7E]
[13:24:24:864] : Identity Resolving Key (IRK): [0A:2D:F4:65:E3:BD:7B:49:1E:B4:C0:95:95:13:46:73]
[13:24:24:864] : Identification Address: [0x00A050504320, PUBLIC_ADDRESS]
[13:24:24:864] : Connection Signature Resolving Key (CSRK): [90:D5:06:95:92:ED:91:D7:A8:9E:2C:DC:4A:93:5B:F9]
[13:24:24:864] : 'Command Complete' event received
[13:24:24:864] : Status: BLE_STATUS_OK
While we attempt to connect to the pHin using the CY5677 Dongle and CySmart for Windows (and fail in the attempt), we observe the following exchanges via a BLE sniffer connected to Wireshark:

We also fail to connect from CySmart for iOS, but we get a slightly different trace:

To connect, the initiator must probably provide some keys, which we don’t have. Without a lot more work at reversing (which I have no plans of doing) it will not be possible to access all the features provided by pHin in the GATT (connected) mode. Since this is only accessible after the user resets the device using a magnet, we can safely assume that it’s still possible to use the pHin in its normal pool monitoring mode without the GATT connection.
A BLE connection is used to send data to the monitor, or to support large data exchanges. In this case, since it appears to be a maintenance operation assisted by a magnet, the connection could be used:
To send a command to the pHin to set it in deep sleep, for winter (saving energy)
To update the firmware, in case Over-the-Air firmware updates are supported
Advertisement Type after init period¶
The Device announces itself as “Scannable Undirected”. This means that it sends periodic BLE Advertisements, which can be received and decoded by any BLE Central. Is it “Scannable”, not “Connectable”, meaning this it is limited to the GAP Protocol, but does not support BLE Connections (GATT). In other words, after the first 90 seconds, pHin only broadcasts some data periodically and does not rely on the BLE interface for more complicated interactions with the Application (no two-way communication, no high-throughput data transfers, just periodic Advertisement).
Advertisement Data Field: Flags¶
The Flags field announces the general capabilities of the BLE Peripheral. It is contained in the Advertisement packet in all states (during the initial 90s period and beyond).
It has a constant value, as follows:
Data Length = 0x02
Data Type = 0x01
(= FLAGS)
Data Payload = 0x06
We can let CySmart decode the payload for us:

There is nothing really useful for us here. This is a very common configuration for Bluetooth Low Energy peripherals. Note that the “General Discoverable Mode” indicates that the ongoing BLE Advertisements will continue indefinately (as opposed to the “Limited Discoverable Mode”, which implies that BLE Advertisements will eventually timeout, and must be re-trigerred somehow by the user like a button / accelerometer / etc).
Advertisement Data Field: List of 16-bit Service Class UUIDs¶
This option provide the UUIDs of the services implemented by the device. It is contained in the Advertisement packet in all states (during the initial 90s period and beyond).
Data Length =
0x03
Data Type =
0x03
= (List of 16-bit Service Class UUIDs)Data Payload =
0xFE63
The payload 0xFE63
can be looked up in the 16-bit UUID Numbers Document and corresponds to the Service UUID named Connected Yard, Inc.
. This is a company affiliated with the pHin brand:

This can serve 2 purposes:
It can serve to announce that this device (pHin monitor) implements a registered 16-bit UUID Service designed by Connected Yard Inc, instead of using a random, unregistered 128-bit UUID Service.
It can also help BLE scanners to filter the received advertisements. If this option is not present, or if it contains a different value, the scanner can drop the packet.
Note
The Manufacturer Specific Data option can also serve as a second layer of filtering, as it contains the Manufacturer ID field, which should be reserved for Connected Yard Inc. devices only.
Advertisement Data Field: Manufacturer Specific Data: Initial Measurements¶
The Manufacturer Specific Data option allows the device to advertise custom data in any format that is suitable for the application. It is contained in the Advertisement packet in all states (during the initial 90s period and beyond). It’s this field that the pHin updates periodically after each new measurement is taken.
After looking at the Manufacturer Specific data for about 1 hour, with the probe in tap water first, and then in a pH7 buffer solution, I had not yet made much progress in decoding the packet. Here is the deduced format, as of now.
The general format is:
LEN TYPE MAN_ID PAYLOAD
17 ff e702 822b67e1df950903a6db5c00796987a0525ce257
This translates into:
Data Length =
0x17
Data Type =
0xFF
(= Manufacturer Specific Data)Data Payload = e702 822b67e1df950903a6db5c00796987a0525ce257
The first two bytes of the Manufacturer Specific Data are reserved to represent the Manufacturer ID. This is a 16-bit unsigned integer encoded in Little-Endian, so the Manufacturer ID is 0x02E7. It comes to no surprise that this manufacturer ID, as reported by the official list of BLE Manufacturer, is Connected Yard, Inc.
.
The remaining 20 bytes is the application-related data, containing the pool measurements, among other things. The fields I was able to identify during this first hour of naïve, wishful thinking are:
A Sequence Number
The Probe Unique Identifier
We can see taht the sequence number increases from 2 to 5 during the first 90s period, then it is refreshed every 10 minutes thereafter. This means it’s likely that multiple pool measurements are taken during the boot period, and then once every 10 minutes during the normal function of the monitor.
Data update period, Sequence Number & Unique Identifier¶
To start understandîng what each of these 20 bytes represents, we start by letting the BLE Advertisement run with the probe dipped in tap water. We observe the following payloads at the reported time since the start of the wireshark capture. The time indicated here is the time of the first packet received just after the payload was updated:
Byte# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
Time
175s e7 02 82 2b 67 e3 df 95 09 05 a6 db 5c 00 79 69 87 a0 52 5c e2 57
769s e7 02 82 2b 67 e3 cd c5 09 06 a3 db 5c 00 79 69 87 a0 52 5c e2 57
1376s e7 02 82 5b 67 e3 c7 c5 09 07 a6 db 5c 00 79 69 87 a0 52 5c e2 57
1968s e7 02 82 5b 67 e3 c4 f5 09 08 a6 db 5c 00 79 69 87 a0 52 5c e2 57
From this sequence, we can already infer the following:
The data seems to be updated every 10 minutes.
Byte 9 increases by 1 in each update. It is very probably a sequence number.
Bytes 14 to 19 contain the Bluetooth Address, with 1 bit difference:
The Bluetooth Address is
dc:52:a0:87:69:79
Bytes 14 to 19 contain
5c:52:a0:87:69:79
. Only the very first bit changes (0xd=1101b, 0x5=0101b)
We can see that the full Bluetooth Address is contained in the Manufacturer Specific Data, allowing the application to unambiguously identify the pHin (which would have been impossible to do based solely on the Shortened Local Name of pHinA087
).
Measuring pH7, pH4 and pH10 buffers¶
Let’s try to find where the pH is located in the Advertisements by using some pH buffer solutions.

I started by filling the probe’s storage cap with a pH7 buffer solution and recorded the following Manufacturer Specific Data payloads in the next advertisements:
Byte# 0 1 2 3 4 5 6 7 8 9(SEQ) 10 11 12 13 14-19(ADDR) 20 21
Time
2576s e7 02 82 ab 65 e3 eb 75 05 09 ac db 5c 00 796987a0525c e2 57
3168s e7 02 82 5b 67 e3 d0 c5 06 0a ac db 5c 00 796987a0525c e2 57
3792s e7 02 82 eb 67 e3 cd 25 07 0b ac db 5c 00 796987a0525c e2 57
4402s e7 02 82 bb 67 e3 cd 25 07 0c ac db 5c 00 796987a0525c e2 57
5009s e7 02 82 bb 67 e3 d0 55 07 0d ac db 5c 00 796987a0525c e2 57
5570s e7 02 82 db 65 e3 cd 55 07 0e ac db 5c 00 796987a0525c e2 57
I rinced the probe and storage cap thouroughly, then repeated the measurement with a pH4 buffer solution. Here are the first few measurements in that mode:
Byte# 0 1 2 3 4 5 6 7 8 9(SEQ) 10 11 12 13 14-19(ADDR) 20 21
Time
6176s e7 02 82 1b 68 e3 75 a6 0b 0f a6 db 5c 00 796987a0525c e2 57
6769s e7 02 82 6b 69 e3 7b d6 0b 10 a6 db 5c 00 796987a0525c e2 57
7378s e7 02 82 2b 6a e3 75 36 0c 11 a6 db 5c 00 796987a0525c e2 57
Finally, I repeated the experiment again with a pH10 buffer:
Byte# 0 1 2 3 4 5 6 7 8 9(SEQ) 10 11 12 13 14-19(ADDR) 20 21
Time
7969s e7 02 82 0b 69 e3 5b 95 06 12 a6 db 5c 00 796987a0525c e2 57
8577s e7 02 82 9b 69 e3 40 c5 06 13 a6 db 5c 00 796987a0525c e2 57
9171s e7 02 82 5b 6a e3 3a 35 06 14 ac db 5c 00 796987a0525c e2 57
9777s e7 02 82 8b 6a e3 34 05 06 15 a6 db 5c 00 796987a0525c e2 57
Previous deductions still hold:
Byte 9 is an incremental sequence number
Bytes 14 to 19 are the unique ID of the pHin monitor
Bytes 10-13 and 20-21 have not changed since the start
Between the tap water and the differet pH buffers, among the bytes that regularly change, it’s bytes 7 and 8 which have changed the most.
Example of these bytes in tap water: f5 09
Example of these bytes in a pH 7 buffer: 25 07
Example of these bytes in a pH 4 buffer: a6 0b
My attempts to interpret these bytes did not lead to any significant break-through. If thes are 16-bit values of the pH in millivolts, it looks like little-endian given the slow-varying nature of the second byte (i.e. the second byte looks like the MSB). This would make, in decimal:
Tap Water: 2549
pH7 Buffer: 1829
pH4 Buffer: 2982
pH10 Buffer: 1685
Whatever the represention used here, I can say that this interpretation is incorrect because I’m expecting a linear relationship between pH and the pH electrode’s millivolt value.
At this point I realized that my setup is really impractical for reverse-engineering the measurement.
- Lack of ground truth
It would be a lot simpler to know the result of the decoding for a given unknown payload. The pHin application certainly decodes this and probably displays the pH, ORP and temperature somewhere, but I’m not a pHin customer and installing the application today is impossible because they do not accept new registrations. The person who gave me the pHins might have been able to transfer his user/login, but I didn’t want to bother him again.
- Lack of sensor control
It’s also impractical to stimulate the probe itself. I have several buffer solution from Atlas Scientific (pH4, pH7, pH10, ORP225mV), which is better than nothing, but it would be so much better if I could sweep the entire range of possible sensor values (you see where I’m going with this, right?).
For the first hurdle, since I’ve recently learned about the General Galactic pHinshed app, I could simply install that and continue reversing with it if I’m still stuck by the time they publish it (it turns out I didn’t need to, as we will see later).
For the second obstacle, we need to dive into how the electronics work to see how we can replace the probe by an emulator. That’s right, let’s ditch the sensor and emulate it!