From 8a84b42a3beb3e5a5ae0783f95c4731926b0015e Mon Sep 17 00:00:00 2001 From: patri Date: Fri, 23 Oct 2026 01:54:36 +0200 Subject: [PATCH] Add 11-BLE-Scanner: on-screen BLE scanner, inspector and ELM327 test - NimBLE-Arduino 2.5.1, BLE work in a background task on core 0 - LVGL 9 UI for the round screen: device list (OBD-like devices in green), tap to list services/characteristics, ELM test (ATZ/ATE0/ATI/ATRV/0100) - results saved to LittleFS /ble_log.txt; serial d/c/s commands - shared lv_conf: Montserrat 16 and 20 enabled Co-Authored-By: Claude Opus 5.5 --- 11-BLE-Scanner/README.md | 29 ++ 11-BLE-Scanner/platformio.ini | 13 + 11-BLE-Scanner/src/ble_worker.cpp | 436 ++++++++++++++++++++++++++++++ 11-BLE-Scanner/src/ble_worker.h | 53 ++++ 11-BLE-Scanner/src/main.cpp | 239 ++++++++++++++++ 11-BLE-Scanner/src/result_log.cpp | 85 ++++++ 11-BLE-Scanner/src/result_log.h | 12 + CLAUDE.md | 5 +- shared/include/lv_conf.h | 4 +- 9 files changed, 873 insertions(+), 3 deletions(-) create mode 100644 11-BLE-Scanner/README.md create mode 100644 11-BLE-Scanner/platformio.ini create mode 100644 11-BLE-Scanner/src/ble_worker.cpp create mode 100644 11-BLE-Scanner/src/ble_worker.h create mode 100644 11-BLE-Scanner/src/main.cpp create mode 100644 11-BLE-Scanner/src/result_log.cpp create mode 100644 11-BLE-Scanner/src/result_log.h diff --git a/11-BLE-Scanner/README.md b/11-BLE-Scanner/README.md new file mode 100644 index 0000000..135a7fd --- /dev/null +++ b/11-BLE-Scanner/README.md @@ -0,0 +1,29 @@ +# BLE Scanner (JC3636W518) + +Stand-alone BLE scanner with on-screen UI, made to find out how the user's ELM327 ("ELM327 Mini V2.1 Dual Mode 5.1") talks over BLE. +Uses the shared board library and LVGL 9.6 (`../shared`), NimBLE-Arduino 2.5.1. + +## On the device +1. **Scan screen** — live list of BLE devices: name, MAC, signal (dBm). Devices that look like an OBD adapter (name contains OBD/ELM/VLINK/…, or advertises service FFF0/FFE0/18F0) are shown in **green** with `OBD?`. `(nc)` = not connectable. + - **Pause / Scan** — stop/resume scanning. **Save** — write the current list to flash. +2. **Tap a device** — connects and lists all services and characteristics with their properties + (`R` read, `W` write, `WnR` write without response, `N` notify, `I` indicate). + If a service has a notify + write pair (ELM-style UART), it is shown as **ELM candidate** and **ELM test** becomes active. +3. **ELM test** — sends `ATZ`, `ATE0`, `ATI`, `ATRV` (battery voltage) and `0100` (supported PIDs, needs ignition on) and shows the replies. +4. **Back** — disconnect and return to scanning. + +Every inspect and ELM test is saved automatically (together with the scan list) to `/ble_log.txt` in internal flash (LittleFS, ~900 KB). + +## Reading the results later (USB, serial 115200) +Send a single character: +- `d` — dump `/ble_log.txt` +- `c` — clear the log +- `s` — print the current device list + +e.g. `pio device monitor` in this folder, then type `d`. + +## Build / flash +``` +pio run -t upload +``` +Note: the ELM327 is only powered when plugged into the car's OBD port. diff --git a/11-BLE-Scanner/platformio.ini b/11-BLE-Scanner/platformio.ini new file mode 100644 index 0000000..4ea5ef6 --- /dev/null +++ b/11-BLE-Scanner/platformio.ini @@ -0,0 +1,13 @@ +; BLE scanner / inspector / ELM327 tester with on-screen UI — JC3636W518, common settings in shared/jc3636w518.ini + +[platformio] +extra_configs = ../shared/jc3636w518.ini + +[shared] +dir = ../shared + +[env:jc3636w518] +extends = jc3636w518 +lib_deps = + ${jc3636w518.lib_deps} + h2zero/NimBLE-Arduino@2.5.1 diff --git a/11-BLE-Scanner/src/ble_worker.cpp b/11-BLE-Scanner/src/ble_worker.cpp new file mode 100644 index 0000000..835ba42 --- /dev/null +++ b/11-BLE-Scanner/src/ble_worker.cpp @@ -0,0 +1,436 @@ +#include "ble_worker.h" + +#include + +#include "result_log.h" + +// ---------- shared state (protected by `mtx`) ---------- +static SemaphoreHandle_t mtx; +static std::vector devices; +static uint32_t devices_version = 1; +static String text; +static uint32_t text_version = 1; +static volatile BleState state = BleState::Scanning; +static volatile bool elm_candidate = false; + +// ---------- requests from the UI to the worker task ---------- +enum class Req +{ + None, + Inspect, + ElmTest, + Back, + Pause, + Resume, +}; +static volatile Req request = Req::None; +static BleDeviceInfo request_dev; + +// ---------- worker-owned BLE objects ---------- +static NimBLEClient *client = nullptr; +static NimBLERemoteCharacteristic *elm_write = nullptr; +static NimBLERemoteCharacteristic *elm_notify = nullptr; +static String elm_rx; // notifications received from the ELM (guarded by mtx) + +static void lock() { xSemaphoreTake(mtx, portMAX_DELAY); } +static void unlock() { xSemaphoreGive(mtx); } + +static void set_text(const String &t) +{ + lock(); + text = t; + text_version++; + unlock(); +} + +static void add_text(const String &t) +{ + lock(); + text += t; + text_version++; + unlock(); +} + +static String upper(String s) +{ + s.toUpperCase(); + return s; +} + +// Services that cheap BLE OBD adapters typically use for their serial (UART-like) link +static const uint16_t ELM_SERVICE_HINTS[] = {0xFFF0, 0xFFE0, 0x18F0}; + +static bool name_looks_like_obd(const String &name) +{ + String n = upper(name); + return n.indexOf("OBD") >= 0 || n.indexOf("ELM") >= 0 || n.indexOf("VLINK") >= 0 || + n.indexOf("V-LINK") >= 0 || n.indexOf("VEEPEAK") >= 0 || n.indexOf("VGATE") >= 0; +} + +// ---------- scanning ---------- +class ScanCallbacks : public NimBLEScanCallbacks +{ + void onResult(const NimBLEAdvertisedDevice *adv) override + { + String addr = adv->getAddress().toString().c_str(); + String name = adv->haveName() ? String(adv->getName().c_str()) : String(); + String svcs; + bool hint = name_looks_like_obd(name); + for (uint8_t i = 0; i < adv->getServiceUUIDCount(); i++) + { + NimBLEUUID uuid = adv->getServiceUUID(i); + if (svcs.length()) + svcs += ","; + svcs += uuid.toString().c_str(); + for (uint16_t h : ELM_SERVICE_HINTS) + if (uuid.equals(NimBLEUUID(h))) + hint = true; + } + + lock(); + BleDeviceInfo *d = nullptr; + for (auto &e : devices) + if (e.address == addr) + d = &e; + if (!d) + { + devices.push_back(BleDeviceInfo()); + d = &devices.back(); + d->address = addr; + } + d->addr_type = adv->getAddress().getType(); + if (name.length()) + d->name = name; // keep a name once seen (scan responses don't always repeat it) + if (svcs.length()) + d->services = svcs; + d->rssi = adv->getRSSI(); + d->elm_hint = d->elm_hint || hint; + d->connectable = adv->isConnectable(); + devices_version++; + unlock(); + } +}; + +static ScanCallbacks scan_callbacks; + +static void scan_start() +{ + NimBLEScan *scan = NimBLEDevice::getScan(); + scan->setScanCallbacks(&scan_callbacks, true); // want duplicates: keeps RSSI live + scan->setActiveScan(true); // ask for scan responses (names) + scan->setInterval(100); + scan->setWindow(90); + scan->start(0, false, true); // 0 = scan until stopped + state = BleState::Scanning; +} + +static void scan_stop() +{ + NimBLEDevice::getScan()->stop(); +} + +// ---------- inspect ---------- +static String props(const NimBLERemoteCharacteristic *c) +{ + String p; + if (c->canRead()) + p += "R "; + if (c->canWrite()) + p += "W "; + if (c->canWriteNoResponse()) + p += "WnR "; + if (c->canNotify()) + p += "N "; + if (c->canIndicate()) + p += "I "; + p.trim(); + return p; +} + +static void disconnect_client() +{ + elm_candidate = false; + elm_write = nullptr; + elm_notify = nullptr; + if (client) + { + if (client->isConnected()) + client->disconnect(); + NimBLEDevice::deleteClient(client); + client = nullptr; + } +} + +static void do_inspect(const BleDeviceInfo &dev) +{ + scan_stop(); + state = BleState::Connecting; + String title = dev.name.length() ? dev.name : String("(no name)"); + set_text(title + "\n" + dev.address + " " + dev.rssi + " dBm\nConnecting...\n"); + + client = NimBLEDevice::createClient(); + client->setConnectTimeout(10000); + NimBLEAddress addr(std::string(dev.address.c_str()), dev.addr_type); + if (!client->connect(addr)) + { + add_text(String("Connect FAILED (error ") + client->getLastError() + ")\n"); + log_append("Inspect " + dev.address, title + "\nconnect failed, error " + client->getLastError()); + disconnect_client(); + state = BleState::Failed; + return; + } + add_text(String("Connected, MTU ") + client->getMTU() + "\n"); + + // list services and characteristics, and look for an ELM-style UART pair (notify + write) + String out; + int best_score = -1; + const std::vector &svcs = client->getServices(true); + for (NimBLERemoteService *s : svcs) + { + out += String("\nSVC ") + s->getUUID().toString().c_str() + "\n"; + NimBLERemoteCharacteristic *w = nullptr, *n = nullptr; + for (NimBLERemoteCharacteristic *c : s->getCharacteristics(true)) + { + out += String(" ") + c->getUUID().toString().c_str() + " [" + props(c) + "]\n"; + if (!n && (c->canNotify() || c->canIndicate())) + n = c; + if (!w && (c->canWrite() || c->canWriteNoResponse())) + w = c; + } + if (w && n) + { + int score = 1; + for (uint16_t h : ELM_SERVICE_HINTS) + if (s->getUUID().equals(NimBLEUUID(h))) + score = 2; + if (score > best_score) + { + best_score = score; + elm_write = w; + elm_notify = n; + } + } + } + if (elm_write && elm_notify) + { + elm_candidate = true; + out += String("\nELM candidate:\n write ") + elm_write->getUUID().toString().c_str() + + "\n notify " + elm_notify->getUUID().toString().c_str() + "\n"; + } + else + { + out += "\nNo notify+write pair found (not an ELM-style UART).\n"; + } + add_text(out); + state = BleState::Connected; + + log_append("Scan list", ble_devices_as_text()); + log_append("Inspect " + dev.address, title + " " + dev.rssi + " dBm\n" + out); +} + +// ---------- ELM327 test ---------- +static void on_elm_notify(NimBLERemoteCharacteristic *c, uint8_t *data, size_t len, bool is_notify) +{ + lock(); + for (size_t i = 0; i < len; i++) + elm_rx += (char)data[i]; + unlock(); +} + +// Sends one command and waits for the ELM prompt '>' (or a timeout). Returns the reply. +static String elm_command(const char *cmd, uint32_t timeout_ms) +{ + lock(); + elm_rx = ""; + unlock(); + String line = String(cmd) + "\r"; + bool with_response = !elm_write->canWriteNoResponse(); // prefer write-without-response if offered + if (!elm_write->writeValue((const uint8_t *)line.c_str(), line.length(), with_response)) + return "(write failed)"; + + uint32_t t0 = millis(); + String rx; + while (millis() - t0 < timeout_ms) + { + lock(); + rx = elm_rx; + unlock(); + if (rx.indexOf('>') >= 0) + break; + vTaskDelay(pdMS_TO_TICKS(20)); + } + if (rx.indexOf('>') < 0) + rx += " (timeout)"; + // tidy: CR -> newline, drop prompt + rx.replace("\r", "\n"); + rx.replace(">", ""); + while (rx.indexOf("\n\n") >= 0) + rx.replace("\n\n", "\n"); + rx.trim(); + return rx; +} + +static void do_elm_test() +{ + if (!client || !client->isConnected() || !elm_write || !elm_notify) + { + add_text("\nELM test: not connected / no candidate\n"); + return; + } + state = BleState::ElmTesting; + add_text("\n--- ELM327 test ---\n"); + if (!elm_notify->subscribe(elm_notify->canNotify(), on_elm_notify)) + { + add_text("subscribe FAILED\n"); + state = BleState::Connected; + return; + } + struct + { + const char *cmd; + uint32_t timeout; + const char *what; + } cmds[] = { + {"ATZ", 4000, "reset"}, + {"ATE0", 2000, "echo off"}, + {"ATI", 2000, "version"}, + {"ATRV", 2000, "battery voltage"}, + {"0100", 8000, "supported PIDs (needs ignition on)"}, + }; + String log_text; + for (auto &c : cmds) + { + if (!client->isConnected()) + { + add_text("disconnected!\n"); + break; + } + String reply = elm_command(c.cmd, c.timeout); + String block = String("> ") + c.cmd + " (" + c.what + ")\n" + reply + "\n"; + add_text(block); + log_text += block; + } + elm_notify->unsubscribe(); + add_text("--- done ---\n"); + log_append("ELM test " + String(client->getPeerAddress().toString().c_str()), log_text); + state = BleState::Connected; +} + +// ---------- worker task ---------- +static void worker_task(void *) +{ + for (;;) + { + Req r = request; + if (r != Req::None) + { + request = Req::None; + switch (r) + { + case Req::Inspect: + do_inspect(request_dev); + break; + case Req::ElmTest: + do_elm_test(); + break; + case Req::Back: + disconnect_client(); + set_text(""); + scan_start(); + break; + case Req::Pause: + if (state == BleState::Scanning) + { + scan_stop(); + state = BleState::Paused; + } + break; + case Req::Resume: + if (state == BleState::Paused) + scan_start(); + break; + default: + break; + } + } + // a connection can drop by itself (adapter unplugged / out of range) + if (client && !client->isConnected() && (state == BleState::Connected)) + { + add_text("\nConnection lost.\n"); + state = BleState::Failed; + } + vTaskDelay(pdMS_TO_TICKS(20)); + } +} + +// ---------- public API ---------- +void ble_begin() +{ + mtx = xSemaphoreCreateMutex(); + NimBLEDevice::init("JC3636W518-scanner"); + NimBLEDevice::setPower(9); // dBm, max for ESP32-S3 + scan_start(); + // core 0: Arduino loop/LVGL run on core 1 + xTaskCreatePinnedToCore(worker_task, "ble_worker", 8192, nullptr, 2, nullptr, 0); +} + +void ble_scan_pause(bool pause) { request = pause ? Req::Pause : Req::Resume; } + +bool ble_get_devices(std::vector &out, uint32_t &version) +{ + lock(); + bool changed = version != devices_version; + if (changed) + { + out = devices; + version = devices_version; + } + unlock(); + return changed; +} + +void ble_request_inspect(const BleDeviceInfo &dev) +{ + request_dev = dev; + request = Req::Inspect; +} + +void ble_request_elm_test() { request = Req::ElmTest; } + +void ble_request_back() { request = Req::Back; } + +BleState ble_state() { return state; } + +bool ble_has_elm_candidate() { return elm_candidate; } + +bool ble_get_text(String &out, uint32_t &version) +{ + lock(); + bool changed = version != text_version; + if (changed) + { + out = text; + version = text_version; + } + unlock(); + return changed; +} + +String ble_devices_as_text() +{ + String s; + lock(); + for (auto &d : devices) + { + s += d.address + " " + d.rssi + " dBm " + (d.name.length() ? d.name : String("(no name)")); + if (d.elm_hint) + s += " [OBD?]"; + if (!d.connectable) + s += " [not connectable]"; + if (d.services.length()) + s += " svc: " + d.services; + s += "\n"; + } + unlock(); + return s; +} diff --git a/11-BLE-Scanner/src/ble_worker.h b/11-BLE-Scanner/src/ble_worker.h new file mode 100644 index 0000000..29e55e0 --- /dev/null +++ b/11-BLE-Scanner/src/ble_worker.h @@ -0,0 +1,53 @@ +// BLE scanning, device inspection and ELM327 test, running in a background task. +// All functions are safe to call from the UI (loop) task. +#pragma once + +#include +#include + +struct BleDeviceInfo +{ + String address; // "aa:bb:cc:dd:ee:ff" + uint8_t addr_type; // public / random (needed to connect) + String name; // advertised name ("" if none) + String services; // advertised service UUIDs, comma separated + int rssi; // dBm + bool elm_hint; // name or advertised service looks like an OBD/ELM327 adapter + bool connectable; +}; + +enum class BleState +{ + Scanning, + Paused, + Connecting, + Connected, // inspected; services listed + ElmTesting, + Failed, // connect/discovery failed (see text) +}; + +void ble_begin(); + +// Scan control (only while not connected) +void ble_scan_pause(bool pause); + +// Copies the device list if it changed since `version` (returns true and updates `version`). +bool ble_get_devices(std::vector &out, uint32_t &version); + +// Connect to the device and list its services. +void ble_request_inspect(const BleDeviceInfo &dev); + +// Run the ELM327 command test on the connected device (needs an ELM candidate). +void ble_request_elm_test(); + +// Disconnect and resume scanning. +void ble_request_back(); + +BleState ble_state(); +bool ble_has_elm_candidate(); + +// Inspect / ELM test output text; returns true if it changed since `version`. +bool ble_get_text(String &out, uint32_t &version); + +// Plain-text snapshot of the device list (for the log). +String ble_devices_as_text(); diff --git a/11-BLE-Scanner/src/main.cpp b/11-BLE-Scanner/src/main.cpp new file mode 100644 index 0000000..eae3d3a --- /dev/null +++ b/11-BLE-Scanner/src/main.cpp @@ -0,0 +1,239 @@ +// BLE scanner for the JC3636W518: live device list, tap a device to list its services, +// then run an ELM327 command test. Results are saved to internal flash (dump over serial with 'd'). +#include +#include "jc3636w518.h" +#include "ble_worker.h" +#include "result_log.h" + +#define MAX_LIST_ITEMS 50 + +static lv_obj_t *scr_scan, *scan_title, *scan_status, *scan_list, *btn_pause, *btn_pause_label; +static lv_obj_t *scr_inspect, *insp_title, *insp_text, *insp_text_cont, *btn_elm; +static lv_obj_t *list_items[MAX_LIST_ITEMS]; +static int list_count = 0; + +static std::vector devs; +static uint32_t devs_version = 0; +static uint32_t text_version = 0; +static bool paused = false; +static uint32_t status_msg_until = 0; + +static const lv_color_t COLOR_BG = lv_color_hex(0x000000); +static const lv_color_t COLOR_TEXT = lv_color_hex(0xE0E0E0); +static const lv_color_t COLOR_DIM = lv_color_hex(0x8A8A8A); +static const lv_color_t COLOR_OBD = lv_color_hex(0x4CD964); + +// ---------- helpers ---------- +static lv_obj_t *make_button(lv_obj_t *parent, const char *text, int x, int y, int w, lv_event_cb_t cb) +{ + lv_obj_t *b = lv_button_create(parent); + lv_obj_set_size(b, w, 34); + lv_obj_align(b, LV_ALIGN_TOP_MID, x, y); + lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, NULL); + lv_obj_t *l = lv_label_create(b); + lv_label_set_text(l, text); + lv_obj_set_style_text_font(l, &lv_font_montserrat_16, 0); + lv_obj_center(l); + return b; +} + +static lv_obj_t *make_screen() +{ + lv_obj_t *s = lv_obj_create(lv_screen_active()); + lv_obj_remove_style_all(s); + lv_obj_set_size(s, SCREEN_RES_HOR, SCREEN_RES_VER); + lv_obj_set_style_bg_color(s, COLOR_BG, 0); + lv_obj_set_style_bg_opa(s, LV_OPA_COVER, 0); + lv_obj_remove_flag(s, LV_OBJ_FLAG_SCROLLABLE); + return s; +} + +static void show_scan_screen(bool scan) +{ + if (scan) + { + lv_obj_remove_flag(scr_scan, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(scr_inspect, LV_OBJ_FLAG_HIDDEN); + } + else + { + lv_obj_add_flag(scr_scan, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(scr_inspect, LV_OBJ_FLAG_HIDDEN); + } +} + +// ---------- event handlers ---------- +static void on_device_clicked(lv_event_t *e) +{ + int idx = (int)(intptr_t)lv_event_get_user_data(e); + if (idx < 0 || idx >= (int)devs.size()) + return; + const BleDeviceInfo &d = devs[idx]; + lv_label_set_text(insp_title, d.name.length() ? d.name.c_str() : d.address.c_str()); + lv_label_set_text(insp_text, "Connecting..."); + lv_obj_add_state(btn_elm, LV_STATE_DISABLED); + show_scan_screen(false); + ble_request_inspect(d); +} + +static void on_pause_clicked(lv_event_t *e) +{ + paused = !paused; + ble_scan_pause(paused); + lv_label_set_text(btn_pause_label, paused ? "Scan" : "Pause"); +} + +static void on_save_clicked(lv_event_t *e) +{ + log_append("Scan list (saved by button)", ble_devices_as_text()); + lv_label_set_text(scan_status, "Saved to flash"); + status_msg_until = millis() + 2000; +} + +static void on_back_clicked(lv_event_t *e) +{ + ble_request_back(); + paused = false; + lv_label_set_text(btn_pause_label, "Pause"); + show_scan_screen(true); +} + +static void on_elm_clicked(lv_event_t *e) +{ + lv_obj_add_state(btn_elm, LV_STATE_DISABLED); + ble_request_elm_test(); +} + +// ---------- UI construction ---------- +static void build_ui() +{ + lv_obj_set_style_bg_color(lv_screen_active(), COLOR_BG, 0); + + // scan screen + scr_scan = make_screen(); + scan_title = lv_label_create(scr_scan); + lv_label_set_text(scan_title, "BLE scan"); + lv_obj_set_style_text_font(scan_title, &lv_font_montserrat_20, 0); + lv_obj_set_style_text_color(scan_title, COLOR_TEXT, 0); + lv_obj_align(scan_title, LV_ALIGN_TOP_MID, 0, 20); + + scan_status = lv_label_create(scr_scan); + lv_label_set_text(scan_status, "starting..."); + lv_obj_set_style_text_color(scan_status, COLOR_DIM, 0); + lv_obj_align(scan_status, LV_ALIGN_TOP_MID, 0, 46); + + scan_list = lv_list_create(scr_scan); + lv_obj_set_size(scan_list, 270, 222); + lv_obj_align(scan_list, LV_ALIGN_TOP_MID, 0, 68); + lv_obj_set_style_bg_color(scan_list, lv_color_hex(0x101418), 0); + lv_obj_set_style_border_width(scan_list, 0, 0); + lv_obj_set_style_pad_all(scan_list, 4, 0); + + lv_obj_t *b = make_button(scr_scan, "Pause", -48, 296, 88, on_pause_clicked); + btn_pause = b; + btn_pause_label = lv_obj_get_child(b, 0); + make_button(scr_scan, "Save", 48, 296, 88, on_save_clicked); + + // inspect screen + scr_inspect = make_screen(); + insp_title = lv_label_create(scr_inspect); + lv_obj_set_width(insp_title, 220); + lv_label_set_long_mode(insp_title, LV_LABEL_LONG_MODE_DOTS); + lv_obj_set_style_text_align(insp_title, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(insp_title, &lv_font_montserrat_20, 0); + lv_obj_set_style_text_color(insp_title, COLOR_TEXT, 0); + lv_obj_align(insp_title, LV_ALIGN_TOP_MID, 0, 26); + + insp_text_cont = lv_obj_create(scr_inspect); + lv_obj_set_size(insp_text_cont, 284, 228); + lv_obj_align(insp_text_cont, LV_ALIGN_TOP_MID, 0, 60); + lv_obj_set_style_bg_color(insp_text_cont, lv_color_hex(0x101418), 0); + lv_obj_set_style_border_width(insp_text_cont, 0, 0); + lv_obj_set_style_pad_all(insp_text_cont, 6, 0); + insp_text = lv_label_create(insp_text_cont); + lv_obj_set_width(insp_text, lv_pct(100)); + lv_label_set_long_mode(insp_text, LV_LABEL_LONG_MODE_WRAP); + lv_obj_set_style_text_color(insp_text, COLOR_TEXT, 0); + lv_label_set_text(insp_text, ""); + + make_button(scr_inspect, "Back", -52, 296, 92, on_back_clicked); + btn_elm = make_button(scr_inspect, "ELM test", 52, 296, 104, on_elm_clicked); + lv_obj_add_state(btn_elm, LV_STATE_DISABLED); + + show_scan_screen(true); +} + +// ---------- periodic UI updates ---------- +static void update_scan_list() +{ + if (!ble_get_devices(devs, devs_version)) + return; + int n = min((int)devs.size(), MAX_LIST_ITEMS); + for (int i = 0; i < n; i++) + { + const BleDeviceInfo &d = devs[i]; + if (i >= list_count) + { + lv_obj_t *btn = lv_list_add_button(scan_list, NULL, ""); + lv_obj_add_event_cb(btn, on_device_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i); + lv_obj_set_style_bg_color(btn, lv_color_hex(0x1C232B), 0); + lv_obj_set_style_pad_ver(btn, 6, 0); + list_items[i] = btn; + list_count = i + 1; + } + lv_obj_t *label = lv_obj_get_child_by_type(list_items[i], 0, &lv_label_class); + String line = (d.elm_hint ? "OBD? " : "") + (d.name.length() ? d.name : String("(no name)")) + + "\n" + d.address + " " + d.rssi + " dBm" + (d.connectable ? "" : " (nc)"); + if (strcmp(lv_label_get_text(label), line.c_str()) != 0) + { + lv_label_set_text(label, line.c_str()); + lv_obj_set_style_text_color(label, d.elm_hint ? COLOR_OBD : COLOR_TEXT, 0); + } + } + if (millis() > status_msg_until) + { + String status = String(devs.size()) + " devices" + (paused ? " (paused)" : " scanning"); + lv_label_set_text(scan_status, status.c_str()); + } +} + +static void update_inspect() +{ + String t; + if (ble_get_text(t, text_version)) + { + lv_label_set_text(insp_text, t.c_str()); + lv_obj_scroll_to_y(insp_text_cont, LV_COORD_MAX, LV_ANIM_OFF); // follow new output + } + bool can_test = ble_state() == BleState::Connected && ble_has_elm_candidate(); + if (can_test) + lv_obj_remove_state(btn_elm, LV_STATE_DISABLED); + else + lv_obj_add_state(btn_elm, LV_STATE_DISABLED); +} + +void setup() +{ + delay(200); + Serial.begin(115200); + scr_lvgl_init(); + build_ui(); + log_begin(); + ble_begin(); +} + +void loop() +{ + static uint32_t last_ui = 0; + lv_timer_handler(); + if (millis() - last_ui > 250) + { + last_ui = millis(); + if (lv_obj_has_flag(scr_scan, LV_OBJ_FLAG_HIDDEN)) + update_inspect(); + else + update_scan_list(); + } + log_handle_serial(); + vTaskDelay(2); +} diff --git a/11-BLE-Scanner/src/result_log.cpp b/11-BLE-Scanner/src/result_log.cpp new file mode 100644 index 0000000..903f0e6 --- /dev/null +++ b/11-BLE-Scanner/src/result_log.cpp @@ -0,0 +1,85 @@ +#include "result_log.h" + +#include + +#include "ble_worker.h" + +static const char *LOG_PATH = "/ble_log.txt"; +static bool log_ok = false; + +static void print_help() +{ + Serial.println("BLE scanner: send 'd' to dump /ble_log.txt, 'c' to clear it, 's' to print the current device list."); +} + +void log_begin() +{ + // uses the "spiffs" data partition of huge_app.csv; formats it on first use + log_ok = LittleFS.begin(true); + if (!log_ok) + { + Serial.println("LittleFS mount failed - results will not be saved"); + return; + } + unsigned size = 0; + if (LittleFS.exists(LOG_PATH)) + { + File f = LittleFS.open(LOG_PATH, FILE_READ); + size = f.size(); + f.close(); + } + Serial.printf("Log: %u bytes stored (flash %u / %u bytes used)\n", size, + (unsigned)LittleFS.usedBytes(), (unsigned)LittleFS.totalBytes()); + print_help(); +} + +void log_append(const String &title, const String &text) +{ + if (!log_ok) + return; + File f = LittleFS.open(LOG_PATH, FILE_APPEND); + if (!f) + return; + // no RTC: time since boot + f.printf("===== %s (t=%lus since boot) =====\n", title.c_str(), millis() / 1000); + f.print(text); + if (!text.endsWith("\n")) + f.print("\n"); + f.print("\n"); + f.close(); +} + +void log_handle_serial() +{ + while (Serial.available()) + { + char c = Serial.read(); + if (c == 'd' || c == 'D') + { + Serial.println("----- BEGIN /ble_log.txt -----"); + if (LittleFS.exists(LOG_PATH)) + { + File f = LittleFS.open(LOG_PATH, FILE_READ); + while (f.available()) + Serial.write(f.read()); + f.close(); + } + Serial.println("----- END /ble_log.txt -----"); + } + else if (c == 'c' || c == 'C') + { + LittleFS.remove(LOG_PATH); + Serial.println("Log cleared."); + } + else if (c == 's' || c == 'S') + { + Serial.println("----- devices -----"); + Serial.print(ble_devices_as_text()); + Serial.println("-------------------"); + } + else if (c == 'h' || c == 'H' || c == '?') + { + print_help(); + } + } +} diff --git a/11-BLE-Scanner/src/result_log.h b/11-BLE-Scanner/src/result_log.h new file mode 100644 index 0000000..9556eb5 --- /dev/null +++ b/11-BLE-Scanner/src/result_log.h @@ -0,0 +1,12 @@ +// Result log in internal flash (LittleFS on the "spiffs" partition), readable later over serial. +#pragma once + +#include + +void log_begin(); + +// Appends a block of text (with a timestamp header) to /ble_log.txt. +void log_append(const String &title, const String &text); + +// Serial commands: 'd' = dump the log, 'c' = clear it, 's' = print the device list. Call every loop. +void log_handle_serial(); diff --git a/CLAUDE.md b/CLAUDE.md index 4aff906..e3ebf5c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,6 +79,7 @@ Display color format is `RGB565_SWAPPED` (ST77916 byte order, set in `lv_conf.h` | `1-Instructions` | Spec PDF (duplicate), AIDA64 remote-sensor layout `aida_remote_1.85.rslcd`, MJPEG conversion tool (Windows .NET, converts video to 360×360 MJPEG 25 fps) | | `2-Program Example/Demo_Arduino` | **Vendor only:** `library/` (3 libs), `lv_conf.h` (vendor original), `sd/` (SD listing example; its lv_conf.h has FATFS edits), `docs/` (setup screenshots + compile instructions). The vendor's original LVGL demo sketch no longer exists unmodified. | | `shared` | **Shared code for all JC3636W518 projects:** `jc3636w518.ini` (PlatformIO base config), `include/lv_conf.h` (LVGL 9.6), `lib/JC3636W518` (board library). See its `README.md` | +| `11-BLE-Scanner` | **BLE scanner with on-screen UI** (NimBLE-Arduino 2.5.1): live device list, tap to list services/characteristics, ELM327 test (ATZ/ATE0/ATI/ATRV/0100). Results saved to LittleFS `/ble_log.txt`; serial `d` dump, `c` clear, `s` device list. See its `README.md` | | `10-OBD-Display-2024` | **Earlier OBD speed-display project**, split into separate sketches — see its `README.md`: `display_speed_wifi`, `display_gif_sd`, `display_gif_embedded`, `elm327_wifi_bridge` (classic ESP32), `elmulator`, `assets/gifs`, `_archive/lvgl9_broken` | | `3-Specification` | Spec sheet (factory firmware features, interface photo) | | `4-Chip manual` | ESP32-S3R8 datasheet, PCM5100A datasheet | @@ -94,7 +95,7 @@ Re-flash it with Flash Download Tool: ESP32-S3, `1.85_demo.bin` @ 0x0. ## OBD adapter (user's) AliExpress "ELM327 Mini V2.1 Bluetooth OBD **Dual Mode** 5.1" — dual mode = Bluetooth Classic SPP **and BLE**, so the ESP32-S3 should be able to connect directly over BLE (no bridge ESP32). -Cheap dual-mode clones typically expose BLE service `FFF0` (notify `FFF1`, write `FFF2`) — **not yet verified** on this adapter; scan it first. Adapter is powered from the car's OBD port (pin 16, 12 V). +Cheap dual-mode clones typically expose BLE service `FFF0` (notify `FFF1`, write `FFF2`) — **not yet verified** on this adapter; use `11-BLE-Scanner` in the car (results in its flash log). Adapter is powered from the car's OBD port (pin 16, 12 V). ## Gotchas / observations - **LVGL 8 GIF bug (patched locally, only relevant if LVGL 8 is used again):** LVGL 8.3.11 `gifdec.c` (`gd_open_gif`) filled the canvas *opaque* with the GIF's background color. GIFs with a transparent background (e.g. `loligod.gif`, bg = RGB 28,29,49) showed that dark gray except where "restore to background" frames had cleared to transparent (→ screen black). **Patched** in the vendor copy `2-Program Example/Demo_Arduino/library/lvgl/src/extra/libs/gif/gifdec.c`: initial canvas alpha 0x00 (marked `LOCAL PATCH`). Re-apply if LVGL is ever replaced/upgraded. @@ -113,3 +114,5 @@ Cheap dual-mode clones typically expose BLE service `FFF0` (notify `FFF1`, write - Compiling a 13.9 MB GIF into the firmware will not fit "Huge APP" (3 MB); large media should live on SD (or in a custom partition). - `display_gif_sd` / `display_speed_wifi` `setup()` return early if the SD card fails to mount (black screen); `display_gif_embedded` starts its GIF first. - Draw buffer is in internal RAM; PSRAM is available for larger buffers / images. +- **LittleFS** works on the `spiffs` partition of `huge_app.csv` (~900 KB); the first mount after flashing formats it (prints `Corrupted dir pair` / `mount failed (-84)` and some `task_wdt ... task not found` once — harmless). +- BLE (NimBLE) in the user's home: ~12 devices visible within 10 s. diff --git a/shared/include/lv_conf.h b/shared/include/lv_conf.h index 0844d69..d4cfa6f 100644 --- a/shared/include/lv_conf.h +++ b/shared/include/lv_conf.h @@ -1016,13 +1016,13 @@ #define LV_FONT_MONTSERRAT_14 1 /** Montserrat 16 */ -#define LV_FONT_MONTSERRAT_16 0 +#define LV_FONT_MONTSERRAT_16 1 /* JC3636W518 */ /** Montserrat 18 */ #define LV_FONT_MONTSERRAT_18 0 /** Montserrat 20 */ -#define LV_FONT_MONTSERRAT_20 0 +#define LV_FONT_MONTSERRAT_20 1 /* JC3636W518 */ /** Montserrat 22 */ #define LV_FONT_MONTSERRAT_22 0