Skip to content

Bug in compress_pulses lead to signal consolidation #5

Description

@pedrib

I'm using my clanker to control an AC, and we found a bug while attempting to control a TCL AC (2024 model). I've asked it to do a write-up, and here it is:

Summary

server.py:175 uses max(0, pat[i+1]) to process IR pulse arrays, converting all space (gap) durations to 0µs. This collapses distinct commands from pulse-distance protocols (NEC, Sony SIRC, RC-5, etc.) into identical compressed output.

Root Cause

In _compress_pulses() (server.py, approximately line 174-176):

for i in range(0, len(pat) - 1, 2):
    on = max(0, pat[i])      # mark — positive, preserved
    off = max(0, pat[i+1])   # space — BUG: negative values → 0

IR pulse arrays use alternating positive (mark) and negative (space) values from the learn handler:

signed = [t if i%2==0 else -t for i,t in enumerate(timings)]
# Result: [3120, -1568, 528, -288, 528, -1056, ...]

max(0, -288) → 0, max(0, -1056) → 0. Every space becomes 0 regardless of duration. Since pulse-distance protocols encode information in space duration (e.g., short=0, long=1), this destroys the bit stream.

Reproduction

Tested with TCL AC remote on D552 variant (ElkSMART VID 045C, PID 0195):

import json
from server import encode_ir  # or irblaster in refactored version

# Pre-fix: four distinct codes produce identical compressed output
for name in ['Fan_Speed_Min', 'Fan_Speed_Turbo', 'Temp_19', 'Temp_26']:
    d = json.load(open(f'codes/{name}.json'))
    frames = encode_ir(38000, d['pulses'], 'd552')
    sig = bytes(b for f in frames for b in f)[10:25].hex()
    print(f'{name:20s} {sig}')

# Output (pre-fix):
# Fan_Speed_Min        01002100ffffff0100000000000000
# Fan_Speed_Turbo      01002100ffffff0100000000000000  ← same
# Temp_19              01002100ffffff0100000000000000  ← same
# Temp_26              01002100ffffff0100000000000000  ← same

Impact

Any pulse-distance IR protocol is affected when used with the D552 variant. The compression collapses codes that differ only in space timing, making it impossible to:

  • Send absolute temperature commands to AC units (only toggle works)
  • Distinguish related commands (e.g., fan speed 1 vs fan speed 2)
  • Use any remote that encodes data in gap duration

Four of nine TCL AC codes collapsed into one compressed signature. The same would happen with NEC, Sony SIRC, RC-5, and other common protocols.

Fix

One line at server.py:175:

# Before:
off = max(0, pat[i+1]) if i+1 < len(pat) else 10000

# After:
off = abs(pat[i+1]) if i+1 < len(pat) else 10000

Verification (Post-Fix)

D552 firmware confirmed to accept non-zero space values (ACK on all codes). Post-fix, all 8 codes produce unique compressed output:

Power_On              122012ffffffc20161214220420000
Power_Off             422012ffffffc201621f431f431f13
Fan_Speed_Min         122111ffffffc30161214121410101
Fan_Speed_Turbo       122111ffffffc30161214121420100
Fan_Speed_Variable    122111ffffffc30162204220422012
Temp_19               122111ffffffc30160214121410000
Temp_24               122111ffffffc30160214121410101
Temp_26               122111ffffffc30161214121410000

All verified working on live TCL AC unit.

Additional Context

  • max(0, x) is a sign-handling anti-pattern when x can be negative — abs() is the correct primitive
  • The D552 protocol format supports non-zero space values; the firmware handles them without issue
  • This affects the D552 variant specifically (identified by handshake byte 0x70 0x01). D226 variant uses Huffman encoding path and may or may not be affected
  • Full postmortem and investigation: available on request

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions