Skip to content

Built-in A/C library: 66 protocols generated on demand, including a reverse-engineered Dawlance - #7

Merged
hammad42 merged 5 commits into
mainfrom
feat/ac-library
Sep 4, 2026
Merged

hammad42 merged 5 commits into
mainfrom
feat/ac-library

Conversation

@hammad42

@hammad42 hammad42 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

What this changes

Air conditioners are the reason this device is annoying to set up. An A/C remote
never sends "temperature up" -- every button transmits the unit's entire
state
, so teaching one by hand means capturing every temperature separately,
and you still cannot use a combination you never pressed. Thirteen captures for
one mode.

This adds a built-in library that generates the frames instead.

66 A/C protocols. The 65 IRremoteESP8266 supports through IRac (Gree,
Coolix, Midea, Daikin, Electra, Fujitsu, Haier, Hitachi, LG, Samsung, TCL,
Toshiba, Argo, Airton…) plus DAWLANCE, implemented here. Pick a brand, set
mode / temperature / fan, and the device encodes the frame with the correct
checksum. A "save a whole temperature range" action creates one command per
degree in a single click.

Dawlance, reverse engineered. Not supported upstream -- its 72-bit frame
over a 6.7/3.3 ms header matches nothing in IRremoteESP8266, which is why those
captures decode as UNKNOWN. Derived from 18 captures off a real unit:

AA 11 <mode|power|turbo> <temp-16> 00 <flags> 00 00 <checksum>

byte 0,1   constant AA 11
byte 2     bits 0-2 mode, bit 3 power (09 on / 01 off), bit 7 turbo
byte 3     temperature - 16          (16 -> 0x00 … 28 -> 0x0C)
byte 5     base 0x44, bit 7 display light, bit 0 economy
byte 8     checksum = sum(bytes 0..7) XOR 0xAA

Preset packs in data/presets/, for remotes that cannot be generated at
all. The browser fetches a pack and posts each command to /api/import -- the
same path a restore takes -- so adding a device to the library needs no
firmware change
, only a new file and a line in index.json.
tools/make-preset.sh builds one from any backup.

Why

Generating beats shipping a code database, and not marginally. A database keyed
by combination needs thousands of entries per brand; the encoders already know
the frame layout and checksum, so one brand covers everything including
combinations nobody captured.

Storage follows the same reasoning. A generated command holds the standard
stdAc::state_t struct rather than timings -- 56 bytes against ~1.2 KB --
and is replayed by handing the struct back to the encoder, so the checksum stays
the library's rather than a reconstruction of it, and a library upgrade improves
existing commands for free. This needed no change to the store: the struct
rides in the existing payload, one byte per entry, flagged IR_FLAG_AC_STATE.

Pulling the encoded bytes out instead would have meant a switch with a case per
protocol; one struct covers all sixty-six.

Cost: 5.6 % of flash (68.2 → 73.8 % on esp32dev), leaving 26 % headroom.

How it was tested

  • All four targets build, zero warnings: esp32dev 73.8 %, esp32dev_oled 75.8 %, esp32s3 35.0 %, esp32c3 75.6 %
  • Web UI exercised in a browser against the mock: live preview follows every control, and the range action created four commands into the Air Conditioner group in one click
  • Tested on hardware — ESP32-D0WDQ6, VS1838B receiver, 2N2222-driven LED, updated over the air:
    • 66 protocols listed; preview / send / save all work
    • a generated Gree command stores as 56 bytes and sends through the normal command path (tx: 'Gree Cool 24' (generated GREE))
    • out-of-range temperatures and unknown protocol names rejected with useful messages
    • Dawlance checksum: 18 / 18 captures agree
    • Dawlance encoder vs captures: byte-identical for all 13 temperatures plus power on, power off, eco, lamp off and turbo
  • Documentation updated — API.md (endpoints, DAWLANCE, preset packs), USAGE.md (Library tab, placed before Learning)
  • CHANGELOG.md updated under Unreleased

Notes for the reviewer

One field is a guess, and is labelled as one. Every Dawlance capture was
taken in cool, so only mode = 1 is measured. Heat, dry, fan and auto follow
the ordering these controllers usually use. src/ir_library.cpp marks it
INFERRED, and NOT verified at the point of use, and the docs repeat it. If a
mode turns out wrong, capturing that button still yields a correct raw command.

I refused to ship this generator earlier in the work because the checksum
was only half derived, and a wrong checksum produces commands that fail
silently. That objection is settled — the XOR form checks against 18 of 18
captures and the encoder reproduces every one byte for byte, on the device, over
the API. Everything except the mode field is measured rather than assumed.

The light default is deliberately protocol-specific. Every Dawlance
capture carries the display bit set, so it has to default on for that protocol
or generated frames would differ from the remote. The first version applied that
to all 66, which would have switched other people's displays on unasked; it is
now scoped to Dawlance alone.

HANDOFF.md moves onto this branch and supersedes the copy on
docs/handoff. That branch should be deleted rather than merged.

🤖 Generated with Claude Code

hammad42 and others added 5 commits September 4, 2026 14:03
Teaching an air conditioner by hand means capturing every temperature
separately, because these remotes do not send "temperature up" -- every
button transmits the unit's entire state. Thirteen captures for one mode,
and you still cannot use a combination you never pressed.

IRremoteESP8266 already knows the frame layout and checksum of ~65 of these
protocols through IRac. This exposes that: pick a brand, set mode,
temperature and fan, and the device encodes the frame itself. A "save a
temperature range" action creates the whole set in one action, which is the
part that replaces an afternoon at the receiver.

Storage

  A generated command holds the standard stdAc::state_t struct rather than
  timings -- ~40 bytes against ~1.2 KB -- and is replayed by handing the
  struct back to the library. So the checksum stays the library's rather
  than our reconstruction of one, and a library upgrade improves existing
  commands for free.

  This needs no change to the store: the struct rides in the existing
  payload, one byte per entry, flagged IR_FLAG_AC_STATE. Its size is
  recorded so a future layout change is reported rather than transmitted as
  nonsense. Backup carries the flag, or a restore would replay a state
  struct as if it were timings.

  Pulling the encoded bytes out instead would have meant a switch with a
  case per protocol; one struct covers all sixty-five.

Everything else about them is ordinary: they appear under Remotes, work
with schedules and MQTT, and survive backup and restore. Protocols the
library does not know -- the Dawlance unit this was developed against among
them -- are unaffected and still captured as raw timings.

Costs 5.5 % of flash (68.2 -> 73.7 %), leaving 26 % headroom.

Verified on hardware: 65 protocols listed, preview/send/save all work, a
generated Gree command stores as 56 bytes and sends through the normal
command path, out-of-range temperatures and unknown protocol names are
rejected with useful messages. Verified in a browser: live preview follows
every control, and the range action created four commands into the Air
Conditioner group in one click.

Co-Authored-By: Claude Opus 5 <[email protected]>
The library listed 65 protocols and Dawlance was not among them, because
IRremoteESP8266 does not know it -- its 72-bit frame over a 6.7/3.3 ms
header matches nothing there. Adding it as a preset pack of captured codes
worked, but only for the buttons that had been captured; the whole point of
the library is that a brand covers combinations nobody pressed.

So it is now encoded natively, from the 18 captures taken off the unit:

    AA 11 <mode|power|turbo> <temp-16> 00 <flags> 00 00 <checksum>

    byte 0,1  constant AA 11
    byte 2    bits 0-2 mode, bit 3 power, bit 7 turbo
    byte 3    temperature - 16
    byte 5    bit 7 display light, bit 0 economy
    byte 8    sum(bytes 0..7) XOR 0xAA

Earlier in this work I declined to ship a Dawlance generator because the
checksum was only half derived, and a wrong checksum produces commands that
fail silently. That objection is now settled: the XOR form was worked out
and checks against 18 of 18 captures, and the full encoder reproduces every
one of them byte for byte, on the device, over the API.

One field remains a guess and is marked as such in the source: every
capture was taken in cool, so only mode = 1 is confirmed. Heat, dry, fan and
auto follow the ordering these controllers usually use. If they do not work
on a given unit, capturing that button still gives a correct raw command.

Also fixes a default that leaked: the display bit is set in every Dawlance
capture, so light defaults on for that protocol -- but the first version
applied that to all 66, which would have switched displays on unasked. It
is now scoped to Dawlance alone.

Verified on hardware after OTA: DAWLANCE appears in the picker (66
protocols), and generated frames match the captures for all thirteen
temperatures plus power on, power off, eco, lamp off and turbo.

Co-Authored-By: Claude Opus 5 <[email protected]>
The first version was written before the built-in library existed and
describes a device with six commands and no code generation. Rewritten from
section 3 onward; the hardware and network sections are unchanged because
the hardware is.

What is new in it:

  - a branch table, because there are now three and two are unmerged. This
    file moves to feat/ac-library, which supersedes docs/handoff, so that
    branch should be deleted rather than merged.
  - a section on the built-in library: what it solves, why generating beats
    storing a code database, and how a generated command is stored as a
    state struct rather than timings.
  - the full Dawlance protocol -- byte layout, timings, the checksum, and
    the verification table showing 18/18 on the checksum and byte-identical
    output against every capture. Written down so it survives this
    conversation.
  - the one field that is a guess (mode, only cool was ever captured) called
    out both here and at the top of the task list.
  - the task list reordered by value rather than by area, since the next
    session should start with the unverified modes and the range
    measurement.

Also records two things that bit us and would bite again: schedules have no
backup path, and the shell has now eaten an escape three separate ways --
including a single-quoted perl string that collapsed a comment block onto
one line, swallowed the statement after it, and still compiled.

Identifying values stay redacted; the file says how to read the real ones
back off the device.

Co-Authored-By: Claude Opus 5 <[email protected]>
The PR template asks the author to confirm docs and a changelog entry, and
neither mentioned any of the work after the first library commit.

- CHANGELOG: the Dawlance protocol with its byte layout and checksum, plus
  the preset-pack mechanism. States plainly which part is verified against
  hardware and which single field is inferred.
- API.md: notes that the protocol list is the library's 65 plus DAWLANCE and
  why that one is local, and documents preset packs as static files
  installed through /api/import.
- USAGE.md: a Library section placed *before* Learning, because for an air
  conditioner it is what someone should try first -- capturing thirteen
  temperature buttons is the thing this replaces.

Also fixes a stray line break that split the ir-backup.sh entry mid-sentence.

Co-Authored-By: Claude Opus 5 <[email protected]>
@hammad42
hammad42 merged commit 38f5604 into main Sep 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant