HomeAssistant integration of a re-branded model 'D9' Air quality meter (AHQI) designed by OCRKJ.
Although the documentation focuses on this product, the steps taken can be applied to other devices as well.
By design, the device is a standalone equipment, integrating multiple sensor measurements (temperature, humidity, TVOC, HCHO, PM2.5, PM10, CO2) that are displayed on the integrated LCD.
It includes several display modes (such as graphing) and a internal flash memory for manual saving/reading of measurement samples.
Hardware-wise, the device has no external data connection - the USB port is only for charging, and there's no internal wireless interface (Wi-Fi, or Bluetooth).
As such, no direct solution regarding third-party integration is readily available - the information contained in this repo is obtained via reverse-engineering efforts.
Since reverse-engineering software may pose a infringement on copyrights, no binary files or workspaces are included in this repo.
Instead, it is fully up to the responsibility of the user whether to reproduce the results.
By analyzing the PCB inside the device, several components can be identified (the manually cables soldered are part of the modifications covered in this repo):
- a GD32F103 MCU (clone of a STM32F103),
- several atmospheric sensors with no identifiable markings,
- a rechargeable battery, LCD screen connector, and miscellaneous circuits (power, clocking),
- A footprint for a wireless (WiFi?) module - due to no digital activity on all of the pins, it can be concluded that this feature is neither implemented in HW nor in SW.
- Although the USB socket has the data pins connected to the MCU, the device does not enumerate when connected to a host PC.
- One connector on the top-right, identifiable as a SWD debug interface (pinout from the left:
VCC
,SWCLK
,SWDIO
,GND
), may prove to be the only solution to integrating the device in a HA instance.
As expected, any attempt to read out the flash memory is blocked by means of a active readout protection (RDP) mechanism. Fortunately, the level set by the manufacturer is 1, so SWD connection is still possible.
Since the GD(STM)32F103 is a popular entry-level MCU, there's a number of known RDP vulnerabilities documented, for eg. {1} and {2}.
One paper published by Johannes Obermaier, Marc Schink, and Kosma Moczek covers the GD32F103 clone in particular. Several approaches are taken in extracting the flash memory contents, the fastest being a DMA-based transfer (chapter 6.5.) - blocks of the flash memory are copied to RAM via DMA requests, then saved on the host PC (RDP doesn't protect RAM readout).
While RDP prevents flash read operations, the running software also employs protection against debugging by polling the Debug Halting Control and Status Register
(DHCSR) register, in particular the C_DEBUGEN
bit that is usually set when a external debugger connects to the MCU - a value of 1 triggers a soft reset that hinders running the exploit.
Segger's J-Link EDU debugger and [J-Link Commander] (https://www.segger.com/products/debug-probes/j-link/tools/j-link-commander) CLI were used.
- After connecting the debugger to the board and enabling debug mode, the MCU will, as expected, reset continuously - clearing the
C_DEBUGEN
bit will prevent this - in J-Link Commander runWrite4 0xE000EDF0 0xA05F0000
, then hardware reset the device by holding the right-most button on the device. - Once the device no longer resets itself, the firmware can be easily extracted by following the steps in the paper.
- To ease generating the necessary commands, run the script
jlink_cmd_gen.py
- if necessary, modify theRAM_SIZE
andROM_SIZE
accordingly to the target MCU. - Copy-paste the contents of the resulting file
output/jlink_cmd_gen.txt
into the J-Link Commander window - a number of binary files shall be saved in the same folder. - Run the script
jlink_bin_merge.py
to merge all the binary files into a single filefull_rom_dump.bin
.
To disassemble the file, Ghidra will be used, as it is the best (free) solution currently available.
- Create a new project, and import the
full_rom_dump.bin
file as aARM v7 little endian
binary image, assigning the block nameROM
and startup address0x8000000
. - Open the imported file in Ghidra's CodeBrowser, then open the
Memory Map
window avaiable in theWindow
drop-down menu. - Define a new memory block named
RAM
, with start address0x20000000
and flagsread
,write
, andvolatile
checked. - Run a
Analyze all open...
from theAnalysis
drop-down menu. - Once disassembly is finished, click on the
RAM
block from the top-leftProgram Tree
view - it should contain a number of variable blocks with names starting withDAT_200
. - From the
File
drop-down menu, clickExport Program...
, select format asXML
, then check at minimum theData
checkbox in theOptions...
menu. Save the file asfull_rom_dump.xml
in theoutput
folder. - The resulting XML file contains the addresses and sizes of all the constants and variables detected by the decompiler.
- While the MCU is running, dump the current RAM contents - in J-Link Commander run
SaveBin XX\ram_dump_Y.bin, 20000000, BFFF
, whereXX
is the absolute path to theoutput
folder, andY
the current dump id (starting from 1). - Take note during dump operation of the current measurement values displayed on the D9's LCD screen.
- To ease up the identification process, take multiple RAM dumps, while incrementing the file name's ID, for eg.
ram_dump_1.bin
,ram_dump_2.bin
, etc. - Run the script
xml_addr_dump.py
to extract the values stored at known RAM addresses (based on the XML file previously generated) that are valid (i.e. the bits are not all 0 or all 1). - A table will be displayed with the addresses and sizes of valid values found in the RAM dump(s), together with displaying the values in unsigned/signed, decimal/hexadecimal, and float formats.
- If multiple RAM dump files are found, the displayed table will only include addresses that contain valid values in all the dumps, thus further removing false positives.
- For example, a table obtained from two RAM dumps will have the following header:
| ID ADDR SIZE | UNSIGNED_DEC UNSIGNED_HEX SIGNED_DEC UNSIGNED_HEX FLOAT | UNSIGNED_DEC UNSIGNED_HEX SIGNED_DEC UNSIGNED_HEX FLOAT |
, with a row containing a signed 2-byte value displayed as:
| 302 0x20006f0e 2 | 62400 0xf3c0 -3136 -0xc40 | 62400 0xf3c0 -3136 -0xc40 |
- To identify the addresses where the sensors measurements are stored, compare the value displayed on the D9's screen with the ones displayed in the table (for eg. the temperature is 23.4°C, so a variable could contain value
d234
). - By following the steps described, it was concluded that the target adresses are:
0x20000078
for TEMP in 0.1 °C resolution,0x2000007a
for HUMID in 0.1 %,0x20000086
for PM25 in 1 ug/m^3,0x20000088
for PM10 in 1 ug/m^3,0x200000ca
for CO2 in 1 ppm,0x200000f6
for HCHO in 0.001 ppm,0x20000116
for TVOC in 0.001 mg/m^3.
- Once the RAM addresses and sizes of the measurement values are determined, they can be read anytime during normal MCU runtime.
- A PyOCD-compatible Kitprog2/3 debug probe was used in CMSIS-DAP mode, due to its several advantages (size, reliability, cost) - it may be necessary to update to the latest firmware.
- The Kitprog's SWD lines (
GND
,SWCLK
,SWDIO
) are connected to the D9 debug connector, while the USB power/data lines are connected directly to the D9's micro-USB socket - remove resistorsR11
andR16
to disconnect the data lines from the main MCU. - These changes allow powering both the target device and probe from the same USB connection, and integrating the probe inside the case, thus retaining the overall design aesthetics.
- To prevent any hardware shutdown that will then require physically pushing the power button to restart, connect the anode of diode
D3
toGND
.
- Once hardware setup is complete, the D9 can be connected to the host PC via it's USB socket, now enumerating as a CMSIS-DAP device.
- Debug operations, such as reading RAM/ROM memory contents, can be accomplished with any compatible software, notably PyOCD.
Since the Python script relies on non-standard libraries, a Home Assistant Docker installation is assumed to be already working. Also, a MQTT broker (for example Mosquito) is also installed, configured and accessible in HA.
As a alternative, a separate PC can also be used for interfacing to the D9.
- Install the required python libraries:
sudo pip install paho_mqtt pyocd
. - Install the PyOCD's support package for the targeted microcontroller:
sudo pyocd pack install gd32f103
. - Edit the
private_config.json
file by configuring the user-specific values of:- used MQTT broker (
HOSTNAME
,USERNAME
,PASSWORD
), - measurements sampling period (
SAMPLE_INTERVAL
), - Optionally, modify the
SENSORS
dictionary list based on implementation -"_name_": [_address_, _ratio_, _decimalPlaces_]
.
- used MQTT broker (
- Run the Python script as root:
sudo python3 mqtt_d9.py
.
User configuration is not necessary, as MQTT auto-discovery is implemented.
All the reverse-engineering, development, integration, and documentation efforts are based on the latest software and hardware versions available at the time of writing (November 2022), and licensed under the GNU General Public License v3.0.