An infrared receiver does not hand an ESP32 a universal “button code.” At the electrical boundary, it produces a sequence of detected carrier bursts and gaps. A raw capture represents those intervals as alternating mark and space durations, usually in microseconds.
A capture such as:
uint16_t rawData[] = {
3570, 1620,
554, 342,
498, 1244,
496, 378,
// ...
};is therefore a waveform description. Replaying it successfully depends on preserving four things together: the timing sequence, the correct frame boundary, the modulation carrier used for marks, and enough optical output from the IR LED.
This distinction matters when an ESP32 is used as an air-conditioner remote. AC remotes often send longer, state-oriented messages than simple appliance remotes, and one physical button press can produce more than one decoded capture if the receiver separates a transmission into multiple frames.
Raw timing is different from a decoded protocol value
A decoder may report both a protocol interpretation and raw timings:
PANASONIC 400407200000
address = 0x4004
data = 0x400407200000The decoded fields are useful when the protocol implementation is known. The raw array serves a different purpose: it records the observed mark-space durations closely enough that the transmitter can reproduce the waveform without reconstructing the protocol fields.
With IRremoteESP8266, a raw replay has the general form:
IRsend irsend(IR_TX_PIN);
void setup() {
irsend.begin();
irsend.sendRaw(rawData, rawDataLength, 38);
}The final argument is the carrier frequency in kHz. It is not another timing value from the array. A raw timing array and the carrier frequency are separate parts of the signal.
This is also why storing only a hexadecimal decoder result is not equivalent to storing a raw capture. A protocol-specific sender can turn decoded state back into a waveform when that protocol is supported, while sendRaw() simply replays the supplied timing sequence.
The first complete frame matters
A receiver can produce output that looks like this during one remote press:
Raw Timing[99]:
+3570, -1620, +554, -342, ...
PANASONIC 400407200000followed by another capture:
Raw Timing[71]:
+500, -372, +496, -376, ...
UNKNOWN ...Those two arrays should not automatically be treated as interchangeable commands. The first begins with a distinctive long leader and was recognized by the decoder. The second begins in the middle of short mark-space timings and may be a continuation, repeat, partial frame, or separately segmented portion of the transmission.
The practical rule is not “always use the longest array.” The useful capture is the complete frame that represents the intended transmission. Decoder output, repeated captures of the same button, and the presence of a stable leader pattern help identify that boundary.
A good validation process captures the same button several times. The durations will not be numerically identical on every capture because receivers, interrupt latency, oscillator tolerances, and demodulation introduce measurement variation. The structure should nevertheless remain recognizable: similar leader timing, similar number of intervals, and the same pattern of short and long spaces.
Raw arrays should be static when the command is fixed
If a captured command will never change at runtime, it does not need Preferences, EEPROM-style storage, or a filesystem. It can live directly in the firmware image:
const uint16_t RAW_ON[] = {
3570, 1620, 554, 342, 498, 1244, 496, 378,
// remaining timings
};
const uint16_t RAW_ON_LEN =
sizeof(RAW_ON) / sizeof(RAW_ON[0]);The command can then be transmitted directly:
irsend.sendRaw(RAW_ON, RAW_ON_LEN, 38);This has a useful operational property: a reboot cannot erase the command because the array is compiled into the firmware.
Runtime storage is appropriate for a different design, where the device must learn arbitrary remotes after deployment. In that case the firmware needs to store both the timing count and timing data, validate bounds before loading them, and retain any signal metadata required for replay.
Static and learned RAW storage solve different problems. Adding nonvolatile runtime storage to a fixed command only adds state and failure modes without changing the waveform.
MQTT should trigger the local command, not carry the waveform
For a network-connected remote, MQTT can remain independent of IR encoding. A small command topic is enough:
topic: ac/panasonic/cmd
payload: ONThe ESP32 maps that application command to a local raw array:
void callback(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) {
msg += static_cast<char>(payload[i]);
}
if (msg == "ON") {
irsend.sendRaw(RAW_ON, RAW_ON_LEN, 38);
}
}This boundary is useful. MQTT carries intent; the firmware owns the physical waveform. Broker clients do not need to know whether the device uses raw replay, a Panasonic encoder, or another protocol implementation.
Separate arrays can represent fixed commands:
if (msg == "ON") {
irsend.sendRaw(RAW_ON, RAW_ON_LEN, 38);
} else if (msg == "OFF") {
irsend.sendRaw(RAW_OFF, RAW_OFF_LEN, 38);
} else if (msg == "COOL") {
irsend.sendRaw(RAW_COOL, RAW_COOL_LEN, 38);
}The arrays must actually contain captures for those states. Aliasing RAW_OFF and RAW_COOL to RAW_ON only gives three MQTT names to the same physical transmission.
AC state makes raw replay intentionally rigid
Raw replay is exact but inflexible. If an AC remote sends its full state, a captured COOL 24 C frame may also encode fan speed, swing position, timer fields, or other settings. Replaying that frame later restores whatever state was captured in it.
That behavior is useful when the desired command is a fixed preset. It becomes awkward when MQTT needs dynamic commands such as:
TEMP:18
TEMP:24
FAN:AUTO
FAN:HIGHGenerating those combinations from raw captures requires a separate stored waveform for every required state. A protocol-specific AC class is a better fit when the protocol is supported because firmware can update fields and let the encoder construct the resulting frame.
Raw replay and protocol encoding can therefore coexist in a design, but they should not be confused. Raw replay says “reproduce this waveform.” A protocol encoder says “construct a waveform for this state.”
The IR LED needs a driver stage
Correct timings do not guarantee that the indoor unit receives them. An ESP32 GPIO is a logic output, while an IR LED is a current-driven optical load. A transistor or suitable MOSFET can let the GPIO switch the LED current without forcing the GPIO itself to supply the transmit pulse current.
A typical NPN low-side arrangement is:
+5 V
|
current-limit
resistor
|
IR LED
|
C
ESP32 GPIO--R_B--B NPN
E
|
GNDThe emitter and collector are not interchangeable in normal operation. The transistor pinout must be checked for the exact part and package; TO-92 devices with similar shapes do not share one universal E-B-C order.
The resistor values also have different jobs. The base resistor limits GPIO-to-base current. The LED resistor limits LED current. Giving both resistors the same value simply because both are resistors is not an electrical design rule.
For example, the LED resistor should be selected from supply voltage, LED forward voltage, driver voltage drop, desired pulse current, and the LED’s pulse ratings:
R ≈ (V_supply - V_F - V_driver) / I_LEDThe base drive must then be sufficient to switch the transistor at that collector current without exceeding the GPIO’s electrical limits. Component datasheets, not resistor color availability, set those boundaries.
Receiver and transmitter can coexist, but self-reception needs attention
A transceiver build may keep an IR receiver on one GPIO and an IR LED driver on another:
const uint16_t IR_RX_PIN = 4;
const uint16_t IR_TX_PIN = 9;
IRrecv irrecv(IR_RX_PIN);
IRsend irsend(IR_TX_PIN);When the transmitter fires, the nearby receiver may see the ESP32’s own IR output. Firmware that captures every received frame can consequently log or store its own transmission as though it came from the handheld remote.
If learning and sending are both enabled, the design should define when capture is allowed. A simple implementation can suspend or ignore reception around a local transmission; a more elaborate one can compare timestamps or expected frames. The important point is that physical proximity creates a feedback path even though the software objects are separate.
Raw replay is a waveform contract
The reliable unit of storage is not a friendly label such as ON. It is the complete signal contract behind that label:
command name
|
v
complete mark/space array
+
carrier frequency
+
known frame boundary
|
v
IR transmitter driver
|
v
optical signalWhen those pieces stay aligned, MQTT can remain a thin control layer and the ESP32 can replay a captured command deterministically. When one piece is wrong, the failure can look deceptively similar: the MQTT message arrives, the code calls sendRaw(), and the AC still does nothing.
That is why raw IR debugging is best split by boundary. First verify that the intended complete frame was captured. Then verify that the stored array is unchanged. Then verify carrier and transmitter output. Only after those layers work does the network trigger matter.