How can I start reverse engineering the UART protocol used by a Hisense AC WiFi module?
0x97 as the main status frame and 0x29 as the control frame. [#21893661]Czy wolisz polskÄ wersjÄ strony elektroda?
Nie, dziÄkujÄ Przekieruj mnie tam### General frame structure
| Byte offset(s) | Example | Working meaning | Confidence |
| -------------- | ------------------------------------------------------ | ------------------------------------------------------------------------ | ---------- |
| `0:1` | `F4 F5` | Frame header / sync | High |
| `2` | `00` / `01` | Source or direction byte. `00` = Wi-Fi module TX, `01` = AC-side TX | High |
| `3` | `40` | Protocol family / constant channel byte | High |
| `4` | `0C`, `13`, `29`, `7B`, `97` | Opcode / frame kind. Does not currently behave like a simple length byte | High |
| `5:12` | `00 00 01 01 FE 01 00 00` or `01 00 FE 01 01 01 01 00` | Routing / endpoint / addressing block | Medium |
| `13:14` | `65 00`, `66 00`, `1E 00` | Packet type / subtype family | High |
| `15:n-4` | varies | Payload / body | High |
| `n-4` | `01`, `02`, `04`, `05` | Trailing control / count byte, exact purpose unknown | Low |
| `n-3` | e.g. `B3`, `5C`, `7A`, `B1` | Checksum byte | Medium |
| `n-2:n-1` | `F4 FB` | Frame footer / terminator | High |#### `0x97` long status/state response
| Offset(s) | Example bytes | Working meaning |
| --------- | ------------------------- | --------------------------- |
| `0:1` | `F4 F5` | Header |
| `2` | `01` | Sent by AC side |
| `3` | `40` | Protocol family |
| `4` | `97` | Long status opcode |
| `5:12` | `01 00 FE 01 01 01 01 00` | Routing block |
| `13:14` | `66 00` | Status family |
| `15:n-4` | long variable payload | State / telemetry data |
| `n-4` | e.g. `04`, `05` | Trailing control/count byte |
| `n-3` | e.g. `B1`, `7A` | Checksum |
| `n-2:n-1` | `F4 FB` | Footer |#### `0x29` control/update
| Offset(s) | Example bytes | Working meaning |
| --------- | --------------------------------- | ---------------------------------------------- |
| `0:1` | `F4 F5` | Header |
| `2` | `00` | Sent by Wi-Fi module |
| `3` | `40` | Protocol family |
| `4` | `29` | Control/update opcode |
| `5:12` | `00 00 01 01 FE 01 00 00` | Routing block |
| `13:14` | `65 00` | Control/update family |
| `15:45` | variable control payload | Desired state / control fields |
| `18:19` | e.g. `5C 2D`, `3C 2D`, `00 33` | High-value control pair within payload |
| `46` | usually `02` | Trailing control/count byte |
| `47` | e.g. `5C`, `3C`, `06` | Checksum |
| `48:49` | `F4 FB` | Footer ### Known frame families
| Opcode byte `[4]` | Direction | Typical type/subtype | Working meaning | Confidence |
| ----------------- | --------- | -------------------- | --------------------------------------------- | ---------- |
| `0C` | TX | `66 00` | Short poll request | High |
| `13` | TX/RX | `1E 00` | Short query / telemetry / confirmation family | Medium |
| `29` | TX | `65 00` | Control / update request | High |
| `7B` | RX | `65 00` | Direct response / ack to `0x29` | High |
| `97` | RX | `66 00` | Longer status / state report | High |PARSED STRUCTURE:
header : F4 F5 [0:2]
src : 01 (ac) [2:3]
family : 40 [3:4]
opcode : 97 (status) [4:5]
route : 01 00 FE 01 01 01 01 00 [5:13]
type : 66 [13:14]
subtype : 00 [14:15]
body : 01 12 00 08 1A 1A 1A 80 80 00 01 01 00 00 00 00 00 00 00 00 40 00 80 05 00 00 00 00 00 0C 2C 0E 00 00 00 DF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [15:156]
trail : 05 [156:157]
checksum : 96 [157:158]
footer : F4 FB [158:160]
body_len : 8D
RULESET: Main Status
ROUTING: Key=STATUS_66_POLL -> Targets=[Main Status]
Pair: TX seq 15 (Īt=+199.7ms)
DECODED:
STATUS_DATA_MARKER : stable_status_page_marker_candidate (01)
STATUS_WIND_STATUS : high (12)
STATUS_SLEEP_STATUS : off (00)
STATUS_MODE_RUN_DIRECTION_PACKED_CANDIDATE : fan_only_on_horizontal_swing_off (08)
STATUS_INDOOR_TEMPERATURE_SETTING_DISPLAY : 26
STATUS_INDOOR_TEMPERATURE_STATUS_DISPLAY : 26
STATUS_INDOOR_PIPE_TEMPERATURE_C : 26
STATUS_INDOOR_HUMIDITY_SETTING : unavailable (80)
STATUS_INDOOR_HUMIDITY_STATUS : unavailable (80)
STATUS_FEEL_DISPLAY_CONTEXT_09_11_CANDIDATE : 00 01 01
STATUS_PRESET_OR_FLAGS_11 : alt_runtime_context_candidate (01)
STATUS_TIMER_RTC_BLOCK_12_19_CANDIDATE : 00 00 00 00 00 00 00 00
STATUS_PRESET_STAGE_20_CANDIDATE : vertical_swing_on_candidate (40)
STATUS_PRESET_STAGE_21_CANDIDATE : normal_or_clear_candidate (00)
STATUS_FEATURE_PRESET_STATE_20_23_CANDIDATE : 40 00 80 05
STATUS_FEATURE_EXTENSION_24_25_CANDIDATE : 00 00
STATUS_RUNTIME_TELEMETRY_26_31_CANDIDATE : 00 00 00 0C 2C 0E
STATUS_OUTDOOR_TELEMETRY_32_39_CANDIDATE : 00 00 00 DF 00 00 00 00
STATUS_POWER_LOAD_INDEX_35_CANDIDATE : DF
STATUS_OUTDOOR_SYSTEM_STATE_40_45_CANDIDATE : 00 00 00 00 00 00
STATUS_RESERVED_ZERO_46_55_CANDIDATE : 00 00 00 00 00 00 00 00 00 00
STATUS_BLOCK_56_71 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
STATUS_BLOCK_72_87 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
STATUS_BLOCK_88_103 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
STATUS_BLOCK_104_120 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
RAW:
F4 F5 01 40 97 01 00 FE 01 01 01 01 00 66 00 01 12 00 08 1A 1A 1A 80 80 00 01 01 00 00 00 00 00 00 00 00 40 00 80 05 00 00 00 00 00 0C 2C 0E 00 00 00 DF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05 96 F4 FB
PAYLOAD:
01 12 00 08 1A 1A 1A 80 80 00 01 01 00 00 00 00 00 00 00 00 40 00 80 05 00 00 00 00 00 0C 2C 0E 00 00 00 DF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
ASCII: ....................@.........,..............................................................................................................iFeeltemperature-source override behavior
C/F)
TL;DR: With 5 known frame families and a 141-byte status payload, this WIP shows that āit is doableā to reverse engineer the Hisense AC WiFi UART protocol. It is for makers who want to decode full-state HVAC packets, map bytes to features, and build reliable control or telemetry tooling for Hisense air conditioners. [#21893661]
Why it matters: This thread turns raw serial traffic into a practical roadmap for decoding status, control, swing, and telemetry fields on a Hisense AC.
| Approach | What it captures | Strength in this thread | Limitation |
|---|---|---|---|
| UART sniffing | Full serial frames | Revealed opcodes 0x97, 0x29, 0x7B, routing bytes, and checksums |
Many payload fields still need mapping |
| IR-style state comparison | Full-state behavior patterns | Helped confirm that multiple states are sent together | Does not expose UART framing details |
Key insight: The most valuable packet is opcode
0x97: it carries the long AC-side status report, including decoded mode, fan, temperatures, swing, and several telemetry candidates. Once you can capture and diff0x97, the protocol becomes much easier to map. [#21893661]
0x0C, 0x13, 0x29, 0x7B, and 0x97, each tied to a repeatable direction and packet role. [#21893661]0x97 sample is 160 bytes long overall, with a 141-byte body (0x8D), plus header F4 F5, checksum, and footer F4 FB. [#21893661]0x08 becomes 0x0A, heat 0x18 becomes 0x1A, and cool 0x28 becomes 0x2A. [#21893661]0x97 as the main status frame and 0x29 as the control frame. [#21893661]F4 F5, then source byte 00 or 01, family byte 40, an opcode at byte 4, an 8-byte routing block at bytes 5:12, type/subtype at bytes 13:14, a variable payload, one trailing control byte, one checksum byte, and footer F4 FB. The checksum sits at n-3, and the trailer byte sits at n-4. That pattern holds across short and long frames described in the thread. [#21893661]0x97 is the long AC-to-module status report, while 0x29 is the module-to-AC control/update request. In the thread, 0x97 uses source 01, type/subtype 66 00, and a long payload carrying state and telemetry. 0x29 uses source 00, type/subtype 65 00, and a shorter control payload, with important bytes noted at offsets 18:19. The sample 0x97 body is 0x8D bytes long, while the shown 0x29 layout runs to byte 49 including checksum and footer. [#21893661]0x97 is the long status/state response sent by the AC side. The thread already maps fields for power, HVAC mode, target temperature, fan speed, horizontal swing, vertical swing, sleep, eco, fan mute, super, iFeel, dimmer/display state, smart operating context, and temperature unit. In one parsed example, the decoder reports high fan, fan-only mode, horizontal swing off, vertical swing on candidate, and three separate 26 °C values. Several telemetry blocks remain labeled as candidates rather than confirmed meanings. [#21893661]0x97 commonly shows 01 00 FE 01 01 01 01 00, while 0x29 uses 00 00 01 01 FE 01 00 00. That reversal suggests direction-aware addressing between the WiFi module and the AC side. [#21893661]n-3, with examples such as B1, 7A, 5C, 3C, 06, and 96. Verify it by capturing repeated frames, flipping one known field, and checking whether the checksum changes in sync with payload edits while header F4 F5 and footer F4 FB stay fixed. If one altered byte causes a predictable checksum shift, you can narrow the algorithm quickly. [#21893661]0x08 becomes 0x0A, heat 0x18 becomes 0x1A, and cool 0x28 becomes 0x2A. Each change adds exactly 0x02, which strongly suggests a single feature bit rather than a new operating mode. That matters because you should decode the byte as combined state, not as a flat mode enumeration. [#21893661]iFeel temperature-source override behavior, dimmer/display-state reporting, smart operating-context reporting, and temperature unit in C/F. That is enough to support a useful driver even before every telemetry byte is named. The author also states that first working driver versions for another related project are already in full use. [#21893661]0x97 by polling or waiting for AC-side reports, then split the frame into fixed offsets before decoding candidates. A simple 3-step method is: 1. detect header F4 F5, 2. read through footer F4 FB, 3. label bytes by offsets such as source, family, opcode, route, type, payload, trailer, and checksum. The threadās parser marks the sample as source 01, family 40, opcode 97, type/subtype 66 00, body range 15:156, and body length 0x8D. It also links the packet to TX sequence 15 at +199.7 ms. [#21893661]0x40 across 0x0C, 0x13, 0x29, 0x7B, and 0x97. That consistency suggests 0x40 marks the Hisense AC UART command family rather than carrying state data. [#21893661]n-4 and shows values such as 01, 02, 04, and 05. In the sample 0x97 frame, the trailer is 05; in 0x29, it is usually 02. That pattern suggests a counter, control marker, or page indicator. Keep it separate in your parser so you do not mislabel application data and corrupt later field mapping. [#21893661]0x0C is a short poll request, usually TX with type 66 00. 0x13 is a short query, telemetry, or confirmation family used in both directions with type 1E 00. 0x29 is the WiFi moduleās control/update request with type 65 00. 0x7B is the direct response or acknowledgment to 0x29, and 0x97 is the longer AC status/state report with type 66 00. This family map gives you a practical backbone for a parser. [#21893661]F4 F5, family 40, opcodes, route bytes, and long 0x97 payloads. Use IR-style comparison as a decoding strategy, but use UART as the primary data source when you want implementable protocol details. [#21893661]0x97 frames. In the sample, bytes already correlate with target temperature 26, display temperature 26, pipe temperature 26, humidity placeholders 80 80, a vertical swing candidate 40, and power load candidate DF at offset 35. Keep a table of unchanged versus changed offsets across mode, swing, sleep, and fan tests. Edge case: many blocks from offsets 46 through 120 stayed all zero, so do not force meanings onto bytes that never move. [#21893661]
Comments
I have a Frikko branded Split unit that uses the ConnectLife App, my guess the unit is actually manufactured by Hisense and the other name is just pasted on it. I control it through Home Assistant via... [Read more]
I am aware of this and some other integrations but what I am doing is a replacement firmware for it and UART level decoding so these API integrations are of limited use. That being said I made some... [Read more]
Nice, I“m not sure if your module and mine are the same, as soon as I get the chance I could share some photos of it. https://obrazki.elektroda.pl/9999945800_1782504844_thumb.jpg https://obrazki.elektroda.pl/5175953400_1782504844_thumb.jpg... [Read more]
As far as I know Hisense has 3 modules for AC WiFi ( I do not know about other devices!! ) they differ only by some subtle nuances. I am working with that exact module and have another variant on the... [Read more]
Hi, First of all, thanks for documenting your reverse engineering work. It has been very helpful. While reading through your protocol analysis, I noticed something that might be worth revisiting. In... [Read more]
Same protocol confirmed on Hisense Easy Smart (AEH-W4GX socket) ā ESP32 TX works, no response from AC Hi DeDaMrAz , Great work on the protocol decoding! I'm trying to replace the AEH-W4GX dongle... [Read more]
yes it does respond immediately. On the initialization part I don't know yet, I was preoccupied with decoding the control and status frames but I have completed a battery powered logger to capture... [Read more]
Thanks for confirming! Since my AC doesn't respond at all (not even with level shifting ā I'll confirm once my TXS0108E arrives), could you capture the boot sequence with your battery logger? If there's... [Read more]
Hi DeDaMrAz, all, Follow-up on my earlier post. Progress + a new dead end, sharing in case it helps someone else debugging the same connector. What I tried: Direct TTL connection (3.3V ESP32 ā... [Read more]