Using TM1650 with I2CDevice class

I have a question about the use of the I2C device class in an esphome c++ include file.

My code is working when I use the Arduino-style I2C calls: Wire.beginTransmission, Wire.write() and , Wire.endTransmission(). However, when I use the I2CDevice methods set_i2c_address(), and write(). No I2C bus activity occurs, and I can’t view the web page to examine the logs. I’ve attached the file in question so that someone may look at it and spot what I’m missing.

The TM1650 part is unusual part because it has 8 distinct I2C addresses instead of just one. This means I have to set a new I2C device address whenever I want to access a different register in the part.

The reason I want to change to the I2CDevice methods is that I am not seeing the DS1307 on the same I2C bus as the TM1650. I think it has to do with mixing the Arduino style methods with the esphome I2CDevice methods. I want to convert my code over to the I2CDevice methods to test this theory out. This may resolve resolve the DS1307 access errors seen in the log.

I am seeing both the TM1650 register addresses and the DS1307 registers when I do a bus scan, so the I2C bus seems to be OK.

I’ve included the code below with markers (<======) to the areas in question

#include "esphome.h"

#define DEFAULT_ALARM_MIN 30
#define DEFAULT_ALARM_HOUR 6
#define DEFAULT_ALARM_ENABLE false


#define TM1650_DIGIT_BASE 0x34                                      // Address of the left-most digit 
#define TM1650_DCTRL_BASE   0x24                                    // Address of the control register of the left-most digit
#define TM1650_ENABLE_BIT_ONOFF	0b00000001                          // Digit enable on/off bit positon
#define TM1650_MASK_ONOFF	((~TM1650_ENABLE_BIT_ONOFF) & 0xFF)       // Digit on/off bit mask
#define TM1650_BIT_DOT		0b00001000                                // Digit dot on/off bit position
#define TM1650_MASK_DOT		((~TM1650_BIT_DIT) & 0xFF)                // Digit dot on/off mask
#define TM1650_BRIGHT_SHIFT	4                                       // Number of bit shifts for brightness bits
#define TM1650_MASK_BRIGHT	0b10001111                              // Brightness bit mask
#define TM1650_MIN_BRIGHT	0                                         // Minimum brightness
#define TM1650_MAX_BRIGHT	7                                         // Maximum brightness

#define STATE_INIT          0                                       // State set at power on
#define STATE_HW_INIT       1                                       // State when initializing display hardware
#define STATE_DISPLAY_TIME  2                                       // Display time when in this state

#define TAG "xy-clock"


class XY_Clock : public PollingComponent, public CustomAPIDevice, public I2CDevice { //<============ Add I2C class to access its methods in XY_Clock
 private:
  struct XYClockSettings {
    bool alarm_enable;
    uint8_t alarm_hour;
    uint8_t alarm_minute;
  } PACKED;  // NOLINT
 
  sntp::SNTPComponent *sntp_time;
  ds1307::DS1307Component *ds1307_time;
  ErrorCode i2c_result;
  ESPPreferenceObject pref;
  XYClockSettings save{};

  int state;                                                        // State variable
  bool hw_clock_set;                                                // Hardware clock set if true
  uint8_t ctrl_save[4];                                             // Local storage for TM1650 control register values
  uint8_t digit_save[4];                                            // Local storage for digits
  char digits[5];                                                   // Storage for ascii time digits                                                                               

 public:
  // constructor: Set 500mS polling time
  XY_Clock(sntp::SNTPComponent *tc_sntp, ds1307::DS1307Component *tc_ds1307 ) : PollingComponent(1000) {
    sntp_time = tc_sntp;
    ds1307_time = tc_ds1307;
}

  // Change bits in a control register
  void tm1650_set_control_reg(uint8_t index, uint8_t set, uint8_t mask = 0xFF) {
    if(index > 3)
      return;
     
    //Wire.beginTransmission(TM1650_DCTRL_BASE + index);
    ctrl_save[index] &= mask;
    ctrl_save[index] |= set;
    //Wire.write(ctrl_save[index]);
    //Wire.endTransmission();
    set_i2c_address(TM1650_DCTRL_BASE + index); // <================ New I2C methods
    i2c_result = write(ctrl_save + index, 1, true);
  }


  // Change bits in the digit segment register
  void tm1650_set_digit_reg(uint8_t index, uint8_t segments) {
      if(index > 3)
        return;
  
      //Wire.beginTransmission(TM1650_DIGIT_BASE + index);
      digit_save[index] = segments;
      //Wire.write(digit_save[index]);
      //Wire.endTransmission();
      set_i2c_address(TM1650_DIGIT_BASE + index); // <================ New I2C methods
      i2c_result = write(digit_save + index, 1, true);
  }

  // Display a number at an index
  void tm1650_send_digit(uint8_t index, char number){
    static const uint8_t segment_map[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7f, 0x6f};
    if(number < '0' || number > '9')
      return;

    uint8_t value = ((uint8_t) number) & 0x0F;
    uint8_t segments = segment_map[value];
   
    if(index > 1) // Add colon segment if digit 2 or 3
      segments |= 0x80;
    tm1650_set_digit_reg(index, segments);
  }
 
  // This gets called once from the core before update is called
  void setup() override {
    // Set initialization state
    state = STATE_INIT;
    hw_clock_set = false;
    
    uint32_t pref_version =  0x4B82A1C9;
    pref = global_preferences->make_preference<XY_Clock::XYClockSettings>(pref_version ^ fnv1_hash("xy_clock"), true);
    if(pref.load(&save)){
      ESP_LOGD(TAG, "Loaded saved XYClock settings: ena=%u h hour=%u, minute=%u", save.alarm_enable, save.alarm_hour, save.alarm_minute);
    }
    else{
      // Initialize preferences
      save.alarm_enable = 0;
      save.alarm_hour = 6;
      save.alarm_minute = 30;

    }   
    
    // Declare a service "alarm_clock_set"
    //  - Service will be called "esphome.<NODE_NAME>_alarm_clock_set" in Home Assistant.
    //  - The service has 2 arguments (type inferred from method definition):
    //     - alarm_hour: integer
    //     - alarm_minute: boolean
    //  - The function set_alarm_time declared below will attached to the service.
    register_service(&XY_Clock::set_alarm_time, "alarm_time_set",
                    {"alarm_hour", "alarm_minute"});
    // Declare a service "alarm_enable"
    //  - Service will be called "esphome.<NODE_NAME>_alarm_clock_set" in Home Assistant.
    //  - The service has 1 argument (type inferred from method definition):
    //     - enable: boolean
    //  - The function set_alarm_time declared below will attached to the service.
    register_service(&XY_Clock::alarm_enable, "alarm_enable", {"enable"});


  }

  // This gets called repeatedly from the core at specific intervals after setup is called
  void update() override {
    // This will be called appx. every 1000 milliseconds. ESP8266 WIFI code may delay calls to this so timing will not be reliable.
  
    uint8_t init_cr = TM1650_ENABLE_BIT_ONOFF | (3 << TM1650_BRIGHT_SHIFT); // Enable display with intial brightness of 3

    switch(state){
      case STATE_INIT: // State at power on
        state = STATE_HW_INIT;
        break;

      case STATE_HW_INIT: // State during hardware initialization
       // Initialize register saves
        for( uint8_t i = 0; i < 4; i++){
            ctrl_save[i] = 0;
            digit_save[i] = 0;
        }
        // Set digit string length to zero
        digits[0] = 0;

        // Initialize control registers
        tm1650_set_control_reg(0, init_cr);
        tm1650_set_control_reg(1, init_cr);
        tm1650_set_control_reg(2, init_cr);
        tm1650_set_control_reg(3, init_cr);
         
        // Initially set digits to: --:--
        tm1650_set_digit_reg(0, 0x40);
        tm1650_set_digit_reg(1, 0x40);
        tm1650_set_digit_reg(2, 0xC0);
        tm1650_set_digit_reg(3, 0xC0);
        ESP_LOGD("custom", "Display HW initialized");
        state = STATE_DISPLAY_TIME;

        break;


      case STATE_DISPLAY_TIME:
        {
          char time_digits[7];
          uint8_t hour = 25, minute = 60;

          ESP_LOGD(TAG, "Pref alarm hour: %d", save.alarm_hour);
          ESP_LOGD(TAG, "Pref alarm minute: %d", save.alarm_minute);
          ESP_LOGD(TAG, "Pref alarm_enable: %d", save.alarm_enable);

          if(sntp_time->now().is_valid() || homeassistant_time->now().is_valid()){ // See if time is valid
            hour = sntp_time->now().hour;
            minute = sntp_time->now().minute;
            if(!hw_clock_set){
              ESP_LOGD(TAG, "Time valid. Writing time to DS1307");
              ds1307_time->write_time();
              hw_clock_set = true;
            }

          }
          else
            ESP_LOGD(TAG, "No time source is valid");
            
          if((hour < 25) &&(minute < 60)){
            snprintf(time_digits, 7, "%02d%02d", hour, minute);
            for(int i = 0; i < 4; i++)
              tm1650_send_digit(i, time_digits[i]);
          }
        }
        break;


      default:
        state = STATE_HW_INIT;
        break;


    }
 
  }

  /*
  * Set the alarm clock trigger time
  */

  void set_alarm_time(int alarm_hour, int alarm_minute){
    ESP_LOGD(TAG, "Set alarm service call: hour: %d, minute: %d", alarm_hour, alarm_minute);
    save.alarm_hour = alarm_hour;
    save.alarm_minute = alarm_minute;
    pref.save(&save);
  }

 /*
  * Set the alarm enable state
  */

  void alarm_enable(bool enable){
    ESP_LOGD(TAG, "alarm enable service call: state: %d", enable);
    pref.save(&save);
    save.alarm_enable = enable;
  }
};


You have to tell the I2CDevice class which bus you want to use. I solved it by passing in the I2C bus object pointer in the custom device class constructor, then calling I2CDevice::set_i2c_bus() with the object pointer.

Passing in the I2CBus object (third argument):

 XY_Clock(sntp::SNTPComponent *tc_sntp, ds1307::DS1307Component *tc_ds1307, I2CBus *i2c ) : PollingComponent(1000) {
    i2c_bus = i2c;
    sntp_time = tc_sntp;
    ds1307_time = tc_ds1307;
}

Calling the I2CDevice::set_i2c_bus() method:

 void setup() override {
    // Set initialization state
    state = STATE_INIT;
    hw_clock_set = false;
    
    // Set the I2C bus so that the I2CDevice methods work
    set_i2c_bus(i2c_bus);