Skip to content

Instantly share code, notes, and snippets.

@Chocapikk
Last active August 11, 2026 22:28
Show Gist options
  • Select an option

  • Save Chocapikk/0baa8e68b87f8ed0873c39504184ebc6 to your computer and use it in GitHub Desktop.

Select an option

Save Chocapikk/0baa8e68b87f8ed0873c39504184ebc6 to your computer and use it in GitHub Desktop.
Uniwill Liquid Cooling (LCT21001/LCT22002) - Complete BLE Protocol (commands + responses). Reverse-engineered from GCUService.exe IL.

Uniwill Liquid Cooling (LCT21001/LCT22002) - BLE Protocol

Complete protocol documentation for the Uniwill/Clevo laptop liquid cooling units used by XMG OASIS, TUXEDO Aquaris, and other rebrands. Covers both command and response sides.

Reverse-engineered from GCUService.exe (Uniwill's backend service, .NET IL, 402k lines) by decompiling with ikdasm. This is the first public documentation of the response protocol - existing projects (watercooler-manager, TUXEDO Control Center, LPPController) only implement the command side.

BLE Connection

The cooling unit uses BLE GATT with the Nordic UART Service (NUS).

Device Names

  • CoolingSystem LCT21001 (Mk1)
  • CoolingSystem LCT22002 (Mk2)

GATT UUIDs

UUID Role
6e400001-b5a3-f393-e0a9-e50e24dcca9e NUS Service
6e400002-b5a3-f393-e0a9-e50e24dcca9e TX - host writes commands here
6e400003-b5a3-f393-e0a9-e50e24dcca9e RX - host subscribes to notifications here

Connection Handshake

The host (Control Center) connects in this order:

  1. Scan for BLE devices, match by stored MAC address
  2. Connect to the BLE device
  3. Discover GATT services, find NUS service (6e400001-...)
  4. Discover characteristics:
    • Store TX (6e400002-...) for writing commands
    • Enable notifications on RX (6e400003-...) for receiving responses
  5. Write "sw" (bytes 73 77, no 0xFE framing) to TX
  6. Device responds on RX with a firmware version string (UTF-8, no framing)
  7. Host parses the version string. Connection is only considered established after this step.

Step 5-7 are mandatory. If the device doesn't respond to "sw", the connection never completes.

After the connection is established, the host immediately restores the last saved state: pump duty + voltage, fan duty, head LED color + mode, fan LED color + mode. The device should expect a burst of commands right after the handshake.

The firmware version string must be at least 18 characters. The host calls Substring(17) to extract the version part. Example:

CoolingSystem FW V1.00.00
                 ^-- version extracted from here

Command Protocol (Host -> Device)

All commands are 8-byte packets written to the TX characteristic:

[0xFE] [CMD] [ON/OFF] [P1] [P2] [P3] [P4] [0xEF]

0xFE = start marker, 0xEF = end marker.

Commands

Commands used by the host (GCUService.exe):

CMD Name byte[2] byte[3] byte[4] byte[5] byte[6]
0x19 System Mode on/off mode param 0 0
0x1B Fan Control on/off duty (0-100) 0 0 0
0x1C Pump Control on/off duty (0-100) volt_code 0 0
0x1E Head LED on/off R (0-255) G (0-255) B (0-255) mode
0x32 Query Meter 0 0 0 0 0
0x33 Fan LED on/off R (0-255) G (0-255) B (0-255) mode
0x38 Line-off 0 0 0 0 0

Additional commands found in the firmware but never sent by the host. Two firmware versions were analyzed:

  • LCT21001 v1.0.1.7 (40KB, from BT_DFU_FW/15/)
  • LCT22002 v2.0.0.4 (53KB, from BT_DFU_FW/20/)

The LCT22002 firmware is significantly larger and supports more commands. CMD 0x33 (Fan LED) is absent from LCT21001 but present in LCT22002, confirming that the firmware evolves across versions and models.

CMD Name byte[2] byte[3] byte[4] LCT21001 LCT22002
0x1A System Enable on/off 0 0 Yes Yes
0x1D LED Zone 2 on/off 0 0 Yes Yes
0x1F Reset All 0 0 0 Yes Yes
0x30 Send Telemetry 0 0 0 Yes Yes
0x34 Sensor Config on/off param1 param2 Yes Yes
0x35 Timer Config on/off value 0 Yes Yes

LCT22002-only additions:

CMD Name byte[2] byte[3] byte[4] byte[5] byte[6]
0x33 Fan LED on/off R G B zone (1-6)
0x37 FDS Write 1 data0 data1 data2 record key offset (key = byte[6] + 0x1111)

CMD 0x31 exists in the dispatch but is a no-op (immediately exits). It is only meaningful as a response code (device -> host).

CMD 0x33 in the LCT22002 has 6 sub-zones selected by byte[6] (values 1-6), each controlling a different RAM flag. This is more complex than the simple mode byte in the host's implementation.

CMD 0x37 writes to the Nordic FDS (Flash Data Storage) key-value store. File ID is 0x2222 (hardcoded at 0xADBE: movw r1, 0x2222), record key is byte[6] + 0x1111 (range 0x1111-0x1210). Bytes[3-5] are 3 bytes of data, a 4th byte is an auto-incrementing counter from RAM 0x20003760. Only executes when byte[2] == 1. Data is staged in RAM at 0x20004940, record key at 0x2000493E, write flag at 0x20004919. The FDS subsystem handles the actual flash write with wear leveling. This is persistent configuration storage (survives power cycles), not raw flash write or code execution. The host (GCUService) never sends this command.

Extended Protocol: 0xFA/0xAF (LCT22002 only)

A second packet format using 30-byte frames instead of 8-byte. Validated by byte[0]=0xFA and byte[0x1D]=0xAF.

  • byte[1]: boolean-normalized, stored to 0x20003645 (extended config enable)
  • bytes[2-25] (24 bytes): copied to RAM 0x2000362C - 0x20003643
  • bytes[0x1A-0x1C] (3 bytes): copied to RAM 0x20003646 - 0x20003648

Total: 27 bytes of configuration data written to device RAM. The purpose of this data is unknown from the firmware analysis alone - requires correlation with the host or hardware testing.

Plus the firmware version query: write 73 77 (ASCII "sw", 2 bytes, no 0xFE/0xEF framing).

All Packets

Bytes Command
FE 1B 01 DD 00 00 00 EF Fan ON, DD = duty (0-100)
FE 1B 00 00 00 00 00 EF Fan OFF
FE 1C 01 DD VV 00 00 EF Pump ON, DD = duty (0-100), VV = voltage code
FE 1C 00 00 00 00 00 EF Pump OFF
FE 1E 01 RR GG BB MM EF Head LED ON, RGB color, MM = mode
FE 1E 00 00 00 00 00 EF Head LED OFF
FE 33 01 RR GG BB MM EF Fan LED ON, RGB color, MM = mode
FE 33 00 00 00 00 00 EF Fan LED OFF
FE 32 00 00 00 00 00 EF Query meter state
FE 19 00 01 00 00 00 EF Sleep (sent when clearing paired device)
FE 19 01 04 50 00 00 EF Start priming (water fill)
FE 19 00 04 00 00 00 EF Stop priming
FE 38 00 00 00 00 00 EF Line-off (sent on disconnect, not sleep)
73 77 Query firmware version (no framing)

Firmware-only packets (not sent by the host, found in firmware binary):

Bytes Command
FE 1A 01 00 00 00 00 EF System enable (on)
FE 1A 00 00 00 00 00 EF System enable (off)
FE 1D 01 00 00 00 00 EF LED zone 2 on
FE 1D 00 00 00 00 00 EF LED zone 2 off
FE 1F 00 00 00 00 00 EF Reset all outputs to defaults
FE 30 00 00 00 00 00 EF Request telemetry notification (25-byte response)
FE 34 01 P1 P2 00 00 EF Sensor config enable, P1 and P2 written to RAM
FE 34 00 00 00 00 00 EF Sensor config disable
FE 35 01 VV 00 00 00 EF Timer config enable, VV written to RAM, calls function
FE 35 00 00 00 00 00 EF Timer config disable

Pump Voltage Codes

byte[4] of the pump command (0x1C):

Voltage Code
7V 2
8V 3
11V 0
12V 1

LED Modes

byte[6] of head LED (0x1E) and fan LED (0x33):

Mode Value
Static 0
Breathe 1
Colorful 2
Breathe Color 3

System Mode (0x19) Sub-Commands

byte[2] byte[3] Meaning
0 1 Sleep
1 4 Start priming, byte[4] = speed (0x50)
0 4 Stop priming

Firmware-Only Commands

These commands are handled by the nRF52832 firmware but never sent by the Windows host service. Found by disassembling ble_app_uart_pca10040_s132.bin using r2 -a arm -b 16. Handler details below are from the LCT21001 firmware (40KB). The LCT22002 firmware (53KB) was also fully analyzed - its dispatch contains the same commands plus 0x33 (Fan LED) and 0x37 (Flash Write), with RAM addresses shifted to different offsets.

0x1A - System Enable

Reads byte[2], normalizes to boolean (0 or 1), writes to RAM 0x20002b01. Clears RAM 0x200033c8 to 0. No other side effects.

0x1D - LED Zone 2

Reads byte[2], normalizes to boolean, writes to RAM 0x20003399. Clears RAM 0x20003398 to 0. Structurally identical to 0x1A but writes to different RAM addresses. The RAM addresses are in the 0x200033xx range, adjacent to the head LED (0x1E) variables.

Note: CMD 0x33 (Fan LED, sent by the host) is not in the LCT21001 v1.0.1.7 dispatch but is present in the LCT22002 v2.0.0.4 dispatch. The relationship between 0x1D and 0x33 is unknown without testing on hardware.

0x1F - Reset All

Takes no parameters. Calls function at 0x4454 and returns. No packet bytes are read beyond the CMD byte.

0x30 - Send Telemetry

Takes no parameters. Copies a 25-byte template from flash (0x2efb8), processes it through function 0x84c2, then sends it as a BLE GATT notification via sd_ble_gatts_hvx (0x7704). Retries on NRF_ERROR_RESOURCES (0x13). Uses the same code path as the "sw" handler. The 25-byte response format and contents of the template are not yet analyzed.

0x34 - Sensor Configuration

Three parameters: byte[2] = enable (boolean) -> RAM 0x20003c0b, byte[3] -> 32-bit write to RAM 0x20003c0c, byte[4] -> 32-bit write to RAM 0x20003c10. No function call after writing.

0x35 - Timer Configuration

Two parameters: byte[2] = enable (boolean) -> RAM 0x20003c18, byte[3] -> 32-bit write to RAM 0x20002b44. Calls function 0x7cd8 after writing (unlike 0x34 which does not call any function).

Response Protocol (Device -> Host)

Responses are BLE notifications on the RX characteristic. The host parser distinguishes two types based on the first byte.

Type 1: Meter State (first byte == 0xFE)

Format: FE [31|32] XX SS 00 00 00 EF

  • byte[1] = 0x31: unsolicited push from the device (periodic or on state change)
  • byte[1] = 0x32: response to a Query Meter command
  • byte[2]: unused (parser ignores it)
  • byte[3] = SS: the meter state value

Both 0x31 and 0x32 are parsed identically - the host reads byte[3] and fires a meter state event.

Meter state values:

SS Meaning
2 Flow OK. Host sets IsMeterNormal = true.
Anything else Error. Host sets IsMeterNormal = false, but only if the pump is currently running. If the pump is off, the error is ignored.

When IsMeterNormal goes false, the host disables GPU overclocking (via SetStatusSS(false)) and shows an error in the UI.

The host does not poll meter state. It expects the device to push 0x31 notifications on its own. The Query Meter command (0x32) exists in the code but is never called automatically.

Type 2: Firmware Version (first byte != 0xFE)

If the first byte is not 0xFE, the entire payload is decoded as a UTF-8 string and stored as the firmware version. This is the response to the "sw" query sent during the connection handshake.

Parsing Logic (reconstructed from IL)

if (bytes[0] == 0xFE)
{
    if (bytes[1] == 0x32 || bytes[1] == 0x31)
    {
        meter_state = bytes[3];
        Event_Meter_State(meter_state);
    }
    // Other 0xFE packets: ignored
}
else
{
    fw_version = UTF8.GetString(bytes);
    MergeFWversion(fw_version);
}

Meter State Error Handling (reconstructed from IL)

void OnMeterState(int state)
{
    if (!connected) return;

    if (state == 2)
        is_meter_normal = true;
    else if (pump_volt > 0)
        is_meter_normal = false;

    SetHWOCStatus(is_meter_normal);
    BroadcastStatus();
}

Water Fill / Priming Sequence

When the user triggers "Input Water" in the Control Center, the host runs this sequence:

  1. Send FE 19 01 04 50 00 00 EF (start priming)
  2. Fan ON at 50% duty
  3. Loop 8 times:
    • Pump OFF, wait 500ms
    • Pump ON (duty=100, volt=8V), wait 5500ms
    • Pump OFF, wait 2500ms
  4. Send FE 19 00 04 00 00 00 EF (stop priming)
  5. Restore normal operating mode

Total duration: ~68 seconds. The timing is entirely host-controlled. The device just needs to accept the pump/fan commands as they come.

Firmware Implementation Guide

What a custom board emulating the cooling unit needs to implement.

1. Advertise as NUS peripheral

Device name must be CoolingSystem LCT21001 or CoolingSystem LCT22002. Expose the Nordic UART Service with TX and RX characteristics.

2. Handle commands on TX (0x0002)

Accept writes. If 8 bytes with 0xFE/0xEF framing, dispatch on byte[1]:

  • 0x1B - Set fan duty
  • 0x1C - Set pump duty + voltage
  • 0x1E - Set head LED color + mode
  • 0x33 - Set fan LED color + mode
  • 0x19 - System mode (sleep / priming)
  • 0x32 - Respond with current meter state
  • 0x38 - Shutdown

If 2 bytes 73 77 ("sw"), respond with firmware version string.

3. Send responses on RX (0x0003)

When Send Format
Host sends 73 77 CoolingSystem FW V1.00.00 UTF-8 string, no framing. At least 18 chars. Mandatory for connection.
Flow state changes FE 31 00 SS 00 00 00 EF SS=2 means OK, anything else means error. Push this, host doesn't poll.
Host sends FE 32 ... FE 32 00 SS 00 00 00 EF Same SS value as above.

If you don't have a flow sensor, always send SS = 2.

That's it. The host tracks everything else (pump duty, fan duty, LED settings) on its own side.

EC Registers (Windows only)

The Windows service also reads Embedded Controller registers via WMI. Not relevant for BLE implementations, but documented for completeness.

Address Description Format
0x07C7 LC support enable Bit 1 = enable/disable
0x07E6 Fan duty readback Bit 7 = valid, bits 0-6 = value
0x07E7 Pump voltage readback Bit 7 = valid, bits 0-6 = value

Appendix: IL Proof

Everything above is extracted from GCUService.exe, the Uniwill backend service at Program Files/OEM/Control Center/UniwillService/MyControlCenter/. Decompiled with ikdasm into 402,742 lines of CIL.

ntfscat /dev/nvme0n1p3 \
  "Program Files/OEM/Control Center/UniwillService/MyControlCenter/GCUService.exe" \
  > GCUService.exe
ikdasm GCUService.exe > gcu_service_il.txt   # 402,742 lines

Key classes: Demo.BLE, Demo.BLE_STATE, GCUService.MyFan.BTCooling.LiquidCooling, GCUService.MyFan.BTCooling.LiquidHWOC.

Device names and UUIDs

// Demo.BLE constructor
IL_0001:  ldstr      "CoolingSystem LCT21001"
IL_000c:  ldstr      "CoolingSystem LCT22002"
IL_0017:  ldstr      "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
IL_0027:  ldstr      "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
IL_0037:  ldstr      "6e400003-b5a3-f393-e0a9-e50e24dcca9e"

BLE state enum

// Demo.BLE_STATE
.field public static literal valuetype Demo.BLE_STATE DeviceNotReady = int32(0)
.field public static literal valuetype Demo.BLE_STATE DeviceIsReady = int32(1)
.field public static literal valuetype Demo.BLE_STATE Disconnected = int32(2)
.field public static literal valuetype Demo.BLE_STATE Scanning = int32(3)
.field public static literal valuetype Demo.BLE_STATE Connecting = int32(4)
.field public static literal valuetype Demo.BLE_STATE IsConnectable = int32(5)
.field public static literal valuetype Demo.BLE_STATE Connected = int32(6)

Command packets (.data cil section)

.data cil I_005F71F4 = bytearray (FE 1C 01 00 00 00 00 EF)   // Pump ON
.data cil I_005F71FC = bytearray (FE 19 01 04 50 00 00 EF)   // Start Priming
.data cil I_005F7204 = bytearray (FE 1E 01 00 00 00 00 EF)   // LED ON
.data cil I_005F7218 = bytearray (FE 32 00 00 00 00 00 EF)   // Query Meter
.data cil I_005F7220 = bytearray (FE 19 00 01 00 00 00 EF)   // Sleep
.data cil I_005F7228 = bytearray (FE 1B 00 00 00 00 00 EF)   // Fan OFF
.data cil I_005F723C = bytearray (FE 1B 01 00 00 00 00 EF)   // Fan ON
.data cil I_005F7244 = bytearray (FE 1E 00 00 00 00 00 EF)   // LED OFF
.data cil I_005F7258 = bytearray (FE 33 01 00 00 00 00 EF)   // Fan LED ON
.data cil I_005F7260 = bytearray (FE 19 00 04 00 00 00 EF)   // Stop Priming
.data cil I_005F7280 = bytearray (FE 1C 00 00 00 00 00 EF)   // Pump OFF
.data cil I_005F728C = bytearray (FE 33 00 00 00 00 00 EF)   // Fan LED OFF
.data cil I_005F7294 = bytearray (FE 38 00 00 00 00 00 EF)   // Line-off

"sw" firmware query (GetGattServiceCCCD d__48)

IL_0273:  ldstr      "sw"
IL_0278:  ldc.i4.0    // BinaryStringEncoding.Utf8
IL_0279:  call       CryptographicBuffer::ConvertStringToBinary(string, BinaryStringEncoding)
IL_0283:  call       instance Task<bool> Demo.BLE::WriteBufferToSelectedCharacteristicAsync(IBuffer)

Response parser (FormatValueByPresentation)

IL_000a:  ldelem.u1                          // bytes[0]
IL_000b:  ldc.i4     0xfe                    // compare with 0xFE
IL_0010:  bne.un.s   IL_0068                 // if != 0xFE -> UTF-8 branch

IL_0014:  ldelem.u1                          // bytes[1]
IL_0015:  ldc.i4.s   50                      // 0x32
IL_0017:  beq.s      IL_0020                 // if == 0x32 -> meter state

IL_001b:  ldelem.u1                          // bytes[1]
IL_001c:  ldc.i4.s   49                      // 0x31
IL_001e:  bne.un.s   IL_0057                 // if != 0x31 -> skip

IL_0022:  ldc.i4.3                           // index 3
IL_0023:  ldelem.u1                          // bytes[3] = meter_state
IL_0024:  call       instance void Demo.BLE::set_meter_state(int32)
IL_0052:  callvirt   instance void Report_Meter_State_Handler::Invoke(int32)

// UTF-8 branch:
IL_0069:  call       System.Text.Encoding::get_UTF8()
IL_006f:  callvirt   instance string System.Text.Encoding::GetString(uint8[])
IL_0074:  call       instance void Demo.BLE::set_BTDevFWversion(string)
IL_0095:  call       instance void Demo.BLE::MergeFWversion(string)

Meter state handler (LC_Event_Meter_State)

IL_0001:  ldfld      bool LiquidCooling::_Connected
IL_0006:  brfalse.s  IL_0042                 // if (!connected) return

IL_0009:  ldc.i4.2                           // compare state with 2
IL_000a:  bne.un.s   IL_0015                 // if != 2 -> else

IL_000d:  ldc.i4.1                           // true
IL_000e:  stfld      bool LiquidCooling::IsMeterNormal

IL_0016:  ldfld      int32 LiquidCooling::_PumpVolt
IL_001c:  ble.s      IL_0025                 // if (PumpVolt <= 0) skip

IL_001f:  ldc.i4.0                           // false
IL_0020:  stfld      bool LiquidCooling::IsMeterNormal

IL_0037:  call       instance void LiquidHWOC::SetStatusSS(bool)
IL_003d:  call       instance void LiquidCooling::SendLCsettings()

MergeFWversion (connection finalization)

IL_009e:  ldc.i4.s   17
IL_00a0:  callvirt   instance string System.String::Substring(int32)
IL_00a5:  stfld      string Demo.BLE::LC_FWver   // version starts at char 17

IL_0151:  ldc.i4.6                               // BLE_STATE.Connected
IL_0152:  callvirt   instance void Report_BLE_State_Handler::Invoke(BLE_STATE)

Pump voltage mapping (PumpControl)

IL_0001:  ldc.i4.s   11   ->  code = 0
IL_000a:  ldc.i4.s   12   ->  code = 1
IL_0013:  ldc.i4.7        ->  code = 2
IL_001a:  (default)       ->  code = 3

// Template: I_005F71F4 (FE 1C 01 00 00 00 00 EF)
IL_0038:  ldc.i4.3   // byte[3] = duty
IL_003c:  ldc.i4.4   // byte[4] = volt_code

EC registers

.field public static literal uint16 CoolingModeECAddress = uint16(0x07C7)
.field public static literal uint16 ADDR_LC_FAN_VALUE = uint16(0x07E6)
.field public static literal uint16 ADDR_LC_PUMP_VALUE = uint16(0x07E7)

// ReadPumpVolt:
IL_0008:  ldstr      "LiquidCooling"          // WMI namespace
IL_000d:  ldc.i4     0x7e7                     // EC register
IL_001a:  ldc.i4.7                             // bit 7 = valid flag
IL_0024:  ldc.i4.s   127                       // & 0x7F = value

Firmware command dispatch (ARM Thumb disassembly)

Two firmware versions analyzed:

  • LCT21001 v1.0.1.7 (40KB) from BT_DFU_FW/15/LCT21001_DFU-AP_v1.0.1.7.zip
  • LCT22002 v2.0.0.4 (53KB) from BT_DFU_FW/20/LCT22002_DFU-AP_v2.0.0.4.zip

Both are nRF52832 ARM Cortex-M4 Thumb, SoftDevice s132. Disassembled with r2 -a arm -b 16.

LCT21001 dispatch at 0x44A0:

0x44A8:  cmp r2, 0xfe       ; byte[0] == start marker?
0x44AE:  cmp r3, 0xef       ; byte[7] == end marker?
0x44BE:  cmp r3, 0x1a       ; CMD 0x1A - System Enable
0x44DE:  cmp r3, 0x1b       ; CMD 0x1B - Fan Control
0x451A:  cmp r3, 0x1c       ; CMD 0x1C - Pump Control
0x45DE:  cmp r3, 0x1d       ; CMD 0x1D - LED Zone 2
0x45FC:  cmp r3, 0x1e       ; CMD 0x1E - Head LED
0x465E:  cmp r3, 0x1f       ; CMD 0x1F - Reset All
0x4670:  cmp r3, 0x30       ; CMD 0x30 - Send Telemetry
0x46D4:  cmp r3, 0x19       ; CMD 0x19 - System Mode
0x47D4:  cmp r3, 0x32       ; CMD 0x32 - Query Meter
0x47E0:  cmp r3, 0x34       ; CMD 0x34 - Sensor Config
0x47FC:  cmp r3, 0x35       ; CMD 0x35 - Timer Config
0x4818:  cmp r3, 0x38       ; CMD 0x38 - Line-off
; ASCII dispatch:
0x4566:  cmp r2, 0x73       ; byte[0] == 's'
0x456C:  cmp r3, 0x77       ; byte[1] == 'w'

LCT22002 dispatch at 0x62D2. Three protocols: standard 8-byte (0xFE/0xEF), ASCII "sw" (0x73/0x77), and extended 30-byte (0xFA/0xAF):

; Standard 8-byte protocol
0x62D2:  cmp r3, 0xfe       ; byte[0] == start marker?
0x62D8:  cmp r3, 0xef       ; byte[7] == end marker?
0x62F6:  cmp r3, 0x1a       ; CMD 0x1A - System Enable
0x6424:  cmp r3, 0x1b       ; CMD 0x1B - Fan Control
0x645E:  cmp r3, 0x1c       ; CMD 0x1C - Pump Control
0x64B2:  cmp r3, 0x1d       ; CMD 0x1D - LED Zone 2
0x64CE:  cmp r3, 0x1e       ; CMD 0x1E - Head LED
0x652E:  cmp r3, 0x1f       ; CMD 0x1F - Reset All
0x65E4:  cmp r3, 0x30       ; CMD 0x30 - Send Telemetry
0x6646:  cmp r3, 0x19       ; CMD 0x19 - System Mode
0x668A:  cmp r3, 0x31       ; CMD 0x31 - no-op (exits immediately)
0x6690:  cmp r3, 0x32       ; CMD 0x32 - Query Meter
0x669C:  cmp r3, 0x33       ; CMD 0x33 - Fan LED (not in LCT21001)
0x6746:  cmp r3, 0x34       ; CMD 0x34 - Sensor Config
0x6762:  cmp r3, 0x35       ; CMD 0x35 - Timer Config
0x677E:  cmp r3, 0x37       ; CMD 0x37 - Flash Write (not in LCT21001)
0x67B8:  cmp r3, 0x38       ; CMD 0x38 - Line-off

; ASCII protocol
0x6320:  cmp r3, 0x73       ; byte[0] == 's'
0x6326:  cmp r3, 0x77       ; byte[1] == 'w'

; Extended 30-byte protocol
0x6398:  cmp r3, 0xfa       ; byte[0] == 0xFA
0x63A0:  cmp r3, 0xaf       ; byte[0x1D] == 0xAF

Example handler - CMD 0x1A (LCT21001):

0x44BE:  cmp r3, 0x1a       ; CMD == 0x1A?
0x44C0:  bne 0x44de         ; no -> next CMD
0x44C2:  ldr r1, [0x4724]   ; log string
0x44C6:  bl 0x9c0           ; log
0x44CA:  ldr r3, [r5, 0x10] ; packet pointer
0x44CC:  ldr r2, [0x4728]   ; r2 = 0x20002b01
0x44CE:  ldrb r3, [r3, 2]   ; r3 = byte[2]
0x44D0:  subs r3, 0         ; normalize to boolean
0x44D2:  it ne
0x44D4:  movs r3, 1
0x44D6:  strb r3, [r2]      ; store to 0x20002b01

Example handler - CMD 0x30 Send Telemetry (LCT21001):

0x4672:  bl 0x9c0           ; log
; Copy 25-byte template from flash 0x2efb8 to stack
0x4688:  bl 0x84c2          ; format response buffer
; Send BLE notification:
0x46A4:  bl 0x7704          ; sd_ble_gatts_hvx()
0x46A8:  cmp r0, 8          ; NRF_ERROR_INVALID_STATE -> exit
0x46AE:  cmp r0, 0x13       ; NRF_ERROR_RESOURCES -> retry
0x46B4:  cmp r0, 5          ; NRF_ERROR_NOT_FOUND -> exit
0x46B8:  cmp r0, 0          ; NRF_SUCCESS -> exit

File inventory

File Path Type Description
GCUService.exe OEM/Control Center/UniwillService/MyControlCenter/ .NET IL Backend service (fully decompiled)
GamingCenter3_Cross.dll UWP package UWP AOT Frontend (strings only)
M2Mqtt.Net.dll Same as GCUService .NET IL Local IPC MQTT library
BTSavingSettings.json MyControlCenter/BTSavingSettings/ JSON Paired device MAC
LCSavingSettings.json MyControlCenter/LCSavingSettings/ JSON LC settings
LiquidHWOC.json MyControlCenter/LiquidHWOC/ JSON GPU OC settings
BT_DFU_FW.zip OEM/Control Center/ZipFiles/ ZIP Nordic DFU firmware
BT_DFU_Tool.zip OEM/Control Center/ZipFiles/ ZIP Nordic DFU tool
ble_app_uart_pca10040_s132.bin Inside BT_DFU_FW/15/LCT21001_DFU-AP_v1.0.1.7.zip ARM Thumb nRF52832 LCT21001 firmware (40KB, fully analyzed)
ble_app_uart_pca10040_s132.bin Inside BT_DFU_FW/20/LCT22002_DFU-AP_v2.0.0.4.zip ARM Thumb nRF52832 LCT22002 firmware (53KB, dispatch analyzed)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment