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


