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
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
| Thing | Value |
|---|---|
| Emitter | Two SFH4045N, 950 nm, 39 Ω each, switched by a MOSFET from GPIO45. Active high, idle low |
| Receiver | TSOP75338, 38 kHz demodulator, output on GPIO46. Active low, already demodulated |
| Peripheral | RMT, transmit and receive, 1 MHz resolution: one tick is one microsecond |
| Carrier | A property of the transmit channel: 38 kHz, 33 % duty, on the mark. Sony wants 40 kHz |
| NEC timing | Leader 9000/4500 µs, bit 0 560/560, bit 1 560/1690, repeat 9000/2250. Decode with ±300 µs of margin |
| Protocols run | NEC standard and extended, Samsung32, Sony SIRC-12 on a board. RC5, RC6 and Kaseikyo are not written |
| Current | 147 mA peak while a mark is lit, about 12 mA averaged over a frame |
| Kode driver | None yet. Kode's nec_ir in the validation repository sends NEC |
| Validated | Kode-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){}, ©));
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
kdrv_ir: the infrared pair inkode-sdk.