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>
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
|
||||||
```
|
```
|
||||||
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 592 KiB After Width: | Height: | Size: 592 KiB |
|
Before Width: | Height: | Size: 13 MiB After Width: | Height: | Size: 13 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 6.2 MiB After Width: | Height: | Size: 6.2 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 455 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 757 KiB After Width: | Height: | Size: 757 KiB |
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
@@ -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] ...`.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||