Kode Dot

Infrared

The infrared emitter and receiver on a Kode Dot, both on the RMT peripheral: what a remote's frame looks like, how the Kode Dot hears itself, and the code to send one.

What it is

Infrared is how a TV remote talks: a light you cannot see, switched on and off in a pattern, at a carrier the receiver is tuned to. The Kode Dot has both halves. Two emitters send, so it can be the remote; a receiver listens, so it can learn a remote's codes or take commands from one.

The P4's RMT peripheral is built for exactly this. You hand it a list of on and off durations in microseconds and it adds the 38 kHz carrier on the way out; the receiver hands it back marks and spaces with the carrier already stripped. Because both halves are on board, the Kode Dot can hear what it sends, which is the fastest test you have.

How it is connected

flowchart LR
p4["ESP32-P4"] -- "GPIO45, RMT TX" --> q["MOSFET"] --> tx["2× SFH4045N, 950 nm"]
rx["TSOP75338, 38 kHz"] -- "GPIO46, RMT RX" --> p4
Emitters and receiver on two GPIOs, both driven by the RMT peripheral

The two SFH4045N emitters are switched through a MOSFET from GPIO45, active high, with 39 Ω each to the battery rail. The TSOP75338 receiver demodulates 38 kHz and drives GPIO46 low for each mark. No enable, no reset, no interrupt: two pins and nothing else.

Specifications

ThingValue
EmitterTwo SFH4045N, 950 nm, 39 Ω each, switched by a MOSFET from GPIO45. Active high, idle low
ReceiverTSOP75338, 38 kHz demodulator, output on GPIO46. Active low, already demodulated
PeripheralRMT, transmit and receive, 1 MHz resolution: one tick is one microsecond
CarrierA property of the transmit channel: 38 kHz, 33 % duty, on the mark. Sony wants 40 kHz
NEC timingLeader 9000/4500 µs, bit 0 560/560, bit 1 560/1690, repeat 9000/2250. Decode with ±300 µs of margin
Protocols runNEC standard and extended, Samsung32, Sony SIRC-12 on a board. RC5, RC6 and Kaseikyo are not written
Current147 mA peak while a mark is lit, about 12 mA averaged over a frame
Kode driverNone yet. Kode's nec_ir in the validation repository sends NEC
ValidatedKode-Dot-Val/10-IR: self-echo on the board and a Flipper Zero cross-check

Gotchas

Apps that use it

On the marketplace, apps that put this part to work. Install one from start/first-app and see it before you write a line.

Example code

Send one NEC frame, the format most TV remotes use. Point a phone camera at the front of the Kode Dot and the emitters flash faintly; a TV that speaks NEC address 0x00 reacts to command 0x15.

/*
 * infrared_nec.c - 2026-09-18 - from Kode with love
 *
 * Sends one NEC frame from the Kode Dot's infrared emitter: address 0x00,
 * command 0x15, the way most TV remotes frame a key press. Point a phone
 * camera at the front of the Kode Dot and the emitters flash faintly; the
 * receiver on the same board decodes the frame straight back.
 */
#include "driver/rmt_tx.h"
#include "driver/rmt_encoder.h"
 
#define IR_TX_GPIO 45
 
/* One NEC bit: a 560 µs mark, then a 560 µs space for 0 or 1690 µs for 1. */
static rmt_symbol_word_t nec_bit(bool one)
{
    return (rmt_symbol_word_t){ .level0 = 1, .duration0 = 560, .level1 = 0, .duration1 = one ? 1690 : 560 };
}
 
/* The 34-symbol frame: leader, address, ~address, command, ~command, stop. */
static void nec_frame(rmt_symbol_word_t *out, uint8_t address, uint8_t command)
{
    *out++ = (rmt_symbol_word_t){ .level0 = 1, .duration0 = 9000, .level1 = 0, .duration1 = 4500 };
    uint32_t payload = address | (uint32_t)(uint8_t)~address << 8
                     | (uint32_t)command << 16 | (uint32_t)(uint8_t)~command << 24;
    for (int i = 0; i < 32; i++) *out++ = nec_bit((payload >> i) & 1);
    *out = (rmt_symbol_word_t){ .level0 = 1, .duration0 = 560, .level1 = 0, .duration1 = 0x7FFF };
}
 
/* A 1 MHz RMT channel with a 38 kHz carrier on the mark, then one frame. */
void app_main(void)
{
    rmt_channel_handle_t channel;
    rmt_tx_channel_config_t cfg = { .clk_src = RMT_CLK_SRC_DEFAULT, .gpio_num = IR_TX_GPIO,
                                    .mem_block_symbols = 48, .resolution_hz = 1000000, .trans_queue_depth = 1 };
    ESP_ERROR_CHECK(rmt_new_tx_channel(&cfg, &channel));
    rmt_carrier_config_t carrier = { .frequency_hz = 38000, .duty_cycle = 0.33 };
    ESP_ERROR_CHECK(rmt_apply_carrier(channel, &carrier));
    rmt_encoder_handle_t copy;
    ESP_ERROR_CHECK(rmt_new_copy_encoder(&(rmt_copy_encoder_config_t){}, &copy));
    ESP_ERROR_CHECK(rmt_enable(channel));
 
    rmt_symbol_word_t frame[34];
    nec_frame(frame, 0x00, 0x15);
    ESP_ERROR_CHECK(rmt_transmit(channel, copy, frame, sizeof frame, &(rmt_transmit_config_t){}));
}

Compiled on the date in the header with ESP-IDF v6.1. The timings and the carrier are the ones Kode's validation ran on a board, where a Flipper Zero read the frame as NEC A:00 C:15.

In the SDK