Protocol Overview

A developer’s orientation to the Hisense A/C RS-485 bus protocol: what the frames look like, which mistakes have already been made, and where the full reference is. This page is a summary. The byte reference is reverse-engineering/docs/03-rs485-ac-protocol.md (“doc 03” below), and the vendor firmware’s bus handling is in reverse-engineering/docs/10-stock-fw-init-and-comms.md (“doc 10”).

The protocol is hardware-validated: trust the repo values

Framing, checksum, and every byte offset were sniff-confirmed against the real W41H1 bus (command + status) by remote-button correlation. The offsets originally borrowed from the older AEH-W4A1 reference project were wrong in several places and are corrected in this repo. Do not re-derive or “fix” them from reference or other-model docs. The executable source of truth is the codec in the ESPHome component, firmware/esphome/components/hisense_ac/hisense_protocol.cpp, and the hardware-validated reference driver it is tested against, firmware/test/reference-driver/hisense_rs485.cpp (hisense_parse_status / hisense_build_command).

Physical layer

   
Transport RS-485, 9600 8N1 (auto-probed against 115200 by stock; the link is 9600)
Pins (module) UART0 TX = PA_14, RX = PA_13; DE = PA_17
Role Wi-Fi module is bus master / sole initiator; the A/C mainboard is a pure slave/responder

Two facts about DE differ between the RE docs, and both are correct in context. Doc 03 notes the transceiver on the stock module is effectively auto-direction (no gpio_* in the UART path it first analysed), while the deeper stock-firmware trace (doc 10) proves the stock driver does bring up a DE line on PA_17 and toggles it around every TX (5 ms pre-guard, 1 ms post-guard). This project’s firmware drives DE on every board: 5 ms of settle before the first byte, and the frame’s wire time plus 25 ms before release.

Frame envelope

The module↔A/C frame is F4 F5 … F4 FB delimited. The full envelope the stock firmware uses (doc 10 §3.2):

F4 F5 | DIR | CTRL | LEN | 00 00 | seqHi seqLo | FE 01 | 00 00 | CLASS | payload… | CKSUM | F4 FB
Field Meaning
F4 F5 / F4 FB start / end markers
DIR 0x00 module→A/C, 0x01 A/C→module (RX rejects !=1)
CTRL 0x40 normal. bit6 → 2-byte checksum; bit5 → 16-bit BE length; bit0 → extra pre-LEN byte
LEN payload count; total frame = LEN + 9 (this fixed the old fixed-82 assumption that dropped every 160-byte frame)
seqHi/Lo the A/C’s device-type / sub-type, 00 00 until the DevType reply answers, that unit’s code after (below)
CLASS message class (below)
CKSUM big-endian running SUM over the payload, width from CTRL & 0xC0 (0x40 → 2 bytes). Not XOR, not a CRC.

Checksum + byte-stuffing: all ac_* frames use CTRL=0x40 → 2-byte checksum. A checksum byte equal to the 0xF4 marker is escaped by doubling it (… F4 → … F4 F4 …). The builder stuffs on TX; the bus task un-stuffs on RX. A frame whose checksum never contains 0xF4 never exercises this path, a known test trap.

The seqHi/Lo bytes are a device-type, not a session token

Read this before touching the framing. Getting it wrong once cost a working A/C.

Those two envelope bytes look like a per-session token, and the stock disassembly can be read that way. They are the attached unit’s device-type / sub-type: a static identifier for that model. The module sends 00 00 until the A/C answers the DevType (0x0A) handshake, then sends the code that reply carries, on every later frame.

Read the code from the DevType reply’s inner payload [3]/[4] (frame bytes [16]/[17]). Do not read that reply’s envelope [9]/[10].

Measured on a live unit, both values from the same 0x0A reply:

device-type  inner [3]/[4] : 01 01   <- send this
session tok  envel [9]/[10]: 00 00   <- NOT this

One build stamped the envelope [9]/[10] instead. It read 00 00, treated that as “no token yet”, and sent 00 00 on every frame after the handshake. The A/C rejected all of them: the link never came up, no status arrived, and the unit needed a firmware update to recover.

A 0x66 status reply does carry 01 01 at [9]/[10], which is what makes the token reading look right. The DevType reply is the one that differs, and it is the frame that matters.

01 01 is one model’s code. A different Hisense model answers with a different code, which is why the driver learns it rather than hardcoding it. Details and the per-model capability map: reverse-engineering/docs/11-model-capability-map.md.

Message classes

Class Name Purpose
0x0A DevType First handshake; identify device type, open session. Must complete before status/command frames are honored.
0x07 Version Exchange MCU/proto version; gates legacy-compat fields
0x1E (num30) Link/status heartbeat ~1 Hz. Module reports its Wi-Fi/commissioning state; A/C replies with request bits; this is the “77” channel (below)
0x66 sub 00 Status poll Live A/C state → the 160-byte status reply
0x66 sub 40 ProductType Model code + decoded feature bit-flags
0x65 Control / set The command write (fan/mode/temp/…)
0x1D (num29) Reconfig / OTA A/C demands re-provisioning / OTA reboot
0x67 not decoded The stock module sends it after each ProductType exchange, about once a minute. Seen only on a bench against a synthetic A/C (issue #110). Our driver never sends it and ignores it

Provenance: only the envelope is firmware-confirmed from the stock binary. The stock module treats temps/fan/compressor as a bit-packed + transparent block it forwards to cloud without field-decoding. The byte-per-field maps below were confirmed a different way: on the live bus, by toggling each control and diffing the reply. Both are in doc 03.

Status vs command maps (summary)

Two directions, two different frames. Full tables (with the confirmed hex per field) in doc 03.

Status: 160-byte class-0x66 reply (hardware-confirmed by remote-button diffing):

Byte Field
16 fan speed (six speeds, more than the reference’s three)
18 mode: three bits, 4 to 6 (AUTO = 5 in status). Bit 7 is not part of the mode; the parsers mask it off
19 setpoint °C (direct integer)
20 room temp °C (direct integer, not the W4A1 (raw-32)*0.5556)
35 flags1: vswing / hswing / aux-heat / eco / turbo
36 flags2: purify / mute
41–45 compressor freq + outdoor/coil temp (telemetry)

Command: 50-byte class-0x65 frame (DI-tap-confirmed on the stock dongle):

Byte Field Note
16 fan ref’s 0x05/07/09 were WRONG; fan*2+1
18 mode, and power in the low nibble AUTO = 0x90 in command, which differs from status value 5. The stock module ORs power-on 0x0C into the same byte (0x5C cool, 0x3C heat), so power and mode travel in one frame
19 temp 2n+1, whole °C
23 beeper bit 2 (0x04), the stock t_beep. Set: the unit beeps when it takes this frame. Every builder sets it; the Beeper switch clears it
32 vertical swing 0xC0 swing / 0x40 fixed
33 eco (0x30/0x10) / turbo (0x0C/0x04) ref values were wrong

Power and mode in one frame. The firmware sends a mode request to a unit that is off as one frame with 0x0C in byte 18, as the stock module does. 0x5C and 0x3C are in the stock image. The values for auto, dry and fan-only (0x9C, 0x7C, 0x1C) follow from the same field rule and are marked VERIFY in the source. If the unit is still off at the check, the node falls back to a power-on frame followed by a mode frame. Evidence and file offsets: doc 10 section 5b-2.

Horizontal swing is N/A: no motor on this unit; the bit toggles but nothing moves. Only vertical swing is offered by default (supports_horizontal_swing turns the other on for a variant that has the motor).

Entity ↔ RS-485 mapping

The component translates climate, switch and select writes to A/C fields and status frames back to entity state. The mapping and every decision around it (shadow sync, confirm and retry, preset planning) are pure functions in the component’s headers under firmware/esphome/components/hisense_ac/ (hisense_mapping.h, hisense_special.h, hisense_confirm.h, hisense_entities.h), all host-tested.

Home Assistant A/C concept
climate mode, off mode field (cool / heat / auto / dry / fan), power bit
current temperature indoor temp sensor
target temperature setpoint, whole degrees, 16 to 32 °C, cool and heat only
fan mode, swing mode six-step fan ladder, vertical swing
eco / turbo / quiet / sleep presets and switches the special-mode bytes, sent 10 s apart

The “77” request

There is no dedicated reset frame. The remote’s Horizon Airflow ×6 → “77” is a remote→mainboard key sequence; the mainboard signals the module by setting a request bit in its reply to the ~1 Hz 0x1E heartbeat. Static RE inferred bit3 (0x08); the hardware truth is that “77” pulses payload[4] = 0x20 (bit5, smart-config) for a single ~1 Hz frame, then clears. Conversely, the A/C lights “77” iff the module reports prov_status in its outbound 0x1E, so the display tracks the module’s pairing state. Both directions are proven on hardware. Full bitfields and the outbound builder are in doc 03, section Recommission ("77"): the request bits in the 0x1E reply.

The vendor firmware uses the request to reset Wi-Fi and start pairing. This firmware has no pairing step. Its component reports the request as the on_setup_request trigger and can light “77” with the hisense_ac.set_setup_indicator action, and the shipped configuration binds the two to Wi-Fi setup mode: the node opens its hotspot and captive portal, and “77” stays on the panel until setup mode ends (Wi-Fi Setup Mode). 0x20 is taken at once, 0x08 has to hold for three replies, and a second request within 10 s is ignored. The reference driver under firmware/test/reference-driver/ still carries the first handler (edge-triggered on 0x08|0x20, with a re-entry lockout).

Bring-up ordering on Realtek’s SDK

This applies to firmware written directly on Realtek’s SDK for the stock module. Under ESPHome the platform does the bring-up. Initialise the DE GPIO (PA_17) and the UART from a task, after the RTOS scheduler and the SDK HAL are up, never from a static constructor. Doing it early faults through unpopulated HAL tables, which was a boot fault in an early build. The vendor firmware’s exact ordering (mutex first, a persistent gpio_t, the DE toggle sequence) and the root-cause analysis are in doc 10.

The two references

  • reverse-engineering/docs/03-rs485-ac-protocol.md: the full frame format, the checksum, every confirmed byte map, the telemetry block, the timer encoding and the “77” bitfields.
  • reverse-engineering/docs/10-stock-fw-init-and-comms.md: the vendor firmware’s init, bus bring-up, the DE pin, the link state machine, the transaction primitive, and the analysis of the DE pin against the flash pins.

Back to top