Add 12-OBD-Speed: ELM327 speed over BLE on a GIF background
- speed page: 120 px antialiased number (lv_font_conv) + 0-200 km/h arc gauge over ezgif-5-cb56986bc7.gif (embedded via board_build.embed_files) - swipe-left menu: scan, OBD-like adapters first, tap to select; adapter remembered in NVS, auto-connect at boot and reconnect every 3 s - ELM327 init ATZ/ATE0/ATL0/ATS0/ATH0/ATSP0/0100, then 010D polling - shared ini: -DLV_LVGL_H_INCLUDE_SIMPLE for generated fonts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e9c0fac68
commit
66f70c0cc1
9 files changed
+4391
No files matched your search
@@ -0,0 +1,29 @@
|
|||||||
|
# OBD Speed (JC3636W518)
|
||||||
|
|
||||||
|
Vehicle speed from an ELM327 over BLE, shown as a big number + arc gauge on top of an animated GIF.
|
||||||
|
Uses the shared board library and LVGL 9.6 (`../shared`), NimBLE-Arduino 2.5.1.
|
||||||
|
|
||||||
|
## Use
|
||||||
|
- **Speed page** — speed in km/h (`--` = no data: not connected, or ignition off), arc gauge 0–200 km/h,
|
||||||
|
status at the bottom (adapter name when running, otherwise what it is doing).
|
||||||
|
- **Swipe left → adapter menu** — scans while open. OBD-like adapters (name OBD/ELM/…, or service FFF0/FFE0/18F0)
|
||||||
|
are listed first in green. **Tap one** to select it: it is remembered (NVS) and connected immediately.
|
||||||
|
**Forget** clears the remembered adapter, **Done** (or swipe right) returns to the speed page.
|
||||||
|
- On power-up the remembered adapter is connected automatically; if the link drops or the adapter is not
|
||||||
|
there (car off), it retries every 3 s. With no adapter remembered, the device starts on the menu page.
|
||||||
|
|
||||||
|
## How it talks to the ELM327
|
||||||
|
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.
|
||||||
|
Init: `ATZ`, `ATE0`, `ATL0`, `ATS0`, `ATH0`, `ATSP0`, `0100`; then `010D` (speed) is polled continuously.
|
||||||
|
Serial (115200) prints status changes as `[obd] ...`.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
| File | What |
|
||||||
|
|---|---|
|
||||||
|
| `src/main.cpp` | LVGL UI (tileview: speed page + menu page) |
|
||||||
|
| `src/obd_link.cpp/.h` | BLE scan, connect/reconnect, ELM327 init and speed polling (background task on core 0) |
|
||||||
|
| `src/font_speed_120.c` | Montserrat Medium 120 px, digits and `-`, 4 bpp (generated with `lv_font_conv`, see header) |
|
||||||
|
| `data/bg.gif` | Background (`ezgif-5-cb56986bc7.gif`, 360×360), linked into the firmware via `board_build.embed_files` |
|
||||||
|
|
||||||
|
Replace `data/bg.gif` with any 360×360 GIF to change the background (keep it under ~3 MB).
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,18 @@
|
|||||||
|
; OBD speed display: ELM327 over BLE, GIF background, swipe-left menu to pick/remember the adapter.
|
||||||
|
; JC3636W518, common settings in shared/jc3636w518.ini
|
||||||
|
|
||||||
|
[platformio]
|
||||||
|
extra_configs = ../shared/jc3636w518.ini
|
||||||
|
|
||||||
|
[shared]
|
||||||
|
dir = ../shared
|
||||||
|
|
||||||
|
[env:jc3636w518]
|
||||||
|
extends = jc3636w518
|
||||||
|
; firmware + 1.6 MB background GIF does not fit huge_app (3 MB): 6.25 MB app slots
|
||||||
|
board_build.partitions = default_16MB.csv
|
||||||
|
; background GIF linked into the firmware as-is (symbols _binary_data_bg_gif_start/_end)
|
||||||
|
board_build.embed_files = data/bg.gif
|
||||||
|
lib_deps =
|
||||||
|
${jc3636w518.lib_deps}
|
||||||
|
h2zero/NimBLE-Arduino@2.5.1
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,308 @@
|
|||||||
|
// OBD speed display for the JC3636W518: vehicle speed from an ELM327 over BLE on top of an animated GIF.
|
||||||
|
// Swipe left for the adapter menu (scan, select, forget); the selected adapter is remembered and reconnected.
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "jc3636w518.h"
|
||||||
|
#include "obd_link.h"
|
||||||
|
|
||||||
|
#define SPEED_MAX 200
|
||||||
|
#define MAX_MENU_ITEMS 30
|
||||||
|
|
||||||
|
LV_FONT_DECLARE(font_speed_120);
|
||||||
|
|
||||||
|
// background GIF linked into the firmware (board_build.embed_files = data/bg.gif)
|
||||||
|
extern const uint8_t bg_gif_start[] asm("_binary_data_bg_gif_start");
|
||||||
|
extern const uint8_t bg_gif_end[] asm("_binary_data_bg_gif_end");
|
||||||
|
|
||||||
|
static const lv_color_t COLOR_TEXT = lv_color_hex(0xFFFFFF);
|
||||||
|
static const lv_color_t COLOR_DIM = lv_color_hex(0x9AA3AD);
|
||||||
|
static const lv_color_t COLOR_ARC = lv_color_hex(0x00C8FF);
|
||||||
|
static const lv_color_t COLOR_OBD = lv_color_hex(0x4CD964);
|
||||||
|
static const lv_color_t COLOR_PANEL = lv_color_hex(0x161C22);
|
||||||
|
|
||||||
|
static lv_obj_t *tileview, *tile_speed, *tile_menu;
|
||||||
|
static lv_obj_t *speed_arc, *speed_label, *speed_shadow, *status_label;
|
||||||
|
static lv_obj_t *menu_saved, *menu_status, *menu_list;
|
||||||
|
static lv_obj_t *menu_items[MAX_MENU_ITEMS];
|
||||||
|
static int menu_item_count = 0;
|
||||||
|
|
||||||
|
static std::vector<ObdDevInfo> scan_devs; // as received from obd_link
|
||||||
|
static std::vector<ObdDevInfo> menu_devs; // display order: OBD-like first
|
||||||
|
static uint32_t scan_version = 0;
|
||||||
|
static String menu_order; // addresses in display order (detects reordering)
|
||||||
|
|
||||||
|
// ---------- 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 void show_tile(lv_obj_t *tile)
|
||||||
|
{
|
||||||
|
lv_tileview_set_tile(tileview, tile, LV_ANIM_ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool menu_visible()
|
||||||
|
{
|
||||||
|
return lv_tileview_get_tile_active(tileview) == tile_menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- events ----------
|
||||||
|
static void on_tile_changed(lv_event_t *e)
|
||||||
|
{
|
||||||
|
obd_scan_enable(menu_visible()); // scan only while the menu is open
|
||||||
|
}
|
||||||
|
|
||||||
|
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)menu_devs.size())
|
||||||
|
return;
|
||||||
|
obd_select_device(menu_devs[idx]);
|
||||||
|
show_tile(tile_speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_forget_clicked(lv_event_t *e)
|
||||||
|
{
|
||||||
|
obd_forget_device();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_done_clicked(lv_event_t *e)
|
||||||
|
{
|
||||||
|
show_tile(tile_speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- speed page ----------
|
||||||
|
static void build_speed_tile()
|
||||||
|
{
|
||||||
|
// background GIF, decoded straight into the display format (no transparency needed on black)
|
||||||
|
static lv_image_dsc_t gif_dsc;
|
||||||
|
gif_dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||||
|
gif_dsc.header.cf = LV_COLOR_FORMAT_RAW;
|
||||||
|
gif_dsc.header.w = bg_gif_start[6] | (bg_gif_start[7] << 8);
|
||||||
|
gif_dsc.header.h = bg_gif_start[8] | (bg_gif_start[9] << 8);
|
||||||
|
gif_dsc.data_size = bg_gif_end - bg_gif_start;
|
||||||
|
gif_dsc.data = bg_gif_start;
|
||||||
|
lv_obj_t *gif = lv_gif_create(tile_speed);
|
||||||
|
lv_gif_set_color_format(gif, lv_display_get_color_format(lv_display_get_default()));
|
||||||
|
lv_gif_set_src(gif, &gif_dsc);
|
||||||
|
lv_obj_center(gif);
|
||||||
|
|
||||||
|
// arc gauge along the round edge: 270° sweep, gap at the bottom
|
||||||
|
speed_arc = lv_arc_create(tile_speed);
|
||||||
|
lv_obj_set_size(speed_arc, 344, 344);
|
||||||
|
lv_obj_center(speed_arc);
|
||||||
|
lv_arc_set_rotation(speed_arc, 135);
|
||||||
|
lv_arc_set_bg_angles(speed_arc, 0, 270);
|
||||||
|
lv_arc_set_range(speed_arc, 0, SPEED_MAX);
|
||||||
|
lv_arc_set_value(speed_arc, 0);
|
||||||
|
lv_obj_remove_style(speed_arc, NULL, LV_PART_KNOB);
|
||||||
|
lv_obj_remove_flag(speed_arc, LV_OBJ_FLAG_CLICKABLE); // let swipes reach the tileview
|
||||||
|
lv_obj_set_style_arc_width(speed_arc, 12, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_arc_width(speed_arc, 12, LV_PART_INDICATOR);
|
||||||
|
lv_obj_set_style_arc_color(speed_arc, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_arc_opa(speed_arc, LV_OPA_30, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_arc_color(speed_arc, COLOR_ARC, LV_PART_INDICATOR);
|
||||||
|
|
||||||
|
// speed number with a soft drop shadow so it stays readable on the GIF
|
||||||
|
speed_shadow = lv_label_create(tile_speed);
|
||||||
|
lv_obj_set_style_text_font(speed_shadow, &font_speed_120, 0);
|
||||||
|
lv_obj_set_style_text_color(speed_shadow, lv_color_hex(0x000000), 0);
|
||||||
|
lv_obj_set_style_text_opa(speed_shadow, LV_OPA_70, 0);
|
||||||
|
lv_label_set_text(speed_shadow, "--");
|
||||||
|
lv_obj_align(speed_shadow, LV_ALIGN_CENTER, 4, -6);
|
||||||
|
|
||||||
|
speed_label = lv_label_create(tile_speed);
|
||||||
|
lv_obj_set_style_text_font(speed_label, &font_speed_120, 0);
|
||||||
|
lv_obj_set_style_text_color(speed_label, COLOR_TEXT, 0);
|
||||||
|
lv_label_set_text(speed_label, "--");
|
||||||
|
lv_obj_align(speed_label, LV_ALIGN_CENTER, 0, -10);
|
||||||
|
|
||||||
|
lv_obj_t *unit = lv_label_create(tile_speed);
|
||||||
|
lv_obj_set_style_text_font(unit, &lv_font_montserrat_20, 0);
|
||||||
|
lv_obj_set_style_text_color(unit, COLOR_TEXT, 0);
|
||||||
|
lv_label_set_text(unit, "km/h");
|
||||||
|
lv_obj_align(unit, LV_ALIGN_CENTER, 0, 62);
|
||||||
|
|
||||||
|
// status pill at the bottom
|
||||||
|
status_label = lv_label_create(tile_speed);
|
||||||
|
lv_obj_set_style_text_font(status_label, &lv_font_montserrat_14, 0);
|
||||||
|
lv_obj_set_style_text_color(status_label, COLOR_TEXT, 0);
|
||||||
|
lv_obj_set_style_bg_color(status_label, lv_color_hex(0x000000), 0);
|
||||||
|
lv_obj_set_style_bg_opa(status_label, LV_OPA_50, 0);
|
||||||
|
lv_obj_set_style_radius(status_label, 12, 0);
|
||||||
|
lv_obj_set_style_pad_hor(status_label, 10, 0);
|
||||||
|
lv_obj_set_style_pad_ver(status_label, 3, 0);
|
||||||
|
lv_label_set_text(status_label, "");
|
||||||
|
lv_obj_align(status_label, LV_ALIGN_BOTTOM_MID, 0, -34);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- menu page ----------
|
||||||
|
static void build_menu_tile()
|
||||||
|
{
|
||||||
|
lv_obj_t *title = lv_label_create(tile_menu);
|
||||||
|
lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
|
||||||
|
lv_obj_set_style_text_color(title, COLOR_TEXT, 0);
|
||||||
|
lv_label_set_text(title, "OBD adapter");
|
||||||
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
|
||||||
|
|
||||||
|
menu_saved = lv_label_create(tile_menu);
|
||||||
|
lv_obj_set_style_text_color(menu_saved, COLOR_TEXT, 0);
|
||||||
|
lv_obj_set_width(menu_saved, 240);
|
||||||
|
lv_label_set_long_mode(menu_saved, LV_LABEL_LONG_MODE_DOTS);
|
||||||
|
lv_obj_set_style_text_align(menu_saved, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
|
lv_obj_align(menu_saved, LV_ALIGN_TOP_MID, 0, 48);
|
||||||
|
|
||||||
|
menu_status = lv_label_create(tile_menu);
|
||||||
|
lv_obj_set_style_text_color(menu_status, COLOR_DIM, 0);
|
||||||
|
lv_obj_set_width(menu_status, 260);
|
||||||
|
lv_label_set_long_mode(menu_status, LV_LABEL_LONG_MODE_DOTS);
|
||||||
|
lv_obj_set_style_text_align(menu_status, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
|
lv_obj_align(menu_status, LV_ALIGN_TOP_MID, 0, 68);
|
||||||
|
|
||||||
|
menu_list = lv_list_create(tile_menu);
|
||||||
|
lv_obj_set_size(menu_list, 276, 196);
|
||||||
|
lv_obj_align(menu_list, LV_ALIGN_TOP_MID, 0, 92);
|
||||||
|
lv_obj_set_style_bg_color(menu_list, COLOR_PANEL, 0);
|
||||||
|
lv_obj_set_style_border_width(menu_list, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(menu_list, 4, 0);
|
||||||
|
|
||||||
|
make_button(tile_menu, "Forget", -50, 296, 92, on_forget_clicked);
|
||||||
|
make_button(tile_menu, "Done", 50, 296, 92, on_done_clicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void build_ui()
|
||||||
|
{
|
||||||
|
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x000000), 0);
|
||||||
|
tileview = lv_tileview_create(lv_screen_active());
|
||||||
|
lv_obj_set_style_bg_color(tileview, lv_color_hex(0x000000), 0);
|
||||||
|
lv_obj_set_scrollbar_mode(tileview, LV_SCROLLBAR_MODE_OFF);
|
||||||
|
tile_speed = lv_tileview_add_tile(tileview, 0, 0, LV_DIR_RIGHT);
|
||||||
|
tile_menu = lv_tileview_add_tile(tileview, 1, 0, LV_DIR_LEFT);
|
||||||
|
lv_obj_add_event_cb(tileview, on_tile_changed, LV_EVENT_VALUE_CHANGED, NULL);
|
||||||
|
build_speed_tile();
|
||||||
|
build_menu_tile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- periodic updates ----------
|
||||||
|
static void update_speed()
|
||||||
|
{
|
||||||
|
static int shown = -2;
|
||||||
|
static String shown_status;
|
||||||
|
int v = obd_speed();
|
||||||
|
if (v != shown)
|
||||||
|
{
|
||||||
|
shown = v;
|
||||||
|
char buf[8];
|
||||||
|
if (v < 0)
|
||||||
|
strcpy(buf, "--");
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "%d", v);
|
||||||
|
lv_label_set_text(speed_label, buf);
|
||||||
|
lv_label_set_text(speed_shadow, buf);
|
||||||
|
lv_arc_set_value(speed_arc, v < 0 ? 0 : min(v, SPEED_MAX));
|
||||||
|
}
|
||||||
|
String st = obd_status_text();
|
||||||
|
if (st != shown_status)
|
||||||
|
{
|
||||||
|
shown_status = st;
|
||||||
|
lv_label_set_text(status_label, st.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_menu()
|
||||||
|
{
|
||||||
|
String saved = obd_saved_name();
|
||||||
|
String addr = obd_saved_address();
|
||||||
|
String line = addr.length() ? "Saved: " + (saved.length() ? saved : String("(no name)")) + " " + addr
|
||||||
|
: String("No adapter saved - tap one below");
|
||||||
|
if (line != lv_label_get_text(menu_saved))
|
||||||
|
lv_label_set_text(menu_saved, line.c_str());
|
||||||
|
String st = obd_status_text();
|
||||||
|
if (st != lv_label_get_text(menu_status))
|
||||||
|
lv_label_set_text(menu_status, st.c_str());
|
||||||
|
|
||||||
|
if (!obd_get_devices(scan_devs, scan_version))
|
||||||
|
return;
|
||||||
|
// display order: OBD-like adapters first, then the rest (each in first-seen order)
|
||||||
|
menu_devs.clear();
|
||||||
|
for (auto &d : scan_devs)
|
||||||
|
if (d.obd_hint)
|
||||||
|
menu_devs.push_back(d);
|
||||||
|
for (auto &d : scan_devs)
|
||||||
|
if (!d.obd_hint)
|
||||||
|
menu_devs.push_back(d);
|
||||||
|
if ((int)menu_devs.size() > MAX_MENU_ITEMS)
|
||||||
|
menu_devs.resize(MAX_MENU_ITEMS);
|
||||||
|
|
||||||
|
String order;
|
||||||
|
for (auto &d : menu_devs)
|
||||||
|
order += d.address;
|
||||||
|
if (order != menu_order)
|
||||||
|
{
|
||||||
|
// order changed (new device or new OBD hint): rebuild the list
|
||||||
|
menu_order = order;
|
||||||
|
lv_obj_clean(menu_list);
|
||||||
|
menu_item_count = 0;
|
||||||
|
for (int i = 0; i < (int)menu_devs.size(); i++)
|
||||||
|
{
|
||||||
|
lv_obj_t *btn = lv_list_add_button(menu_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(0x222A33), 0);
|
||||||
|
lv_obj_set_style_pad_ver(btn, 6, 0);
|
||||||
|
menu_items[i] = btn;
|
||||||
|
menu_item_count = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < menu_item_count; i++)
|
||||||
|
{
|
||||||
|
const ObdDevInfo &d = menu_devs[i];
|
||||||
|
lv_obj_t *label = lv_obj_get_child_by_type(menu_items[i], 0, &lv_label_class);
|
||||||
|
String text = (d.name.length() ? d.name : String("(no name)")) + "\n" + d.address + " " + d.rssi + " dBm";
|
||||||
|
if (text != lv_label_get_text(label))
|
||||||
|
{
|
||||||
|
lv_label_set_text(label, text.c_str());
|
||||||
|
lv_obj_set_style_text_color(label, d.obd_hint ? COLOR_OBD : COLOR_TEXT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
delay(200);
|
||||||
|
Serial.begin(115200);
|
||||||
|
scr_lvgl_init();
|
||||||
|
build_ui();
|
||||||
|
obd_begin();
|
||||||
|
if (obd_saved_address().length() == 0)
|
||||||
|
{
|
||||||
|
// first start: go straight to the adapter menu
|
||||||
|
lv_tileview_set_tile(tileview, tile_menu, LV_ANIM_OFF);
|
||||||
|
obd_scan_enable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
static uint32_t last_speed = 0, last_menu = 0;
|
||||||
|
lv_timer_handler();
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now - last_speed >= 100)
|
||||||
|
{
|
||||||
|
last_speed = now;
|
||||||
|
update_speed();
|
||||||
|
}
|
||||||
|
if (now - last_menu >= 500 && menu_visible())
|
||||||
|
{
|
||||||
|
last_menu = now;
|
||||||
|
update_menu();
|
||||||
|
}
|
||||||
|
vTaskDelay(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
#include "obd_link.h"
|
||||||
|
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
#define RETRY_DELAY_MS 3000
|
||||||
|
#define POLL_FAIL_LIMIT 3
|
||||||
|
|
||||||
|
// ---------- shared state (protected by `mtx`) ----------
|
||||||
|
static SemaphoreHandle_t mtx;
|
||||||
|
static volatile ObdState state = ObdState::NoDevice;
|
||||||
|
static volatile int speed_kmh = -1;
|
||||||
|
static String status_text = "starting";
|
||||||
|
static String saved_addr, saved_name;
|
||||||
|
static uint8_t saved_type = 0;
|
||||||
|
static std::vector<ObdDevInfo> devices;
|
||||||
|
static uint32_t devices_version = 1;
|
||||||
|
static String elm_rx; // bytes received from the ELM (notifications)
|
||||||
|
|
||||||
|
// ---------- requests from the UI ----------
|
||||||
|
static volatile bool req_select = false, req_forget = false;
|
||||||
|
static volatile bool scan_wanted = false;
|
||||||
|
static ObdDevInfo req_dev;
|
||||||
|
|
||||||
|
// ---------- worker-owned ----------
|
||||||
|
static NimBLEClient *client = nullptr;
|
||||||
|
static NimBLERemoteCharacteristic *chr_write = nullptr, *chr_notify = nullptr;
|
||||||
|
static Preferences prefs;
|
||||||
|
|
||||||
|
static void lock() { xSemaphoreTake(mtx, portMAX_DELAY); }
|
||||||
|
static void unlock() { xSemaphoreGive(mtx); }
|
||||||
|
|
||||||
|
static void set_status(const String &s)
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
bool changed = status_text != s;
|
||||||
|
status_text = s;
|
||||||
|
unlock();
|
||||||
|
if (changed)
|
||||||
|
Serial.println("[obd] " + s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services that cheap BLE OBD adapters use for their serial (UART-like) link
|
||||||
|
static const uint16_t ELM_SERVICE_HINTS[] = {0xFFF0, 0xFFE0, 0x18F0};
|
||||||
|
|
||||||
|
static bool name_looks_like_obd(String n)
|
||||||
|
{
|
||||||
|
n.toUpperCase();
|
||||||
|
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 (menu) ----------
|
||||||
|
class ScanCallbacks : public NimBLEScanCallbacks
|
||||||
|
{
|
||||||
|
void onResult(const NimBLEAdvertisedDevice *adv) override
|
||||||
|
{
|
||||||
|
if (!adv->isConnectable())
|
||||||
|
return;
|
||||||
|
String addr = adv->getAddress().toString().c_str();
|
||||||
|
String name = adv->haveName() ? String(adv->getName().c_str()) : String();
|
||||||
|
bool hint = name_looks_like_obd(name);
|
||||||
|
for (uint8_t i = 0; i < adv->getServiceUUIDCount(); i++)
|
||||||
|
for (uint16_t h : ELM_SERVICE_HINTS)
|
||||||
|
if (adv->getServiceUUID(i).equals(NimBLEUUID(h)))
|
||||||
|
hint = true;
|
||||||
|
|
||||||
|
lock();
|
||||||
|
ObdDevInfo *d = nullptr;
|
||||||
|
for (auto &e : devices)
|
||||||
|
if (e.address == addr)
|
||||||
|
d = &e;
|
||||||
|
if (!d)
|
||||||
|
{
|
||||||
|
devices.push_back(ObdDevInfo());
|
||||||
|
d = &devices.back();
|
||||||
|
d->address = addr;
|
||||||
|
d->obd_hint = false;
|
||||||
|
}
|
||||||
|
d->addr_type = adv->getAddress().getType();
|
||||||
|
if (name.length())
|
||||||
|
d->name = name;
|
||||||
|
d->rssi = adv->getRSSI();
|
||||||
|
d->obd_hint = d->obd_hint || hint;
|
||||||
|
devices_version++;
|
||||||
|
unlock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static ScanCallbacks scan_callbacks;
|
||||||
|
|
||||||
|
static void scan_set(bool on)
|
||||||
|
{
|
||||||
|
NimBLEScan *scan = NimBLEDevice::getScan();
|
||||||
|
if (on && !scan->isScanning())
|
||||||
|
{
|
||||||
|
scan->setScanCallbacks(&scan_callbacks, true);
|
||||||
|
scan->setActiveScan(true);
|
||||||
|
scan->setInterval(100);
|
||||||
|
scan->setWindow(90);
|
||||||
|
scan->start(0, false, true);
|
||||||
|
}
|
||||||
|
else if (!on && scan->isScanning())
|
||||||
|
{
|
||||||
|
scan->stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- ELM327 communication ----------
|
||||||
|
static void on_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 a command, waits for the '>' prompt. Returns the reply without prompt/CR, or "" on timeout.
|
||||||
|
static String elm_command(const char *cmd, uint32_t timeout_ms)
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
elm_rx = "";
|
||||||
|
unlock();
|
||||||
|
String line = String(cmd) + "\r";
|
||||||
|
if (!chr_write->writeValue((const uint8_t *)line.c_str(), line.length(), !chr_write->canWriteNoResponse()))
|
||||||
|
return "";
|
||||||
|
uint32_t t0 = millis();
|
||||||
|
String rx;
|
||||||
|
while (millis() - t0 < timeout_ms)
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
rx = elm_rx;
|
||||||
|
unlock();
|
||||||
|
if (rx.indexOf('>') >= 0)
|
||||||
|
{
|
||||||
|
rx.replace(">", "");
|
||||||
|
rx.replace("\r", "\n");
|
||||||
|
rx.trim();
|
||||||
|
return rx;
|
||||||
|
}
|
||||||
|
if (!client->isConnected())
|
||||||
|
return "";
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// "410D3C" (spaces/headers removed by ATS0/ATH0; multiple ECUs possible) -> 60
|
||||||
|
static int parse_speed(String reply)
|
||||||
|
{
|
||||||
|
reply.replace(" ", "");
|
||||||
|
reply.toUpperCase();
|
||||||
|
int i = reply.indexOf("410D");
|
||||||
|
if (i < 0 || reply.length() < (unsigned)i + 6)
|
||||||
|
return -1;
|
||||||
|
return (int)strtol(reply.substring(i + 4, i + 6).c_str(), nullptr, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- connection ----------
|
||||||
|
static void disconnect()
|
||||||
|
{
|
||||||
|
chr_write = chr_notify = nullptr;
|
||||||
|
if (client && client->isConnected())
|
||||||
|
client->disconnect();
|
||||||
|
speed_kmh = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the ELM UART characteristics: service FFF0 (verified: FFF1 notify + write), else any notify+write pair
|
||||||
|
static bool find_characteristics()
|
||||||
|
{
|
||||||
|
chr_write = chr_notify = nullptr;
|
||||||
|
NimBLERemoteService *svc = client->getService(NimBLEUUID((uint16_t)0xFFF0));
|
||||||
|
if (svc)
|
||||||
|
{
|
||||||
|
chr_notify = svc->getCharacteristic(NimBLEUUID((uint16_t)0xFFF1));
|
||||||
|
if (chr_notify && (chr_notify->canWrite() || chr_notify->canWriteNoResponse()))
|
||||||
|
chr_write = chr_notify;
|
||||||
|
else
|
||||||
|
chr_write = svc->getCharacteristic(NimBLEUUID((uint16_t)0xFFF2));
|
||||||
|
}
|
||||||
|
if (!chr_write || !chr_notify)
|
||||||
|
{
|
||||||
|
chr_write = chr_notify = nullptr;
|
||||||
|
for (NimBLERemoteService *s : client->getServices(true))
|
||||||
|
{
|
||||||
|
NimBLERemoteCharacteristic *w = nullptr, *n = nullptr;
|
||||||
|
for (NimBLERemoteCharacteristic *c : s->getCharacteristics(true))
|
||||||
|
{
|
||||||
|
if (!n && (c->canNotify() || c->canIndicate()))
|
||||||
|
n = c;
|
||||||
|
if (!w && (c->canWrite() || c->canWriteNoResponse()))
|
||||||
|
w = c;
|
||||||
|
}
|
||||||
|
if (w && n)
|
||||||
|
{
|
||||||
|
chr_write = w;
|
||||||
|
chr_notify = n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chr_write && chr_notify;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool connect_and_init()
|
||||||
|
{
|
||||||
|
bool was_scanning = NimBLEDevice::getScan()->isScanning();
|
||||||
|
scan_set(false); // NimBLE can't connect while scanning
|
||||||
|
|
||||||
|
state = ObdState::Connecting;
|
||||||
|
set_status("Connecting " + (saved_name.length() ? saved_name : saved_addr));
|
||||||
|
if (!client)
|
||||||
|
{
|
||||||
|
client = NimBLEDevice::createClient();
|
||||||
|
client->setConnectTimeout(8000);
|
||||||
|
}
|
||||||
|
bool ok = client->connect(NimBLEAddress(std::string(saved_addr.c_str()), saved_type));
|
||||||
|
if (was_scanning && scan_wanted)
|
||||||
|
scan_set(true);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
set_status("Adapter not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!find_characteristics() || !chr_notify->subscribe(chr_notify->canNotify(), on_notify))
|
||||||
|
{
|
||||||
|
set_status("Not an ELM adapter?");
|
||||||
|
disconnect();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = ObdState::Initializing;
|
||||||
|
set_status("Initializing ELM327");
|
||||||
|
if (elm_command("ATZ", 4000).length() == 0)
|
||||||
|
{
|
||||||
|
set_status("ELM327 not responding");
|
||||||
|
disconnect();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// echo off, no linefeeds, no spaces, no headers, automatic protocol
|
||||||
|
const char *init[] = {"ATE0", "ATL0", "ATS0", "ATH0", "ATSP0"};
|
||||||
|
for (const char *c : init)
|
||||||
|
elm_command(c, 2000);
|
||||||
|
set_status("Searching protocol");
|
||||||
|
elm_command("0100", 10000); // the first OBD request triggers the protocol search
|
||||||
|
state = ObdState::Running;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- worker task ----------
|
||||||
|
static void load_saved()
|
||||||
|
{
|
||||||
|
prefs.begin("obd", true);
|
||||||
|
saved_addr = prefs.getString("addr", "");
|
||||||
|
saved_name = prefs.getString("name", "");
|
||||||
|
saved_type = prefs.getUChar("type", 0);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void worker_task(void *)
|
||||||
|
{
|
||||||
|
uint32_t next_attempt = 0;
|
||||||
|
int fails = 0;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
// requests from the UI
|
||||||
|
if (req_forget)
|
||||||
|
{
|
||||||
|
req_forget = false;
|
||||||
|
disconnect();
|
||||||
|
prefs.begin("obd", false);
|
||||||
|
prefs.clear();
|
||||||
|
prefs.end();
|
||||||
|
lock();
|
||||||
|
saved_addr = saved_name = "";
|
||||||
|
unlock();
|
||||||
|
}
|
||||||
|
if (req_select)
|
||||||
|
{
|
||||||
|
req_select = false;
|
||||||
|
disconnect();
|
||||||
|
prefs.begin("obd", false);
|
||||||
|
prefs.putString("addr", req_dev.address);
|
||||||
|
prefs.putString("name", req_dev.name);
|
||||||
|
prefs.putUChar("type", req_dev.addr_type);
|
||||||
|
prefs.end();
|
||||||
|
lock();
|
||||||
|
saved_addr = req_dev.address;
|
||||||
|
saved_name = req_dev.name;
|
||||||
|
saved_type = req_dev.addr_type;
|
||||||
|
unlock();
|
||||||
|
next_attempt = 0;
|
||||||
|
}
|
||||||
|
if (state != ObdState::Connecting) // never scan during a connect attempt
|
||||||
|
scan_set(scan_wanted);
|
||||||
|
|
||||||
|
if (saved_addr.length() == 0)
|
||||||
|
{
|
||||||
|
state = ObdState::NoDevice;
|
||||||
|
set_status("No adapter - swipe left");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client || !client->isConnected() || !chr_write)
|
||||||
|
{
|
||||||
|
if (state == ObdState::Running)
|
||||||
|
set_status("Connection lost");
|
||||||
|
speed_kmh = -1;
|
||||||
|
if ((int32_t)(millis() - next_attempt) < 0)
|
||||||
|
{
|
||||||
|
state = ObdState::Waiting;
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fails = 0;
|
||||||
|
if (!connect_and_init())
|
||||||
|
{
|
||||||
|
disconnect();
|
||||||
|
state = ObdState::Waiting;
|
||||||
|
next_attempt = millis() + RETRY_DELAY_MS;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// connected: poll the vehicle speed
|
||||||
|
String reply = elm_command("010D", 1500);
|
||||||
|
int v = parse_speed(reply);
|
||||||
|
speed_kmh = v;
|
||||||
|
if (v >= 0)
|
||||||
|
{
|
||||||
|
fails = 0;
|
||||||
|
set_status(saved_name.length() ? saved_name : saved_addr);
|
||||||
|
}
|
||||||
|
else if (reply.length() == 0)
|
||||||
|
{
|
||||||
|
// no prompt at all: adapter hung or link dead
|
||||||
|
if (++fails >= POLL_FAIL_LIMIT)
|
||||||
|
{
|
||||||
|
set_status("Adapter not answering");
|
||||||
|
disconnect();
|
||||||
|
next_attempt = millis() + RETRY_DELAY_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// "NO DATA", "UNABLE TO CONNECT", "STOPPED"...: ignition off or ECU busy
|
||||||
|
fails = 0;
|
||||||
|
String r = reply;
|
||||||
|
r.replace("\n", " ");
|
||||||
|
set_status(r.substring(0, 24));
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- public API ----------
|
||||||
|
void obd_begin()
|
||||||
|
{
|
||||||
|
mtx = xSemaphoreCreateMutex();
|
||||||
|
load_saved();
|
||||||
|
NimBLEDevice::init("JC3636W518-OBD");
|
||||||
|
NimBLEDevice::setPower(9);
|
||||||
|
xTaskCreatePinnedToCore(worker_task, "obd_link", 8192, nullptr, 2, nullptr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObdState obd_state() { return state; }
|
||||||
|
int obd_speed() { return speed_kmh; }
|
||||||
|
|
||||||
|
String obd_status_text()
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
String s = status_text;
|
||||||
|
unlock();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
String obd_saved_name()
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
String s = saved_name;
|
||||||
|
unlock();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
String obd_saved_address()
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
String s = saved_addr;
|
||||||
|
unlock();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void obd_scan_enable(bool on) { scan_wanted = on; }
|
||||||
|
|
||||||
|
bool obd_get_devices(std::vector<ObdDevInfo> &out, uint32_t &version)
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
bool changed = version != devices_version;
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
out = devices;
|
||||||
|
version = devices_version;
|
||||||
|
}
|
||||||
|
unlock();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void obd_select_device(const ObdDevInfo &dev)
|
||||||
|
{
|
||||||
|
req_dev = dev;
|
||||||
|
req_select = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void obd_forget_device() { req_forget = true; }
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// ELM327-over-BLE link: remembers the selected adapter, auto-connects/reconnects and polls the vehicle speed.
|
||||||
|
// Runs in a background task; all functions are safe to call from the UI (loop) task.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
enum class ObdState
|
||||||
|
{
|
||||||
|
NoDevice, // no adapter selected yet
|
||||||
|
Connecting, // BLE connect in progress
|
||||||
|
Initializing, // ELM327 init (ATZ, protocol search)
|
||||||
|
Running, // polling speed
|
||||||
|
Waiting, // connect/init failed or link lost; retrying shortly
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ObdDevInfo
|
||||||
|
{
|
||||||
|
String address;
|
||||||
|
uint8_t addr_type;
|
||||||
|
String name;
|
||||||
|
int rssi;
|
||||||
|
bool obd_hint; // looks like an OBD adapter (name or service FFF0/FFE0/18F0)
|
||||||
|
};
|
||||||
|
|
||||||
|
void obd_begin();
|
||||||
|
|
||||||
|
ObdState obd_state();
|
||||||
|
int obd_speed(); // km/h, -1 = no data (ignition off / not connected)
|
||||||
|
String obd_status_text(); // short human-readable status line
|
||||||
|
String obd_saved_name(); // "" if none
|
||||||
|
String obd_saved_address();
|
||||||
|
|
||||||
|
// Menu: scan for adapters while the menu is open
|
||||||
|
void obd_scan_enable(bool on);
|
||||||
|
bool obd_get_devices(std::vector<ObdDevInfo> &out, uint32_t &version);
|
||||||
|
|
||||||
|
// Select (and remember) an adapter, or forget the remembered one
|
||||||
|
void obd_select_device(const ObdDevInfo &dev);
|
||||||
|
void obd_forget_device();
|
||||||
@@ -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` |
|
||||||
|
| `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` |
|
||||||
| `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` |
|
| `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` | **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) |
|
| `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) |
|
||||||
@@ -121,3 +122,7 @@ AliExpress "ELM327 Mini V2.1 Bluetooth OBD **Dual Mode** 5.1" — dual mode = Bl
|
|||||||
- 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).
|
- **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.
|
- BLE (NimBLE) in the user's home: ~12 devices visible within 10 s.
|
||||||
|
- Big fonts: generate with `npx lv_font_conv` (Node 22 installed) from `lvgl/scripts/generators/built_in_font/Montserrat-Medium.ttf`; `-DLV_LVGL_H_INCLUDE_SIMPLE` (in shared ini) makes the generated `#include` work.
|
||||||
|
- Binary assets (GIFs): `board_build.embed_files = data/x.gif` → `extern const uint8_t x_start[] asm("_binary_data_x_gif_start")` (no huge C arrays).
|
||||||
|
- Serial output right after a reset can be lost/garbled (USB-CDC re-enumeration, prints from two cores interleave) — don't conclude a task isn't running from a missing early line.
|
||||||
|
- `Preferences` prints `nvs_open failed: NOT_FOUND` the first time a namespace is read before it was written — harmless.
|
||||||
@@ -28,6 +28,8 @@ build_flags =
|
|||||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
; LVGL (also when compiled as a library) uses shared/include/lv_conf.h
|
; LVGL (also when compiled as a library) uses shared/include/lv_conf.h
|
||||||
-DLV_CONF_INCLUDE_SIMPLE
|
-DLV_CONF_INCLUDE_SIMPLE
|
||||||
|
; generated LVGL fonts/images include "lvgl.h" directly
|
||||||
|
-DLV_LVGL_H_INCLUDE_SIMPLE
|
||||||
-I${shared.dir}/include
|
-I${shared.dir}/include
|
||||||
|
|
||||||
; shared/lib: JC3636W518 board library
|
; shared/lib: JC3636W518 board library
|
||||||
|
|||||||
Reference in new issue
Block a user