My Abrobot Bluetooth Proxy and more..

The Abrobot is a cheap ESP23 board with tiny display, widely available on Aliexpress.

My project called abrobot uses this board in ESPHOME and is a normal ESPHOME Bluetooth proxy…

bluetooth_proxy:
  active: true
  connection_slots: 3

It is also a BLE activity counter

The OLED shows the number of devices heard during the preceding 120 seconds.

and an MQTT BLE event reporter

It publishes: abrobot/ble with JSON output such as:

{
  "event": "new",
  "mac": "5C:53:10:AB:60:99",
  "name": "IoTorero Sensor",
  "rssi": -83
}

or:

{
  "event": "rejoined",
  "mac": "AA:BB:CC:DD:EE:FF",
  "name": "",
  "rssi": -57
}

rejoined doesn’t mean the physical device was switched off and back on. It means that the Abrobot had previously seen the MAC, subsequently heard nothing from it for 120 seconds, removed it from the active cache, and then saw it again

There’s a Web UI display and LED. The Web UI gives you: Onboard LED and Display Toggle

Clearly you get very little info on that display (which can be turned on and off along with the blue LED, in the WebUI.

If you want to see new and rejoined devices in detail – an MQTT subscription to abrobat/ble will do the job.

Here is a sample output from the WebUI

Here is the complete ESPHOME code. Just a point here – I used to be lazy and put passwords in my code – I still could as no-one else in my home would know what to do with them – but honestly – just having a secrets file in ESPHOME keeps the lot in one place. If using ESPHOME within Home Assistant, don’t confuse that secrets file with the one in Home Assistant itself.

# ========================================================
# Abrobot-based BLE Proxy with display
# August 2026 Peter Scargill and credit to ChatGPT
# v1.0 - prior to developing the TTGO version with scanner
# https://tech.scargill.net/ttgo-t-display-esphome-ble-scanner-and-bluetooth-proxy/
# ========================================================

esphome:
  name: abrobot
  friendly_name: abrobot

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

# Enable logging
#logger:
#    level: INFO

logger:
  level: DEBUG

# Enable Home Assistant API
api:

ota:
  - platform: esphome
    password: !secret abrobot_ota_password
    
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Abrobot Fallback Hotspot"
    password: "ih2TLODxSA25"

mqtt:
  id: mqtt_client
  broker: !secret mqtt_broker
  username: !secret mqtt_username
  password: !secret mqtt_password
  topic_prefix: abrobot  
  log_topic: null
  
captive_portal:

web_server:
  port: 80
  local: true

i2c:
  id: bus_i2c
  sda: GPIO5
  scl: GPIO6
  scan: true
  frequency: 800kHz

globals:
  - id: display_on
    type: bool
    restore_value: yes
    initial_value: 'true'
  - id: ble_device_count
    type: int
    restore_value: no
    initial_value: '0'
  - id: onboard_led_state
    type: bool
    restore_value: no
    initial_value: 'true'

time:
  - platform: sntp
    id: current_time
    timezone: Europe/Brussels
    servers:
     - 0.pool.ntp.org
     - 1.pool.ntp.org
     - 2.pool.ntp.org    
display:
  - platform: ssd1306_i2c
    id: ssd1306_display
    model: "SSD1306 72x40"
    rotation: 0
    update_interval: 1s
    lambda: |-
      if (!id(display_on)) {
        return;
      }
    
      if (id(onboard_led_state)) {
  
        // BLE activity display
        it.printf(22, 0, id(font3), "BLE");
  
        // Centre the device count
        char count[8];
        snprintf(count, sizeof(count), "%d", id(ble_device_count));
  
        int x;
  
        if (id(ble_device_count) < 10) {
          x = 30;
        } else if (id(ble_device_count) < 100) {
          x = 23;
        } else {
          x = 16;
        }
  
        it.printf(x, 15, id(font1), "%s", count);
    
      } else {
    
        // Normal clock display
        it.strftime(4, 1, id(font1), "%H:%M", id(current_time).now());
        it.strftime(7, 23, id(font2), "%d/%m/%y", id(current_time).now());
      }
    
      // Flashing underline
      if (id(current_time).now().second % 2 == 0) {
        it.horizontal_line(0, 39, 72);
      }
font:
  - id: font1
    file: "fonts/Roboto-Black.ttf"
    size: 25
  - id: font2
    file: "fonts/Roboto-Black.ttf"
    size: 13
  - id: font3
    file: "fonts/Roboto-Black.ttf"
    size: 18

output:
  - platform: gpio
    pin:
      number: GPIO8
      inverted: true
    id: gpio_8

light:
  - platform: binary
    name: "Onboard LED"
    id: onboard_led
    output: gpio_8
    restore_mode: ALWAYS_ON

    on_turn_on:
      - lambda: |-
          id(onboard_led_state) = true;

    on_turn_off:
      - lambda: |-
          id(onboard_led_state) = false;

switch:
  - platform: template
    name: "Display Toggle"
    id: display_toggle
    restore_mode: RESTORE_DEFAULT_ON
    lambda: |-
      if (id(display_on)) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - globals.set:
          id: display_on
          value: 'true'
    turn_off_action:
      - globals.set:
          id: display_on
          value: 'false'

esp32_ble_tracker:
  scan_parameters:
    interval: 1100ms
    window: 1100ms
    active: true

  on_ble_advertise:
    then:
      - lambda: |-
          // Active devices: devices heard within the last 120 seconds
          static std::map<std::string, unsigned long> devices_seen;

          // Historical devices: everything seen since boot
          static std::map<std::string, bool> devices_known;

          unsigned long now = millis();

          std::string mac = x.address_str();
          std::string name = x.get_name();
          int rssi = x.get_rssi();

          // Was this device already active?
          bool was_active =
              (devices_seen.find(mac) != devices_seen.end());

          // Have we ever seen this device since boot?
          bool was_known =
              (devices_known.find(mac) != devices_known.end());

          // ------------------------------------------------
          // MQTT EVENT
          // ------------------------------------------------

          if (!was_known) {

            // First time we've ever seen this MAC
            char payload[256];

            snprintf(
              payload,
              sizeof(payload),
              "{\"event\":\"new\",\"mac\":\"%s\",\"name\":\"%s\",\"rssi\":%d}",
              mac.c_str(),
              name.c_str(),
              rssi
            );

            id(mqtt_client).publish(
              "abrobot/ble",
              payload
            );

            ESP_LOGI(
              "ble_mqtt",
              "NEW BLE: %s  %s  RSSI %d",
              mac.c_str(),
              name.c_str(),
              rssi
            );

            // Add to historical list
            devices_known[mac] = true;

          } else if (!was_active) {

            // We've seen it before, but it wasn't active
            // in the last 120 seconds: it has REJOINED.

            char payload[256];

            snprintf(
              payload,
              sizeof(payload),
              "{\"event\":\"rejoined\",\"mac\":\"%s\",\"name\":\"%s\",\"rssi\":%d}",
              mac.c_str(),
              name.c_str(),
              rssi
            );

            id(mqtt_client).publish(
              "abrobot/ble",
              payload
            );

            ESP_LOGI(
              "ble_mqtt",
              "REJOINED BLE: %s  %s  RSSI %d",
              mac.c_str(),
              name.c_str(),
              rssi
            );
          }

          // ------------------------------------------------
          // UPDATE ACTIVE DEVICE CACHE
          // ------------------------------------------------

          devices_seen[mac] = now;

          // ------------------------------------------------
          // REMOVE DEVICES NOT HEARD FOR 120 SECONDS
          // ------------------------------------------------

          for (auto it = devices_seen.begin();
               it != devices_seen.end(); ) {

            if (now - it->second >= 120000) {
              it = devices_seen.erase(it);
            } else {
              ++it;
            }
          }

          // ------------------------------------------------
          // UPDATE DISPLAY COUNT
          // ------------------------------------------------

          int new_count = devices_seen.size();

          if (new_count != id(ble_device_count)) {

            id(ble_device_count) = new_count;

            ESP_LOGI(
              "ble_count",
              "BLE devices in last 120 seconds: %d",
              new_count
            );
          }
          
bluetooth_proxy:
  active: true
  connection_slots: 3
  
  

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave the field below empty!

Are you human? Please solve:Captcha


The maximum upload file size: 512 MB. You can upload: image, audio, video, document, spreadsheet, interactive, text, archive, code, other. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here