If you are running a Kodi Raspberry Pi media center, you have likely experienced the frustration of HDMI-CEC dropping out, Bluetooth remotes going to sleep, or LibreELEC failing to map your remote's buttons correctly. While wiring an IR sensor directly to the Pi's GPIO used to be the standard fix, the transition to Raspberry Pi OS Bookworm and the Pi 5's new RP1 I/O chip have made microsecond-accurate pulse reading via Linux user-space nearly impossible due to OS scheduling jitter.

The professional embedded solution is to offload the IR decoding to a dedicated microcontroller. By building an ESP32-based IR bridge, you decode the raw 38kHz IR pulses in hardware and push clean JSON-RPC commands over Wi-Fi to your Kodi Raspberry Pi. This guide provides the exact hardware spec-sheet, the complete compilable firmware, and the debugging matrix for when the API refuses your connection.

Difficulty: Intermediate (Requires soldering, Arduino IDE, and basic networking)
Time to Build: 90 minutes
Target Board Variant: ESP32-WROOM-32 DevKit V1 (Code targets Arduino ESP32 Core v2.0.14+)

Hardware Specs & GPIO Pin Mapping

Before firing up the soldering iron, verify your components. The TSOP38238 is specifically chosen for its 38kHz carrier frequency, which matches 95% of modern consumer electronics remotes (NEC and RC5 protocols). Do not substitute a TSOP4838 without checking your remote's datasheet, as the internal bandpass filter will reject the signal.

Table 1: Component BOM and ESP32 Pin Mapping
Component Exact Variant / Value ESP32-WROOM-32 Pin Function / Notes
Microcontroller ESP32-WROOM-32 DevKit V1 (30-pin) N/A Handles Wi-Fi and IR decoding
IR Receiver TSOP38238 (38kHz, 2.5-5.5V) GPIO 15 Demodulated digital output (Active LOW)
Current Limiter 100Ω Resistor (1/4W) Between 3V3 and VCC Prevents inrush current spikes from browning out the ESP32
Decoupling Cap 4.7µF Electrolytic Capacitor Across VCC and GND Filters power supply noise from the IR LED bursts
Target Host Raspberry Pi 4 Model B or Pi 5 Network (Wi-Fi/Ethernet) Runs Kodi (LibreELEC, OSMC, or Pi OS)

Wiring & Assembly Steps

Follow these steps to wire the IR receiver to the ESP32. Keep the lead lengths between the TSOP38238 and the ESP32 under 3 inches to prevent the wires from acting as antennas for ambient RF noise.

  1. Prep the Sensor: Bend the pins of the TSOP38238 90 degrees so it faces forward when the ESP32 is laid flat.
  2. Solder the Filter: Solder the 100Ω resistor to the VCC (Pin 3) leg of the sensor. Solder the 4.7µF capacitor across the VCC and GND (Pin 1) legs. Observe the capacitor's polarity stripe.
  3. Connect to ESP32:
    • Sensor GND (Pin 1) → ESP32 GND
    • Sensor OUT (Pin 2) → ESP32 GPIO 15
    • Sensor VCC (via 100Ω resistor) → ESP32 3V3
  4. Flash the Firmware: Upload the code block below using the Arduino IDE. Ensure you have the ESP32 by Espressif Systems board manager installed and the IRremote library (v4.x) added via the Library Manager.
Bench Tip: If your IR sensor is picking up phantom pulses from fluorescent room lighting, wrap the body of the TSOP38238 in heat-shrink tubing, leaving only the front epoxy lens exposed. This blocks optical noise from the sides.

The Firmware: ESP32 IR-to-Kodi JSON-RPC Bridge

This firmware targets the ESP32-WROOM-32 DevKit V1. It uses the IRremote library to decode the NEC protocol natively in hardware interrupts, bypassing OS jitter. Once a button is decoded, it formats a JSON-RPC payload and fires an HTTP POST request to the Kodi JSON-RPC API v12 running on your Raspberry Pi.

#include <WiFi.h>
#include <HTTPClient.h>
#include <IRremote.hpp>

// --- PIN DEFINITIONS ---
#define RECEIVER_PIN 15  // TSOP38238 OUT pin connected to GPIO 15

// --- NETWORK & KODI CONFIGURATION ---
const char* ssid = "YourNetworkSSID";
const char* password = "YourNetworkPassword";
const char* kodi_ip = "192.168.1.50"; // Your Kodi Raspberry Pi IP
const int kodi_port = 8080;           // Default Kodi webserver port
const char* kodi_user = "kodi";
const char* kodi_pass = "kodi";

// --- NEC IR COMMAND MAPPINGS ---
// Replace these hex values with your specific remote's decoded NEC codes
#define IR_PLAY_PAUSE 0x18E7F00F
#define IR_STOP       0x18E7C837
#define IR_VOL_UP     0x18E7E01F
#define IR_VOL_DOWN   0x18E7906F

void setup() {
  Serial.begin(115200);
  
  // Initialize IR Receiver
  IrReceiver.begin(RECEIVER_PIN, ENABLE_LED_FEEDBACK);
  Serial.println("IR Receiver initialized on GPIO 15");

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected! IP: " + WiFi.localIP().toString());
}

void sendKodiRPC(const char* method, const char* params) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[ERROR] Wi-Fi disconnected. Attempting reconnect...");
    WiFi.reconnect();
    return;
  }

  HTTPClient http;
  String url = "http://" + String(kodi_ip) + ":" + String(kodi_port) + "/jsonrpc";
  http.begin(url);
  http.setAuthorization(kodi_user, kodi_pass);
  http.addHeader("Content-Type", "application/json");

  // Construct JSON-RPC 2.0 Payload
  String payload = "{\"jsonrpc\": \"2.0\", \"method\": \"" + String(method) + "\", \"params\": " + String(params) + ", \"id\": 1}";
  
  int httpCode = http.POST(payload);

  // --- ERROR HANDLING ---
  if (httpCode > 0) {
    if (httpCode == HTTP_CODE_OK) {
      Serial.println("[OK] Command accepted by Kodi.");
    } else if (httpCode == HTTP_CODE_UNAUTHORIZED) {
      Serial.println("[FATAL] HTTP 401 Unauthorized. Check kodi_user and kodi_pass.");
    } else {
      Serial.printf("[WARN] HTTP Response Code: %d\n", httpCode);
    }
  } else {
    Serial.printf("[FATAL] HTTP POST failed, error: %s\n", http.errorToString(httpCode).c_str());
  }
  http.end();
}

void loop() {
  if (IrReceiver.decode()) {
    uint32_t command = IrReceiver.decodedIRData.command;
    
    // Map IR commands to Kodi JSON-RPC methods
    switch (command) {
      case IR_PLAY_PAUSE:
        sendKodiRPC("Player.PlayPause", "{\"playerid\": 0}");
        break;
      case IR_STOP:
        sendKodiRPC("Player.Stop", "{\"playerid\": 0}");
        break;
      case IR_VOL_UP:
        sendKodiRPC("Application.SetVolume", "{\"volume\": \"increment\"}");
        break;
      case IR_VOL_DOWN:
        sendKodiRPC("Application.SetVolume", "{\"volume\": \"decrement\"}");
        break;
      default:
        Serial.printf("Unknown IR Command: 0x%08X\n", command);
        break;
    }
    IrReceiver.resume(); // Enable receiving of the next value
  }
}

Debugging: Connection Refused & CEC Failures

When integrating hardware with a Kodi Raspberry Pi, the failure points usually sit at the network boundary or the HDMI handshake layer. If your ESP32 serial monitor throws an error, use this decision matrix to isolate the fault.

The First Three Things to Check When It Fails

  1. Is the Kodi Webserver Actually Enabled? By default, Kodi's JSON-RPC API is disabled. Navigate to Settings (Gear Icon) → Services → Control, enable Allow control of Kodi via HTTP, and note the port (default 8080).
  2. Are You on the Same Subnet? If your Pi is on a 2.4GHz IoT VLAN and your ESP32 is on the 5GHz main network, AP isolation or firewall rules will drop the HTTP POST request.
  3. Did You Capture the Correct IR Hex Codes? Open the Arduino Serial Monitor at 115200 baud. Press a button on your remote. If the output says Unknown IR Command: 0x00000000, your TSOP38238 carrier frequency does not match your remote.
Table 2: Embedded Debugging Decision Matrix
Exact Error String (Serial / Log) Ranked Causes (Most Likely First) The Fix
[FATAL] HTTP POST failed, error: connection refused 1. Kodi webserver disabled.
2. Wrong IP address in firmware.
3. Pi firewall (iptables/ufw) blocking port 8080.
Enable HTTP control in Kodi Services menu. Verify IP via ifconfig on Pi. Whitelist port 8080.
[FATAL] HTTP 401 Unauthorized 1. Incorrect username/password in ESP32 code.
2. Kodi password changed but not updated in firmware.
Verify credentials in Kodi Settings → Services → Control. Update kodi_user and kodi_pass in C++.
CEC: [PERIPHERAL::CAdapterLinux] Failed to open the CEC device (Kodi Log) 1. TV turned on after Pi (CEC handshake missed).
2. Bad HDMI cable (missing Pin 13).
3. LibreELEC CEC daemon crashed.
Power cycle TV and Pi simultaneously. Swap to a certified High-Speed HDMI cable. SSH into Pi and run systemctl restart cec.
Unknown IR Command: 0x00000000 1. Ambient IR flooding the sensor.
2. Remote uses RC6 protocol (not NEC).
3. Sensor wired to wrong GPIO.
Shield sensor from sunlight. Check IrReceiver.decodedIRData.protocol in code. Verify wiring to GPIO 15.

For deeper HDMI-CEC debugging, always check the physical layer first. I have seen countless hours wasted on software configs when the actual issue was a cheap, unshielded HDMI cable lacking the CEC wire on Pin 13.

Extending vs. Simplifying the Build

Not every project needs to be built from scratch, and not every project should stay basic. Depending on your end goal, here is how you should adapt this Kodi Raspberry Pi remote bridge.

How to Simplify the Build

If you realized halfway through reading this that you do not want to solder capacitors or write C++ firmware, buy a FLIRC USB Dongle. It plugs directly into the Raspberry Pi's USB port, learns your remote's IR signals via a desktop app, and injects them into the Linux kernel as standard keyboard keystrokes. It costs about $25, requires zero coding, and completely bypasses the need for HDMI-CEC or JSON-RPC networking. Choose this route if your primary goal is simply watching media without troubleshooting.

How to Extend the Build

If you want to push this into a fully integrated smart home node, extend the ESP32 firmware with the following upgrades:

  • Add MQTT Integration: Include the PubSubClient library. When the ESP32 receives an IR command, publish it to an MQTT topic (e.g., homeassistant/media/kodi_remote). This allows Home Assistant to track physical remote usage and trigger automations (like dimming smart lights when the "Play" button is pressed).
  • Add an I2C OLED Display: Wire a 0.96" SSD1306 OLED to the ESP32's I2C pins (GPIO 21/22). Use the Kodi JSON-RPC Player.GetItem method to poll the "Now Playing" metadata and render the movie title and progress bar directly on the ESP32's screen, turning your remote into a smart display.
  • Deep Sleep Optimization: If you power the ESP32 via a lithium battery, use the ESP32's ULP (Ultra-Low Power) co-processor to monitor the IR pin. Put the main cores to sleep and wake them only when a 38kHz pulse train is detected, extending battery life from days to months.
Safety & Code Caveat: When exposing the Kodi JSON-RPC API to your local network, never port-forward port 8080 to the public internet. The API allows arbitrary file execution and system shutdown commands. Keep this traffic strictly behind your local router's NAT firewall.