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 <noreply@anthropic.com>
This commit is contained in:
1 parent
71d4bf7b58
commit
8a84b42a3b
9 files changed
+873
-3
No files matched your search
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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 <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();
|
||||||
@@ -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 <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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 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();
|
||||||
@@ -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) |
|
| `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` |
|
||||||
|
| `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` |
|
| `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) |
|
| `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 |
|
||||||
@@ -94,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).
|
||||||
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
|
## 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.
|
- **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).
|
- 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.
|
- `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.
|
- 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.
|
||||||
@@ -1016,13 +1016,13 @@
|
|||||||
#define LV_FONT_MONTSERRAT_14 1
|
#define LV_FONT_MONTSERRAT_14 1
|
||||||
|
|
||||||
/** Montserrat 16 */
|
/** Montserrat 16 */
|
||||||
#define LV_FONT_MONTSERRAT_16 0
|
#define LV_FONT_MONTSERRAT_16 1 /* JC3636W518 */
|
||||||
|
|
||||||
/** Montserrat 18 */
|
/** Montserrat 18 */
|
||||||
#define LV_FONT_MONTSERRAT_18 0
|
#define LV_FONT_MONTSERRAT_18 0
|
||||||
|
|
||||||
/** Montserrat 20 */
|
/** Montserrat 20 */
|
||||||
#define LV_FONT_MONTSERRAT_20 0
|
#define LV_FONT_MONTSERRAT_20 1 /* JC3636W518 */
|
||||||
|
|
||||||
/** Montserrat 22 */
|
/** Montserrat 22 */
|
||||||
#define LV_FONT_MONTSERRAT_22 0
|
#define LV_FONT_MONTSERRAT_22 0
|
||||||
|
|||||||
Reference in new issue
Block a user