Cleanup: remove BLE scanner, rename 10-OBD-Display-2024 to 10-GIF-Tests

- 11-BLE-Scanner removed; its car-test log moved to 12-OBD-Speed/docs
- 10-OBD-Display-2024 -> 10-GIF-Tests (GIF playback experiments)
- the broken LVGL 9 archive was already deleted (unintentionally) in d6982ab
- READMEs and CLAUDE.md references updated; visual check of 80 MHz QSPI /
  RGB565_SWAPPED marked as confirmed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
patriandClaude Opus 5.5 committed 2026-10-23 18:14:23 +02:00
1 parent 66f70c0cc1
commit 733306a9c0
28 files changed
+10 -881

No files matched your search

@@ -1,17 +1,14 @@
# OBD Display (2024 experiments) # GIF tests
Leftovers of the earlier attempt to show car speed from an ELM327 on the JC3636W518. GIF playback experiments on the JC3636W518 (originally part of the 2024 OBD display attempt; renamed 2026-10-23).
The 2024 design needed a second (classic) ESP32 as a Bluetooth-Classic → WiFi bridge; that bridge, the ELMulator Used to find the fastest GIF setup — results are in `../shared/README.md` and `../CLAUDE.md`.
and the WiFi speed sketch were removed on 2026-10-23 (still in git history) — the ELM327 is now read directly over BLE,
see `../12-OBD-Speed`.
## Projects (PlatformIO, LVGL 9.6.0, shared library in `../shared`) ## Projects (PlatformIO, LVGL 9.6.0, shared library in `../shared`)
| Folder | What it does | | Folder | What it does |
|---|---| |---|---|
| `display_gif_embedded/` | GIF compiled into firmware (`src/loligod.c`, 2.2 MB GIF → 3.5 MB firmware, `default_16MB.csv`). Plays at full GIF speed (~23.8 fps, 53 % CPU). GIF starts without SD card. | | `display_gif_embedded/` | `loligod.gif` compiled into firmware (`src/loligod.c`, 2.2 MB GIF → 3.5 MB firmware, `default_16MB.csv`). Plays at full GIF speed (~23.8 fps, 53 % CPU). Starts without SD card. |
| `display_gif_sd/` | `wallpaper.gif` from the SD card, loaded into PSRAM once and played from memory (~15 fps for the current file). | | `display_gif_sd/` | `wallpaper.gif` from the SD card, loaded into PSRAM once and played from memory (~15 fps for the current file). |
| `assets/gifs/` | GIF files (`dump/` = unused collection) | | `assets/gifs/` | GIF files (`dump/` = unused collection) |
| `_archive/lvgl9_broken/` | Unfinished 2024 LVGL 9 port, does not compile |
## Build / upload ## Build / upload
``` ```
-29
View File
@@ -1,29 +0,0 @@
# 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.
-13
View File
@@ -1,13 +0,0 @@
; 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
-436
View File
@@ -1,436 +0,0 @@
#include "ble_worker.h"
#include <NimBLEDevice.h>
#include "result_log.h"
// ---------- shared state (protected by `mtx`) ----------
static SemaphoreHandle_t mtx;
static std::vector<BleDeviceInfo> 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<NimBLERemoteService *> &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<BleDeviceInfo> &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;
}
-53
View File
@@ -1,53 +0,0 @@
// 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 <Arduino.h>
#include <vector>
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<BleDeviceInfo> &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();
-239
View File
@@ -1,239 +0,0 @@
// 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 <Arduino.h>
#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<BleDeviceInfo> 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);
}
-85
View File
@@ -1,85 +0,0 @@
#include "result_log.h"
#include <LittleFS.h>
#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();
}
}
}
-12
View File
@@ -1,12 +0,0 @@
// Result log in internal flash (LittleFS on the "spiffs" partition), readable later over serial.
#pragma once
#include <Arduino.h>
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();
+1 -1
View File
@@ -14,7 +14,7 @@ Uses the shared board library and LVGL 9.6 (`../shared`), NimBLE-Arduino 2.5.1.
## How it talks to the ELM327 ## How it talks to the ELM327
Service `FFF0`, notifications + commands on `FFF1` (verified with the user's "OBDII" adapter, see Service `FFF0`, notifications + commands on `FFF1` (verified with the user's "OBDII" adapter, see
`../11-BLE-Scanner/results/2026-10-23_car_test.txt`); other adapters: first notify+write pair. `docs/2026-10-23_ble_scanner_car_test.txt`, made with the BLE scanner tool that was removed afterwards — in git history); other adapters: first notify+write pair.
Init: `ATZ`, `ATE0`, `ATL0`, `ATS0`, `ATH0`, `ATSP0`, `0100`; then `010D` (speed) is polled continuously. Init: `ATZ`, `ATE0`, `ATL0`, `ATS0`, `ATH0`, `ATSP0`, `0100`; then `010D` (speed) is polled continuously.
Serial (115200) prints status changes as `[obd] ...`. Serial (115200) prints status changes as `[obd] ...`.
+5 -6
View File
@@ -79,9 +79,8 @@ 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) | | `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. | | `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` | | `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` |
| `12-OBD-Speed` | **The new program (in progress):** speed from the ELM327 over BLE as big number + arc gauge on a GIF background; swipe left for the adapter menu (scan, select → remembered in NVS, auto-reconnect every 3 s). See its `README.md` | | `12-OBD-Speed` | **The new program — working in the car (2026-10-23):** speed from the ELM327 over BLE as big number + arc gauge on a GIF background; swipe left for the adapter menu (scan, select → remembered in NVS, auto-reconnect every 3 s). 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-GIF-Tests` | **GIF playback experiments** (was `10-OBD-Display-2024`): `display_gif_embedded`, `display_gif_sd`, `assets/gifs`. The 2024 bridge/ELMulator/WiFi speed sketch, the broken LVGL 9 archive and the BLE scanner tool (`11-BLE-Scanner`: on-screen scan, GATT listing, ELM test, LittleFS log) were removed 2026-10-23 — all in git history |
| `10-OBD-Display-2024` | **Leftovers of the 2024 OBD project:** `display_gif_embedded`, `display_gif_sd`, `assets/gifs`, `_archive/lvgl9_broken` (bridge/ELMulator/WiFi speed sketch removed 2026-10-23) |
| `3-Specification` | Spec sheet (factory firmware features, interface photo) | | `3-Specification` | Spec sheet (factory firmware features, interface photo) |
| `4-Chip manual` | ESP32-S3R8 datasheet, PCM5100A datasheet | | `4-Chip manual` | ESP32-S3R8 datasheet, PCM5100A datasheet |
| `5-Dimension` | Mechanical drawings (JPG + DWG) | | `5-Dimension` | Mechanical drawings (JPG + DWG) |
@@ -96,7 +95,7 @@ Re-flash it with Flash Download Tool: ESP32-S3, `1.85_demo.bin` @ 0x0.
## OBD adapter (user's) ## 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). 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).
**Verified in the car (2026-10-23, `11-BLE-Scanner`, log in `11-BLE-Scanner/results/2026-10-23_car_test.txt`):** **Verified in the car (2026-10-23, with the since-removed BLE scanner tool; log in `12-OBD-Speed/docs/2026-10-23_ble_scanner_car_test.txt`):**
- BLE name **`OBDII`**, MAC `1c:a1:35:69:8d:c5`, advertises service **`0xFFF0`**. Characteristics: **`FFF1` [W, WnR, Notify]**, **`FFF2` [W, WnR]**. - BLE name **`OBDII`**, MAC `1c:a1:35:69:8d:c5`, advertises service **`0xFFF0`**. Characteristics: **`FFF1` [W, WnR, Notify]**, **`FFF2` [W, WnR]**.
Working setup: subscribe to notifications on `FFF1`, write commands (`"...␍"`) to `FFF1` (write without response) — `FFF2` is the usual write characteristic on such clones and should work too. Working setup: subscribe to notifications on `FFF1`, write commands (`"...␍"`) to `FFF1` (write without response) — `FFF2` is the usual write characteristic on such clones and should work too.
- Replies: `ATZ` → `ELM327 v2.1` (echo on by default → send `ATE0`), `ATRV` → `12.9V`, `0100` → `41 00 BE 3E A8 13`. - Replies: `ATZ` → `ELM327 v2.1` (echo on by default → send `ATE0`), `ATRV` → `12.9V`, `0100` → `41 00 BE 3E A8 13`.
@@ -114,8 +113,8 @@ AliExpress "ELM327 Mini V2.1 Bluetooth OBD **Dual Mode** 5.1" — dual mode = Bl
- **LVGL 9.6.0 port** (`display_gif_embedded`, 2026-10-23): **23.4 fps** (= the GIF's own 23.7 fps; decode ~12 ms, refresh ~24 ms) vs 20.6 fps tuned LVGL 8.3. LVGL 9.6 replaced gifdec with bitbank2 AnimatedGIF (2× faster decode, correct transparency — no local patch needed). - **LVGL 9.6.0 port** (`display_gif_embedded`, 2026-10-23): **23.4 fps** (= the GIF's own 23.7 fps; decode ~12 ms, refresh ~24 ms) vs 20.6 fps tuned LVGL 8.3. LVGL 9.6 replaced gifdec with bitbank2 AnimatedGIF (2× faster decode, correct transparency — no local patch needed).
Port notes: `lib_deps = lvgl/lvgl@9.6.0` (takes priority over the vendor lvgl 8 in `lib_extra_dirs`); `lv_conf.h` generated from the 9.6 template (CLIB malloc, refresh 15 ms, GIF on, examples off); in 9.6 public headers live in `include/lvgl/` and `LV_COLOR_DEPTH` is deprecated → `LV_COLOR_FORMAT_DEFAULT` (RGB565); no LV_COLOR_16_SWAP → `lv_draw_rgb565_swap()` in the flush cb; no LV_TICK_CUSTOM → `lv_tick_set_cb(millis)`; no `monitor_cb` → display events `LV_EVENT_REFR_START/RENDER_START/REFR_READY`; images are `lv_image_dsc_t` with `LV_IMAGE_HEADER_MAGIC`. `lv_gif_set_color_format()` switches the GIF buffer format (used, see below). Port notes: `lib_deps = lvgl/lvgl@9.6.0` (takes priority over the vendor lvgl 8 in `lib_extra_dirs`); `lv_conf.h` generated from the 9.6 template (CLIB malloc, refresh 15 ms, GIF on, examples off); in 9.6 public headers live in `include/lvgl/` and `LV_COLOR_DEPTH` is deprecated → `LV_COLOR_FORMAT_DEFAULT` (RGB565); no LV_COLOR_16_SWAP → `lv_draw_rgb565_swap()` in the flush cb; no LV_TICK_CUSTOM → `lv_tick_set_cb(millis)`; no `monitor_cb` → display events `LV_EVENT_REFR_START/RENDER_START/REFR_READY`; images are `lv_image_dsc_t` with `LV_IMAGE_HEADER_MAGIC`. `lv_gif_set_color_format()` switches the GIF buffer format (used, see below).
- **LVGL 9 tuning** (display_gif_embedded, CPU at full 23.8 fps): start 86 % → GIF in display color format 66 % → render `RGB565_SWAPPED` (no swap pass) 60 % → QSPI 80 MHz 55 % → IRAM for hot LVGL code 53 %. No effect: `LV_GIF_CACHE_DECODE_DATA` (not used anywhere in 9.6), 120-row buffers. AnimatedGIF "turbo" mode can't be used: it only outputs in COOKED draw mode, LVGL uses RAW. - **LVGL 9 tuning** (display_gif_embedded, CPU at full 23.8 fps): start 86 % → GIF in display color format 66 % → render `RGB565_SWAPPED` (no swap pass) 60 % → QSPI 80 MHz 55 % → IRAM for hot LVGL code 53 %. No effect: `LV_GIF_CACHE_DECODE_DATA` (not used anywhere in 9.6), 120-row buffers. AnimatedGIF "turbo" mode can't be used: it only outputs in COOKED draw mode, LVGL uses RAW.
- **All JC3636W518 LVGL projects now use LVGL 9.6.0 + the shared library** (display_gif_embedded, display_gif_sd, 11-BLE-Scanner, 12-OBD-Speed). SD drive `A:` = `LV_USE_FS_STDIO` on `/sdcard/`. - **All JC3636W518 LVGL projects now use LVGL 9.6.0 + the shared library** (10-GIF-Tests/*, 12-OBD-Speed). SD drive `A:` = `LV_USE_FS_STDIO` on `/sdcard/`.
- 80 MHz QSPI and RGB565_SWAPPED rendering: **visual check by the user still pending** (colors / glitches). - 80 MHz QSPI and RGB565_SWAPPED rendering: **visually confirmed OK by the user** (colors correct, no glitches).
- Display is round: corners of the 360×360 framebuffer are not visible — design UI within a 360 px circle. - Display is round: corners of the 360×360 framebuffer are not visible — design UI within a 360 px circle.
- 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). - 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` `setup()` returns early if the SD card fails to mount (black screen); `display_gif_embedded` starts its GIF first. - `display_gif_sd` `setup()` returns early if the SD card fails to mount (black screen); `display_gif_embedded` starts its GIF first.