Issues Configuring I2C as Slave with ESPHome and How I Solved Them

Hello everyone,

I faced some challenges when configuring I2C as a slave on ESPHome to communicate with other devices using an ESP32. After several trials and debugging, I managed to resolve the issues, and I wanted to share my experience in case it helps anyone else.

Problem:

In my project, I needed to configure ESPHome so that ESP32 would act as an I2C slave and receive data through the I2C protocol. However, I encountered several obstacles:

  1. Including the Wire library: I had to manually include the Wire library since ESPHome by default uses I2C as a master.
  2. Configuring I2C as a slave: I had to avoid declaring I2C as a master by using the standard i2c: configuration in the YAML file.
  3. Handling received data: I created a custom library to receive data using the onReceive method, as the available solutions didn’t fit my needs perfectly.

Solution:

I created a custom library and configured my YAML file to handle data reception through the onReceive callback. Below, you can find both the code for the library and the ESPHome YAML configuration I used.

Custom Library Code (config/esphome/deauther_i2c_receiver.h):

#include "esphome.h"
#include <Wire.h>

class MyI2CReceiver : public Component {
  public:
    MyI2CReceiver(text_sensor::TextSensor *sensor) {
      this->sensor_ = sensor;
    }

    // Static function for the callback
    static void receiveEventStatic(int numBytes) {
      if (instance_ != nullptr) {
        instance_->receiveEvent(numBytes);  // Calls the non-static method
      }
    }

    void receiveEvent(int numBytes) {
      ESP_LOGD("custom", "%s", "receiving");
      char message[32];  // Buffer for the message
      int index = 0;

      while (Wire.available() > 0 && index < sizeof(message) - 1) {
        message[index] = Wire.read();  // Reads I2C data
        index++;
      }
      message[index] = '\0';  // String terminator

      if (strlen(message) > 0) {
        ESP_LOGD("custom", "%s", message);
        this->sensor_->publish_state(message);  // Publishes the data via MQTT
      }
    }

    void setup() override {
      // Initialize I2C in slave mode
      Wire.begin(0x08);  // Slave address
      Wire.onReceive(receiveEventStatic);  // Callback for receiving data
      instance_ = this;  // Assign the current instance
    }

    void loop() override {}

  protected:
    uint8_t address_;
    text_sensor::TextSensor *sensor_;

    static MyI2CReceiver *instance_;  // Static pointer to the class instance
};

// Define the static instance
MyI2CReceiver *MyI2CReceiver::instance_ = nullptr;

ESPHome YAML Configuration:

esphome:
  name: deauth-detector
  friendly_name: deauth_detector
  includes:
    - deauther_i2c_receiver.h  # Custom file to include
  libraries:
    - "Wire"   
esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: arduino

api:
  encryption:
    key: "YOUR_API_KEY"

ota:
  password: "YOUR_OTA_PASSWORD"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  output_power: 20dB

mqtt:
  broker: !secret mqtt_broker
  username: !secret mqtt_user
  password: !secret mqtt_password

sensor:
  - platform: uptime
    name: "Uptime Sensor"

text_sensor:
  - platform: template
    name: "I2C Sensor Data"
    id: i2c_sensor_data

custom_component:
  - lambda: |-
      auto my_receiver = new MyI2CReceiver(id(i2c_sensor_data));
      return {my_receiver};

logger:
  level: DEBUG

Solution Details:

  1. Initializing I2C as Slave: I used Wire.begin(0x08) to set up the ESP32 as an I2C slave with the address 0x08.
  2. onReceive Callback: The static function receiveEventStatic handles the interaction between the Wire library and the data receiving method in my class.
  3. Publishing Data via MQTT: After reading the data received via I2C, I publish it as a text sensor via MQTT using the text_sensor component.

I hope this solution helps anyone who needs to configure a device as an I2C slave with ESPHome.

1 Like

That’s really great! I started looking today how to create slave i2c device on ESPHome and found this post, it is super useful.
Try to convert your code into external_component, hosted on github. custom_components are deprecated in ESPHome, external_component will be much easier to setup / maintain. I did something very similar for my esphome_canopen project - it was started as single cpp header file, but I quickly converted it into external_component and that was good decision.
Another idea: i2c sensor proxy, allowing to easy expose any ESPHome sensor as i2c slave