API Connections unstable with BLE tracker enabled

I’ve been having issues with API connections dropping on an ESP32 with BLE sensors and BLE tracker enabled. I typically see EOF errors in the HomeAssistant Core logs, though not always.

The ESP32 is reading heart-rate from a bluetooth strap, and then sets the speed of a fan based on a couple of thresholds. The bluetooth and fan control works just fine.

The ESP exposes a few elements for control: a mode select, and the threshold values for low, medium, and high speeds. As long as HomeAssistant doesn’t attempt to change any of these controls the device works find. However, when I try to change the mode or the thresholds, sometimes the change is effected, sometimes not. When it is not, typically there is an EOF error in the Core log.

If I disable BLE scan then I don’t get any errors, and changes are effective. With the scan enabled, a change via HomeAssistant will fail within ~4 attempts. At all times I can be connected to the ESP log output via wifi…even when I am getting the EOF errors the log output continues uninterrupted.

I’ve tried changing the BLE scan window to everything from 30ms to 300ms. Smaller window values seem to work “better”, but I can’t really quantify that…its not significant enough to do so. Note that the smaller scan window values don’t work very well for detecting my BLE device.

I’m not super familiar with this forum, so please excuse any formatting issues or other etiquette missteps. Happy to fill in any additional details.

ETA: The ESP has ~70kb of available heap space, and never resets during any of the above issues. You can see that enabled below, as I was initially concerned that perhaps the BLE component was using up RAM and randomly causing resets. But, I have no evidence that is the case. I’ve never seen heap drop below ~65k, and uptime never resets.

This device has been working for over a year. I’ve only recently noticed this issue. Mostly, “it just works” via automations, and I rarely fiddle with it from the frontend. So, I don’t know exactly when this may have changed since I first deployed it.

esphome:
  name: hrm-fan
  project:
    name: "tom.hrmfan"
    version: "0.9.0"

esp32:
  board: esp32doit-devkit-v1
  framework:
    type: arduino

debug:
  update_interval: 5s
# Enable logging
logger:
  level: DEBUG
  
# Enable Home Assistant API
api:
  on_client_connected:
    - logger.log:
        format: "Client %s connected to API with IP %s"
        args: ["client_info.c_str()", "client_address.c_str()"]
  on_client_disconnected:
    - logger.log: "API client disconnected!"
ota:
  password: "..."

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp32-Test Fallback Hotspot"
    password: "..."

captive_portal:

button:
  - platform: restart
    name: ${name} "Restart"

# BLE configuration for the ESPHome Heart Rate Display
#
# Copyright (c) 2021 Koen Vervloesem
# SPDX-License-Identifier: MIT
#
esp32_ble_tracker:
  id: ble_track
  scan_parameters:
    window: 300ms

ble_client:
#  - mac_address: "F3:74:EF:EE:A8:C1"
  - mac_address: "DB:E3:CE:55:CF:20"
    id: tom_hrm
    on_disconnect: 
      then: 
        lambda: |- 
          id(tom_hrm_bpm).publish_state(0.0); //# send a bunch of 0's to flush the averaging on the receive side.
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          id(tom_hrm_bpm).publish_state(0.0);
          

#
# BLE HRM controlled Fan
#  Operating modes:
#     Disabled = FAN is OFF
#     Low/Med/High = manual mode fan setting as requested
#     HRM = HRM Controlled 
#
select:
  - platform: template
    name: HRM Fan Mode
    id: hrm_fan_mode
    optimistic: true
    update_interval: 1s
    options: 
      - disabled
      - low
      - med
      - high
      - hrm
    initial_option: disabled
    restore_value: true
    set_action:
      - logger.log:
          format: "Chosen option: %s"
          args: ["x.c_str()"]

binary_sensor:
  - platform: gpio
    id: hrm_switch_low
    pin: 
      number: GPIO17
      mode:
        input: true
        pullup: true
  - platform: gpio
    id: hrm_switch_med
    pin: 
      number: GPIO18
      mode:
        input: true
        pullup: true
  - platform: gpio
    id: hrm_switch_high
    pin: 
      number: GPIO19
      mode:
        input: true
        pullup: true

#  HRM Thresholds:
#    OFF = fan is off when HRM is below this value, Low if above
#    MED = Fan is medium if above this value
#    HIGH = Fan is high above this value
#
number:
  - platform: template
    name: low
    id: hrm_low_thresh
    min_value: 75
    max_value: 200
    step: 1
    optimistic: true
    restore_value: true
    
  - platform: template
    name: med
    id: hrm_med_thresh
    min_value: 75
    max_value: 200
    step: 1
    optimistic: true
    restore_value: true
    
  - platform: template
    name: high
    id: hrm_high_thresh
    min_value: 75
    max_value: 200
    step: 1
    optimistic: true
    restore_value: true
  

sensor:
  - platform: debug
    free:
      name: "Heap Free"
    block:
      name: "Heap Max Block"
    loop_time:
      name: "Loop Time"
  - platform: uptime    
    name: HRM Uptime
  - platform: ble_client
    type: characteristic
    ble_client_id: tom_hrm
    id: tom_hrm_bpm
    name: "test Heart rate measurement"
    service_uuid: '180d'  # Heart Rate Service
    characteristic_uuid: '2a37'  # Heart Rate Measurement
    notify: true
    lambda: |-
      if (x.empty()) { 
        ESP_LOGD("HRM:", "Empty\n"); 
        return (float)(x.size());
      }
      uint16_t heart_rate_measurement = x[1];
      if (x[0] & 1) {
          heart_rate_measurement += (x[2] << 8);
      }
      return (float)heart_rate_measurement;
    icon: 'mdi:heart'
    unit_of_measurement: 'bpm'
    filters:
      - sliding_window_moving_average:
          window_size: 6
          send_every: 3
    update_interval: 1s

  - platform: ble_client
    type: characteristic
    ble_client_id: tom_hrm
    id: hrm_battery
    name: "HRM batt"
    service_uuid: '180F'  # Heart Rate Service
    characteristic_uuid: '2a19'  # Heart Rate Measurement

  - platform: ble_client
    type: characteristic
    ble_client_id: tom_hrm
    id: device_name
    service_uuid: '1800'  # Generic Access Profile
    characteristic_uuid: '2a00'  # Device Name
    lambda: |-
      std::string data_string(x.begin(), x.end());
      id(tom_hrm_name).publish_state(data_string.c_str());
      return (float)x.size();
    update_interval: 30s
    
text_sensor:
  - platform: debug
    device:
      name: "Device Info"
    reset_reason:
      name: "Reset Reason"
  - platform: template
    name: "test heart rate sensor name"
    id: tom_hrm_name

  - platform: template
    name: "HRM Fan"
    id: tom_hrm_fan
    
output:
  - platform: gpio
    pin: GPIO27 
    id: low_speed_fan
  - platform: gpio
    pin: GPIO26
    id: med_speed_fan
  - platform: gpio
    pin: GPIO25
    id: high_speed_fan

interval: 
  - interval: 5s
    then:
      lambda: !lambda |-
        int fan_state = 0;
        
        ESP_LOGD("interval:", "fired state = %f", id(tom_hrm_bpm).get_state());
        ESP_LOGD("interval:", "   mode = %s", id(hrm_fan_mode).state.c_str());
        ESP_LOGD("interval:", "   low_t = %f", id(hrm_low_thresh).state);
        ESP_LOGD("interval:", "   med_t = %f", id(hrm_med_thresh).state);
        ESP_LOGD("interval:", "   high_t = %f", id(hrm_high_thresh).state);
        
        // if fan is in a manual mode then set the fan state to that mode 
        if (!strcmp(id(hrm_fan_mode).state.c_str(), "disabled")) fan_state = 0;
        if (!strcmp(id(hrm_fan_mode).state.c_str(), "low")) fan_state = 1;
        if (!strcmp(id(hrm_fan_mode).state.c_str(), "med")) fan_state = 2;
        if (!strcmp(id(hrm_fan_mode).state.c_str(), "high")) fan_state = 3;
        
        if (!strcmp(id(hrm_fan_mode).state.c_str(), "hrm")){ // if in HRM mode then set the speed according to the thresholds
          //id(ble_track).start_scan();
          if (id(tom_hrm_bpm).get_state() < id(hrm_low_thresh).state || isnan(id(tom_hrm_bpm).get_state())) {
            fan_state = 0;
          } 
          else if (id(tom_hrm_bpm).get_state() > id(hrm_low_thresh).state  && id(tom_hrm_bpm).get_state() < id(hrm_med_thresh).state ) {
            fan_state = 1;
          }
          else if (id(tom_hrm_bpm).get_state() > id(hrm_med_thresh).state  && id(tom_hrm_bpm).get_state() < id(hrm_high_thresh).state ) {
            fan_state = 2;
          }
          else {
            fan_state = 3;
          }
        }
        else {
          //id(ble_track).stop_scan();
        }  
        
        // set the fan output state to according to the above
        switch (fan_state) {
          case 0: //off
            ESP_LOGD("interval:", "  Fan = OFF\n");
            id(tom_hrm_fan).publish_state("off");
            id(low_speed_fan).turn_off();
            id(med_speed_fan).turn_off();
            id(high_speed_fan).turn_off();       
            break;
          case 1: //#low
            ESP_LOGD("interval:", "  Fan = LOW\n");  
            id(tom_hrm_fan).publish_state("Low");
            id(low_speed_fan).turn_on();
            id(med_speed_fan).turn_off();
            id(high_speed_fan).turn_off();
            break;
          case 2: //#med
            ESP_LOGD("interval:", "  Fan = MED\n");
            id(tom_hrm_fan).publish_state("Med");
            id(low_speed_fan).turn_off();
            id(med_speed_fan).turn_on();
            id(high_speed_fan).turn_off();
            break;
          case 3: //#high
            ESP_LOGD("interval:", "  Fan = HIGH\n");
            id(tom_hrm_fan).publish_state("High");
            id(low_speed_fan).turn_off();
            id(med_speed_fan).turn_off();
            id(high_speed_fan).turn_on();
          break;
        }