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