I won’t go through the entire workings of my latest BLE Scanner and bluetooth proxy here.It is all detailed in the previous blogs.Suffice it to say that marvellous as the TTGO BT proxy and scanner is, even compared to the tiny Abrobot scanner I made earlier… ULTIMATELY the neat board with 2 in-built buttons is built on an ESP32 that is severely limiting – I had to juggle MQTT, web updates, webui and more and could not do the lot.
Then I had an epiphany – I’ve some esp32-s3 “super mini” boards lying around, came from Aliexpress.I also have the odd ILI94341 320*240 displays – same source.Dirty cheap.
I put the two together – no extra components and the ESP32-S3 has a USB-C connection.To test this combo I had the ESP plugged into a USB connection on my PC so no special power requirements… controls map back to Home Assistant if needed as does the BLE device info.And here is the result… I’m VERY pleased.
To see what this is about, read the two previous entries on the subject.When looking at the webUI below – note that most of my BT devices do not send out a name, just a MAC.Don’t know why that is.
So a neat board with a tiny ESP32-S3 on the back – just needs some kind of simple case… and here is the ESPHOME YAML code to make it all work.You’ll need your WIFI and password secrets file in ESPHOME (my ESPHOME sits inside HOME ASSISTANT)… the wiring is obvious from the code (MISO is not used and the ILI9341 CS pin is tied to 3v3 along with reset – I used the 5v pin on the ESP32 for the ILI9341 VCC).Really -READ the TTGO project – all explained in there and the previous Abrobot project – this is simply a WAY better implementation.
I’m nowhere NEAR out of resources on this one so who knows what it will include in future….only a short while ago I hated BT because of it’s range limits but now I know how simple ble proxies can be (an ESP32 tiny board with nothing else attached and a little ESPHOME code – even the example that comes with ESPHOME will do) I’ve had a complete change of heart.Kind of like Zigbee but here with my new toy I can walk around and check BT connectivity – AND GUESS WHAT – I only just found out that ESPHOME can now select from multiple access points and pick the best for walking all over the house/office with this device – MAGIC.
I use the same password on each access point but of course you don’t have to.# ======================================================== # S3 + ILI9341 BLE PROXY and Scanner 2.1 # Portrait 240 x 320 # August 2026 Peter Scargill and credit to ChatGPT # ======================================================== esphome: name: s3-ili9341-ble friendly_name: S3 ILI9341 BLE Proxy Monitor esp32: board: esp32-s3-devkitc-1 framework: type: arduino logger: web_server: port: 80 api: ota: - platform: esphome password: ttgo_ota_password - platform: web_server wifi: networks: - ssid: !secret wifi_ssid password: !secret wifi_password - ssid: !secret wifi_ssid2 password: !secret wifi_password - ssid: !secret wifi_ssid3 password: !secret wifi_password power_save_mode: none post_connect_roaming: true # ========================================================= # SPI # ========================================================= spi: clk_pin: GPIO4 mosi_pin: GPIO6 # ========================================================= # GLOBALS # ========================================================= globals: - id: ble_page type: int restore_value: no initial_value: "0" - id: ble_macs type: std::string[40] restore_value: no - id: ble_names type: std::string[40] restore_value: no - id: ble_rssi type: int[40] restore_value: no - id: ble_last_seen type: uint32_t[40] restore_value: no - id: ble_device_count type: int restore_value: no initial_value: "0" # true = NAME + MAC # false = MAC only - id: display_name_mode type: bool restore_value: yes initial_value: "true" # ========================================================= # WEB UI CONTROLS # ========================================================= button: # ------------------------------------------------------- # HOME PAGE # ------------------------------------------------------- - platform: template name: "Home Page" icon: "mdi:home" on_press: - lambda: |- id(ble_page) = 0; - component.update: ble_display # ------------------------------------------------------- # PREVIOUS PAGE # ------------------------------------------------------- - platform: template name: "Previous Page" icon: "mdi:chevron-left" on_press: - lambda: |- const int MAX_DEVICES = 40; const int ROWS_PER_PAGE = 24; const uint32_t DEVICE_TIMEOUT = 120000; int count = 0; for (int i = 0; i < MAX_DEVICES; i++) { if (!id(ble_macs)[i].empty()) { uint32_t age = millis() - id(ble_last_seen)[i]; if (age < DEVICE_TIMEOUT) { count++; } } } int pages = (count + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE; if (pages < 1) { pages = 1; } id(ble_page)--; if (id(ble_page) < 0) { id(ble_page) = pages - 1; } - component.update: ble_display # ------------------------------------------------------- # NEXT PAGE # ------------------------------------------------------- - platform: template name: "Next Page" icon: "mdi:chevron-right" on_press: - lambda: |- const int MAX_DEVICES = 40; const int ROWS_PER_PAGE = 24; const uint32_t DEVICE_TIMEOUT = 120000; int count = 0; for (int i = 0; i < MAX_DEVICES; i++) { if (!id(ble_macs)[i].empty()) { uint32_t age = millis() - id(ble_last_seen)[i]; if (age < DEVICE_TIMEOUT) { count++; } } } int pages = (count + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE; if (pages < 1) { pages = 1; } id(ble_page)++; if (id(ble_page) >= pages) { id(ble_page) = 0; } - component.update: ble_display # ========================================================= # DISPLAY PAUSE SWITCH # ON = PAUSED # OFF = RUNNING # ========================================================= switch: - platform: template name: "Pause Display" id: display_pause_switch icon: "mdi:pause-circle" restore_mode: ALWAYS_OFF turn_on_action: - component.suspend: ble_display turn_off_action: - component.resume: ble_display - component.update: ble_display # ========================================================= # BLE TRACKER # ========================================================= esp32_ble_tracker: scan_parameters: interval: 1100ms window: 1100ms active: true on_ble_advertise: then: - lambda: |- const int MAX_DEVICES = 40; const uint32_t DEVICE_TIMEOUT = 120000; char mac_buffer[18]; x.address_str_to(mac_buffer); ESP_LOGI("BLE", "Found: %s %s RSSI: %d", mac_buffer, x.get_name().c_str(), x.get_rssi()); std::string mac = mac_buffer; std::string name = x.get_name(); int rssi = x.get_rssi(); uint32_t now = millis(); // ================================================= // FIND EXISTING DEVICE // ================================================= int found = -1; for (int i = 0; i < MAX_DEVICES; i++) { if (id(ble_macs)[i] == mac) { found = i; break; } } // ================================================= // FIND EMPTY SLOT // ================================================= if (found < 0) { for (int i = 0; i < MAX_DEVICES; i++) { if (id(ble_macs)[i].empty()) { found = i; break; } } } // ================================================= // IF FULL, REPLACE OLDEST INACTIVE DEVICE // ================================================= if (found < 0) { uint32_t oldest_time = 0xFFFFFFFFUL; int oldest_slot = -1; for (int i = 0; i < MAX_DEVICES; i++) { uint32_t age = now - id(ble_last_seen)[i]; if (age >= DEVICE_TIMEOUT && id(ble_last_seen)[i] < oldest_time) { oldest_time = id(ble_last_seen)[i]; oldest_slot = i; } } if (oldest_slot >= 0) { found = oldest_slot; } } // ================================================= // STORE DEVICE // ================================================= if (found >= 0) { id(ble_macs)[found] = mac; id(ble_names)[found] = name; id(ble_rssi)[found] = rssi; id(ble_last_seen)[found] = now; } // ================================================= // COUNT ACTIVE DEVICES // ================================================= int count = 0; for (int i = 0; i < MAX_DEVICES; i++) { if (!id(ble_macs)[i].empty()) { uint32_t age = now - id(ble_last_seen)[i]; if (age < DEVICE_TIMEOUT) { count++; } } } id(ble_device_count) = count; # ========================================================= # FONTS # ========================================================= font: - file: "gfonts://Inconsolata" id: font_row size: 12 - file: "gfonts://Roboto" id: font_title size: 18 - file: "gfonts://Roboto" id: font_small size: 11 # ========================================================= # DISPLAY # # ILI9341 # Physical resolution: 240 x 320 # Portrait # # X = 0 ...239 # Y = 0 ...
319 # ========================================================= display: - platform: ili9xxx model: ili9341 id: ble_display dc_pin: GPIO5 reset_pin: GPIO8 invert_colors: false color_palette: 8BIT data_rate: 10MHz rotation: 0 update_interval: 1s lambda: |- // ================================================= // DISPLAY DIMENSIONS // ================================================= const int SCREEN_WIDTH = 240; const int SCREEN_HEIGHT = 320; const int RIGHT_EDGE = SCREEN_WIDTH - 1; // ================================================= // LAYOUT // ================================================= const int HEADER_Y = 5; const int HEADER_LINE_Y = 27; const int FIRST_Y = 32; const int LINE_HEIGHT = 13; const int FOOTER_LINE_Y = 306; const int FOOTER_Y = 309; const int ROWS_PER_PAGE = 20; // ================================================= // COLOURS // ================================================= auto black = Color(0, 0, 0); auto white = Color(255, 255, 255); auto cyan = Color(0, 255, 255); auto blue = Color(0, 120, 255); auto green = Color(0, 255, 0); auto yellow = Color(255, 255, 0); auto orange = Color(255, 165, 0); auto red = Color(255, 0, 0); auto purple = Color(200, 150, 255); it.fill(black); // ================================================= // HEADER // ================================================= it.print( 5, HEADER_Y, id(font_title), blue, "BLE Proxy Monitor" ); it.strftime( RIGHT_EDGE - 4, HEADER_Y, id(font_title), cyan, TextAlign::TOP_RIGHT, "%H:%M", id(sntp_time).now() ); it.line( 0, HEADER_LINE_Y, RIGHT_EDGE, HEADER_LINE_Y, blue ); // ================================================= // BUILD ACTIVE DEVICE LIST // ================================================= const int MAX_DEVICES = 40; int order[MAX_DEVICES]; int count = 0; for (int i = 0; i < MAX_DEVICES; i++) { if (!id(ble_macs)[i].empty()) { uint32_t age = millis() - id(ble_last_seen)[i]; if (age < 120000) { order[count++] = i; } } } // ================================================= // SORT STRONGEST RSSI FIRST // ================================================= for (int i = 0; i < count - 1; i++) { for (int j = i + 1; j < count; j++) { if (id(ble_rssi)[order[j]] > id(ble_rssi)[order[i]]) { int temp = order[i]; order[i] = order[j]; order[j] = temp; } } } // ================================================= // PAGE CALCULATION // ================================================= int pages = (count + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE; if (pages < 1) { pages = 1; } // No physical page buttons on this S3 yet.// Display page 1 for now.int current_page = id(ble_page); int start = current_page * ROWS_PER_PAGE; int end = start + ROWS_PER_PAGE; if (end > count) { end = count; } // ================================================= // DEVICE ROWS // ================================================= int y = FIRST_Y; for (int n = start; n < end; n++) { int i = order[n]; // ------------------------------------------------- // MAC // ------------------------------------------------- std::string mac = id(ble_macs)[i]; it.print( 2, y, id(font_row), white, mac.c_str() ); // ------------------------------------------------- // NAME // ------------------------------------------------- std::string name = id(ble_names)[i]; if (id(display_name_mode) && !name.empty()) { // Keep the name clear of RSSI.
if (name.length() > 13) { name = name.substr(0, 13); } it.print( 120, y, id(font_row), purple, name.c_str() ); } // ------------------------------------------------- // RSSI // ------------------------------------------------- Color rssi_color; if (id(ble_rssi)[i] >= -55) { rssi_color = green; } else if (id(ble_rssi)[i] >= -65) { rssi_color = yellow; } else if (id(ble_rssi)[i] >= -79) { rssi_color = orange; } else { rssi_color = red; } char rssi_text[8]; snprintf( rssi_text, sizeof(rssi_text), "%d", id(ble_rssi)[i] ); it.print( RIGHT_EDGE - 2, y, id(font_row), rssi_color, TextAlign::TOP_RIGHT, rssi_text ); y += LINE_HEIGHT; } // ================================================= // FOOTER LINE // ================================================= it.line( 0, FOOTER_LINE_Y, RIGHT_EDGE, FOOTER_LINE_Y, blue ); // ================================================= // FOOTER - DEVICE COUNT // Fixed left position // ================================================= char count_text[32]; snprintf( count_text, sizeof(count_text), "%d BLE devices", count ); it.print( 5, FOOTER_Y, id(font_small), cyan, count_text ); // ================================================= // FOOTER - PAGE // Fixed right position // ================================================= char page_text[32]; snprintf( page_text, sizeof(page_text), "PAGE %d/%d", current_page + 1, pages ); it.print( RIGHT_EDGE - 4, FOOTER_Y, id(font_small), cyan, TextAlign::TOP_RIGHT, page_text ); # ========================================================= # TIME # ========================================================= time: - platform: sntp id: sntp_time timezone: Europe/Madrid
Read More