12-OBD-Speed: battery voltage arc and idle/screen-off mode

- ATRV every 2 s; voltage arc in the bottom gap (11-15 V) + number,
  colored by state (low / weak / resting / charging / overcharge)
- idle after Off/2/5/10 min without speed data (settings page, NVS):
  screen off, GIF paused, OBD polling stopped, ATRV every 30 s;
  wakes on touch (swallowed by a top-layer overlay) or > 13.2 V
- status pill moved up to make room for the voltage

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
patriandClaude Opus 5.5 committed 2026-10-23 18:32:56 +02:00
1 parent 0777db3498
commit be69bda4f1
5 files changed
+258 -16

No files matched your search

+8 -2
View File
@@ -5,11 +5,17 @@ Uses the shared board library and LVGL 9.6 (`../shared`), NimBLE-Arduino 2.5.1.
## Use ## Use
- **Speed page** — speed in km/h (`--` = no data: not connected, or ignition off), arc gauge 0–200 km/h, - **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). status pill below the number (adapter name when running, otherwise what it is doing).
**Battery voltage** (`ATRV`, every 2 s) as a small arc in the bottom gap (11–15 V) + number; color:
red < 11.8 V, orange < 12.2 V, green < 13.0 V (resting OK), blue ≤ 14.8 V (charging), red above (overcharge).
- **Swipe left → adapter menu** — scans while open. OBD-like adapters (name OBD/ELM/…, or service FFF0/FFE0/18F0) - **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. 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. **Forget** clears the remembered adapter, **Done** (or swipe right) returns to the speed page.
- **Swipe left again → settings** — screen rotation 0 / 90 / 180 / 270 degrees (tap to apply; remembered in NVS, applied at boot). - **Swipe left again → settings** — screen rotation 0 / 90 / 180 / 270 degrees, and **screen off without data** Off / 2 / 5 / 10 min (default 2). Both remembered in NVS.
- **Idle** — after that time without speed data (ignition off, adapter gone) the screen goes dark, the GIF pauses and
OBD polling stops; only `ATRV` every 30 s (answered by the adapter itself, doesn't wake the car's bus).
Wakes on a touch (the touch is swallowed) or by itself when the voltage rises above 13.2 V (engine started).
Not used while no adapter is chosen.
- On power-up the remembered adapter is connected automatically; if the link drops or the adapter is not - 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. there (car off), it retries every 3 s. With no adapter remembered, the device starts on the menu page.
+159 -10
View File
@@ -1,12 +1,16 @@
// OBD speed display for the JC3636W518: vehicle speed from an ELM327 over BLE on top of an animated GIF. // 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. // Swipe left for the adapter menu (scan, select, forget); the selected adapter is remembered and reconnected.
// Swipe left again for settings (screen rotation, remembered). // Swipe left again for settings (screen rotation, idle timeout; remembered).
// Battery voltage is shown as a small arc in the bottom gap; after a while without speed data the
// screen goes dark (idle) and wakes on touch or when the engine starts.
#include <Arduino.h> #include <Arduino.h>
#include <Preferences.h> #include <Preferences.h>
#include "jc3636w518.h" #include "jc3636w518.h"
#include "obd_link.h" #include "obd_link.h"
#define SPEED_MAX 200 #define SPEED_MAX 200
#define VOLT_MIN_X10 110 // voltage arc range 11.0 .. 15.0 V
#define VOLT_MAX_X10 150
#define MAX_MENU_ITEMS 30 #define MAX_MENU_ITEMS 30
LV_FONT_DECLARE(font_speed_120); LV_FONT_DECLARE(font_speed_120);
@@ -22,8 +26,10 @@ static const lv_color_t COLOR_OBD = lv_color_hex(0x4CD964);
static const lv_color_t COLOR_PANEL = lv_color_hex(0x161C22); static const lv_color_t COLOR_PANEL = lv_color_hex(0x161C22);
static lv_obj_t *tileview, *tile_speed, *tile_menu, *tile_settings; static lv_obj_t *tileview, *tile_speed, *tile_menu, *tile_settings;
static lv_obj_t *rot_buttons[4]; static lv_obj_t *rot_buttons[4], *idle_buttons[4];
static const uint8_t IDLE_MINUTES[4] = {0, 2, 5, 10}; // 0 = never
static lv_obj_t *speed_arc, *speed_label, *speed_shadow, *status_label; static lv_obj_t *speed_arc, *speed_label, *speed_shadow, *status_label;
static lv_obj_t *volt_arc, *volt_label, *bg_gif, *idle_overlay;
static lv_obj_t *menu_saved, *menu_status, *menu_list; static lv_obj_t *menu_saved, *menu_status, *menu_list;
static lv_obj_t *menu_items[MAX_MENU_ITEMS]; static lv_obj_t *menu_items[MAX_MENU_ITEMS];
static int menu_item_count = 0; static int menu_item_count = 0;
@@ -109,6 +115,38 @@ static void on_rotation_clicked(lv_event_t *e)
p.end(); p.end();
} }
// ---------- settings: idle timeout ----------
static uint8_t load_idle_minutes()
{
Preferences p;
p.begin("ui", true);
uint8_t m = p.getUChar("idle", 2);
p.end();
return m;
}
static void highlight_idle(uint8_t minutes)
{
for (int i = 0; i < 4; i++)
lv_obj_set_style_bg_color(idle_buttons[i], IDLE_MINUTES[i] == minutes ? COLOR_ARC : lv_color_hex(0x222A33), 0);
}
static void on_idle_clicked(lv_event_t *e)
{
uint8_t minutes = IDLE_MINUTES[(int)(intptr_t)lv_event_get_user_data(e)];
obd_set_idle_timeout(minutes);
highlight_idle(minutes);
Preferences p;
p.begin("ui", false);
p.putUChar("idle", minutes);
p.end();
}
static void on_idle_overlay_pressed(lv_event_t *e)
{
obd_wake(); // touch while the screen is dark: wake up (the touch itself is swallowed)
}
// ---------- speed page ---------- // ---------- speed page ----------
static void build_speed_tile() static void build_speed_tile()
{ {
@@ -120,10 +158,10 @@ static void build_speed_tile()
gif_dsc.header.h = bg_gif_start[8] | (bg_gif_start[9] << 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_size = bg_gif_end - bg_gif_start;
gif_dsc.data = bg_gif_start; gif_dsc.data = bg_gif_start;
lv_obj_t *gif = lv_gif_create(tile_speed); bg_gif = lv_gif_create(tile_speed);
lv_gif_set_color_format(gif, lv_display_get_color_format(lv_display_get_default())); lv_gif_set_color_format(bg_gif, lv_display_get_color_format(lv_display_get_default()));
lv_gif_set_src(gif, &gif_dsc); lv_gif_set_src(bg_gif, &gif_dsc);
lv_obj_center(gif); lv_obj_center(bg_gif);
// arc gauge along the round edge: 270° sweep, gap at the bottom // arc gauge along the round edge: 270° sweep, gap at the bottom
speed_arc = lv_arc_create(tile_speed); speed_arc = lv_arc_create(tile_speed);
@@ -141,6 +179,28 @@ static void build_speed_tile()
lv_obj_set_style_arc_opa(speed_arc, LV_OPA_30, 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); lv_obj_set_style_arc_color(speed_arc, COLOR_ARC, LV_PART_INDICATOR);
// battery voltage arc in the bottom gap of the speed arc, filling left to right
volt_arc = lv_arc_create(tile_speed);
lv_obj_set_size(volt_arc, 344, 344);
lv_obj_center(volt_arc);
lv_arc_set_rotation(volt_arc, 45);
lv_arc_set_bg_angles(volt_arc, 8, 82); // small margin to the speed arc ends
lv_arc_set_mode(volt_arc, LV_ARC_MODE_REVERSE); // starts at the left end (bottom-left)
lv_arc_set_range(volt_arc, VOLT_MIN_X10, VOLT_MAX_X10);
lv_arc_set_value(volt_arc, VOLT_MIN_X10);
lv_obj_remove_style(volt_arc, NULL, LV_PART_KNOB);
lv_obj_remove_flag(volt_arc, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_arc_width(volt_arc, 12, LV_PART_MAIN);
lv_obj_set_style_arc_width(volt_arc, 12, LV_PART_INDICATOR);
lv_obj_set_style_arc_color(volt_arc, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_arc_opa(volt_arc, LV_OPA_30, LV_PART_MAIN);
volt_label = lv_label_create(tile_speed);
lv_obj_set_style_text_font(volt_label, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(volt_label, COLOR_DIM, 0);
lv_label_set_text(volt_label, "-- V");
lv_obj_align(volt_label, LV_ALIGN_BOTTOM_MID, 0, -30);
// speed number with a soft drop shadow so it stays readable on the GIF // speed number with a soft drop shadow so it stays readable on the GIF
speed_shadow = lv_label_create(tile_speed); speed_shadow = lv_label_create(tile_speed);
lv_obj_set_style_text_font(speed_shadow, &font_speed_120, 0); lv_obj_set_style_text_font(speed_shadow, &font_speed_120, 0);
@@ -171,7 +231,7 @@ static void build_speed_tile()
lv_obj_set_style_pad_hor(status_label, 10, 0); lv_obj_set_style_pad_hor(status_label, 10, 0);
lv_obj_set_style_pad_ver(status_label, 3, 0); lv_obj_set_style_pad_ver(status_label, 3, 0);
lv_label_set_text(status_label, ""); lv_label_set_text(status_label, "");
lv_obj_align(status_label, LV_ALIGN_BOTTOM_MID, 0, -34); lv_obj_align(status_label, LV_ALIGN_CENTER, 0, 100);
} }
// ---------- menu page ---------- // ---------- menu page ----------
@@ -220,14 +280,14 @@ static void build_settings_tile()
lv_obj_set_style_text_font(label, &lv_font_montserrat_16, 0); lv_obj_set_style_text_font(label, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(label, COLOR_DIM, 0); lv_obj_set_style_text_color(label, COLOR_DIM, 0);
lv_label_set_text(label, "Screen rotation (degrees)"); lv_label_set_text(label, "Screen rotation (degrees)");
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 86); lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 54);
static const char *names[4] = {"0", "90", "180", "270"}; static const char *names[4] = {"0", "90", "180", "270"};
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
lv_obj_t *b = lv_button_create(tile_settings); lv_obj_t *b = lv_button_create(tile_settings);
lv_obj_set_size(b, 104, 48); lv_obj_set_size(b, 96, 40);
lv_obj_align(b, LV_ALIGN_TOP_MID, (i % 2) ? 58 : -58, 118 + (i / 2) * 60); lv_obj_align(b, LV_ALIGN_TOP_MID, (i % 2) ? 52 : -52, 80 + (i / 2) * 46);
lv_obj_add_event_cb(b, on_rotation_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i); lv_obj_add_event_cb(b, on_rotation_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i);
lv_obj_t *l = lv_label_create(b); lv_obj_t *l = lv_label_create(b);
lv_label_set_text(l, names[i]); lv_label_set_text(l, names[i]);
@@ -237,6 +297,27 @@ static void build_settings_tile()
} }
highlight_rotation(getRotation()); highlight_rotation(getRotation());
lv_obj_t *idle_label = lv_label_create(tile_settings);
lv_obj_set_style_text_font(idle_label, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(idle_label, COLOR_DIM, 0);
lv_label_set_text(idle_label, "Screen off without data (min)");
lv_obj_align(idle_label, LV_ALIGN_TOP_MID, 0, 180);
static const char *idle_names[4] = {"Off", "2", "5", "10"};
for (int i = 0; i < 4; i++)
{
lv_obj_t *b = lv_button_create(tile_settings);
lv_obj_set_size(b, 58, 40);
lv_obj_align(b, LV_ALIGN_TOP_MID, -99 + i * 66, 206);
lv_obj_add_event_cb(b, on_idle_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i);
lv_obj_t *l = lv_label_create(b);
lv_label_set_text(l, idle_names[i]);
lv_obj_set_style_text_font(l, &lv_font_montserrat_16, 0);
lv_obj_center(l);
idle_buttons[i] = b;
}
highlight_idle(load_idle_minutes());
make_button(tile_settings, "Done", 0, 296, 110, on_done_clicked); make_button(tile_settings, "Done", 0, 296, 110, on_done_clicked);
} }
@@ -253,9 +334,74 @@ static void build_ui()
build_speed_tile(); build_speed_tile();
build_menu_tile(); build_menu_tile();
build_settings_tile(); build_settings_tile();
idle_overlay = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(idle_overlay);
lv_obj_set_size(idle_overlay, SCREEN_RES_HOR, SCREEN_RES_VER);
lv_obj_add_flag(idle_overlay, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(idle_overlay, on_idle_overlay_pressed, LV_EVENT_PRESSED, NULL);
lv_obj_add_flag(idle_overlay, LV_OBJ_FLAG_HIDDEN);
} }
// ---------- periodic updates ---------- // ---------- periodic updates ----------
static lv_color_t voltage_color(float v)
{
if (v < 0)
return COLOR_DIM;
if (v < 11.8f)
return lv_color_hex(0xFF3B30); // red: battery low
if (v < 12.2f)
return lv_color_hex(0xFF9500); // orange: getting weak
if (v < 13.0f)
return lv_color_hex(0x4CD964); // green: resting, OK
if (v <= 14.8f)
return lv_color_hex(0x0A84FF); // blue: charging (engine running)
return lv_color_hex(0xFF3B30); // red: overcharging
}
static void update_voltage()
{
static int shown_x10 = -2;
float v = obd_voltage();
int x10 = v < 0 ? -1 : (int)(v * 10 + 0.5f);
if (x10 == shown_x10)
return;
shown_x10 = x10;
lv_color_t c = voltage_color(v);
lv_arc_set_value(volt_arc, x10 < 0 ? VOLT_MIN_X10 : constrain(x10, VOLT_MIN_X10, VOLT_MAX_X10));
lv_obj_set_style_arc_color(volt_arc, c, LV_PART_INDICATOR);
char buf[12];
if (x10 < 0)
strcpy(buf, "-- V");
else
snprintf(buf, sizeof(buf), "%d.%d V", x10 / 10, x10 % 10);
lv_label_set_text(volt_label, buf);
lv_obj_set_style_text_color(volt_label, c, 0);
}
static void update_idle()
{
static bool shown_idle = false;
bool idle = obd_is_idle();
if (idle == shown_idle)
return;
shown_idle = idle;
if (idle)
{
// screen off, animation paused, touches only wake up
lv_tileview_set_tile(tileview, tile_speed, LV_ANIM_OFF);
lv_gif_pause(bg_gif);
lv_obj_remove_flag(idle_overlay, LV_OBJ_FLAG_HIDDEN);
screen_switch(false);
}
else
{
lv_gif_resume(bg_gif);
lv_obj_add_flag(idle_overlay, LV_OBJ_FLAG_HIDDEN);
screen_switch(true);
}
}
static void update_speed() static void update_speed()
{ {
static int shown = -2; static int shown = -2;
@@ -345,6 +491,7 @@ void setup()
scr_lvgl_init(); scr_lvgl_init();
setRotation(load_rotation()); setRotation(load_rotation());
build_ui(); build_ui();
obd_set_idle_timeout(load_idle_minutes());
obd_begin(); obd_begin();
if (obd_saved_address().length() == 0) if (obd_saved_address().length() == 0)
{ {
@@ -363,6 +510,8 @@ void loop()
{ {
last_speed = now; last_speed = now;
update_speed(); update_speed();
update_voltage();
update_idle();
} }
if (now - last_menu >= 500 && menu_visible()) if (now - last_menu >= 500 && menu_visible())
{ {
+78 -2
View File
@@ -4,12 +4,20 @@
#include <Preferences.h> #include <Preferences.h>
#define RETRY_DELAY_MS 3000 #define RETRY_DELAY_MS 3000
#define IDLE_RETRY_DELAY_MS 30000
#define POLL_FAIL_LIMIT 3 #define POLL_FAIL_LIMIT 3
#define VOLTAGE_INTERVAL_MS 2000 // ATRV while driving
#define IDLE_VOLTAGE_INTERVAL_MS 30000 // ATRV while idle (adapter-internal, doesn't wake the car's bus)
#define WAKE_VOLTAGE 13.2f // above this the alternator is charging: engine running
// ---------- shared state (protected by `mtx`) ---------- // ---------- shared state (protected by `mtx`) ----------
static SemaphoreHandle_t mtx; static SemaphoreHandle_t mtx;
static volatile ObdState state = ObdState::NoDevice; static volatile ObdState state = ObdState::NoDevice;
static volatile int speed_kmh = -1; static volatile int speed_kmh = -1;
static volatile float voltage = -1;
static volatile bool idle = false;
static volatile uint32_t idle_timeout_ms = 2 * 60 * 1000;
static volatile uint32_t last_data_ms = 0; // last valid speed (or wake-up)
static String status_text = "starting"; static String status_text = "starting";
static String saved_addr, saved_name; static String saved_addr, saved_name;
static uint8_t saved_type = 0; static uint8_t saved_type = 0;
@@ -18,7 +26,7 @@ static uint32_t devices_version = 1;
static String elm_rx; // bytes received from the ELM (notifications) static String elm_rx; // bytes received from the ELM (notifications)
// ---------- requests from the UI ---------- // ---------- requests from the UI ----------
static volatile bool req_select = false, req_forget = false; static volatile bool req_select = false, req_forget = false, req_wake = false;
static volatile bool scan_wanted = false; static volatile bool scan_wanted = false;
static ObdDevInfo req_dev; static ObdDevInfo req_dev;
@@ -155,6 +163,23 @@ static int parse_speed(String reply)
return (int)strtol(reply.substring(i + 4, i + 6).c_str(), nullptr, 16); return (int)strtol(reply.substring(i + 4, i + 6).c_str(), nullptr, 16);
} }
// "12.6V" -> 12.6, -1 if not a voltage
static float parse_voltage(const String &reply)
{
int v = reply.indexOf('V');
if (v <= 0)
return -1;
float f = reply.substring(0, v).toFloat();
return f > 0 ? f : -1;
}
static void read_voltage()
{
float v = parse_voltage(elm_command("ATRV", 1500));
if (v > 0)
voltage = v;
}
// ---------- connection ---------- // ---------- connection ----------
static void disconnect() static void disconnect()
{ {
@@ -259,9 +284,28 @@ static void load_saved()
static void worker_task(void *) static void worker_task(void *)
{ {
uint32_t next_attempt = 0; uint32_t next_attempt = 0;
uint32_t next_voltage = 0;
int fails = 0; int fails = 0;
last_data_ms = millis();
for (;;) for (;;)
{ {
if (req_wake)
{
req_wake = false;
if (idle)
Serial.println("[obd] wake (touch)");
idle = false;
last_data_ms = millis();
next_attempt = 0;
}
// no speed data for a while (ignition off / adapter gone): go idle
if (!idle && idle_timeout_ms && millis() - last_data_ms > idle_timeout_ms)
{
idle = true;
speed_kmh = -1;
Serial.println("[obd] idle");
}
// requests from the UI // requests from the UI
if (req_forget) if (req_forget)
{ {
@@ -296,6 +340,8 @@ static void worker_task(void *)
if (saved_addr.length() == 0) if (saved_addr.length() == 0)
{ {
state = ObdState::NoDevice; state = ObdState::NoDevice;
idle = false;
last_data_ms = millis(); // no idle while no adapter is chosen (user is setting up)
set_status("No adapter - swipe left"); set_status("No adapter - swipe left");
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
continue; continue;
@@ -317,11 +363,36 @@ static void worker_task(void *)
{ {
disconnect(); disconnect();
state = ObdState::Waiting; state = ObdState::Waiting;
next_attempt = millis() + RETRY_DELAY_MS; next_attempt = millis() + (idle ? IDLE_RETRY_DELAY_MS : RETRY_DELAY_MS);
} }
next_voltage = 0;
continue; continue;
} }
if (idle)
{
// only the battery voltage now and then; an engine start (charging voltage) wakes us up
if ((int32_t)(millis() - next_voltage) >= 0)
{
next_voltage = millis() + IDLE_VOLTAGE_INTERVAL_MS;
read_voltage();
if (voltage > WAKE_VOLTAGE)
{
idle = false;
last_data_ms = millis();
Serial.printf("[obd] wake (%.1f V)\n", voltage);
}
}
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
if ((int32_t)(millis() - next_voltage) >= 0)
{
next_voltage = millis() + VOLTAGE_INTERVAL_MS;
read_voltage();
}
// connected: poll the vehicle speed // connected: poll the vehicle speed
String reply = elm_command("010D", 1500); String reply = elm_command("010D", 1500);
int v = parse_speed(reply); int v = parse_speed(reply);
@@ -329,6 +400,7 @@ static void worker_task(void *)
if (v >= 0) if (v >= 0)
{ {
fails = 0; fails = 0;
last_data_ms = millis();
set_status(saved_name.length() ? saved_name : saved_addr); set_status(saved_name.length() ? saved_name : saved_addr);
} }
else if (reply.length() == 0) else if (reply.length() == 0)
@@ -365,6 +437,10 @@ void obd_begin()
ObdState obd_state() { return state; } ObdState obd_state() { return state; }
int obd_speed() { return speed_kmh; } int obd_speed() { return speed_kmh; }
float obd_voltage() { return voltage; }
bool obd_is_idle() { return idle; }
void obd_wake() { req_wake = true; }
void obd_set_idle_timeout(uint32_t minutes) { idle_timeout_ms = minutes * 60 * 1000; }
String obd_status_text() String obd_status_text()
{ {
+7
View File
@@ -28,6 +28,13 @@ void obd_begin();
ObdState obd_state(); ObdState obd_state();
int obd_speed(); // km/h, -1 = no data (ignition off / not connected) int obd_speed(); // km/h, -1 = no data (ignition off / not connected)
String obd_status_text(); // short human-readable status line String obd_status_text(); // short human-readable status line
float obd_voltage(); // battery voltage from ATRV (V), -1 = unknown
// Idle: after `minutes` without speed data (0 = never) polling stops; only ATRV every 30 s.
// Leaves idle by itself when the voltage shows the engine running (> 13.2 V), or via obd_wake().
void obd_set_idle_timeout(uint32_t minutes);
bool obd_is_idle();
void obd_wake();
String obd_saved_name(); // "" if none String obd_saved_name(); // "" if none
String obd_saved_address(); String obd_saved_address();
+6 -2
View File
@@ -79,7 +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 — 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` | | `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, battery-voltage arc in the bottom gap; swipe left for the adapter menu (scan, select → remembered in NVS, auto-reconnect every 3 s), again for settings (rotation, idle/screen-off timeout). 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-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 |
| `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 |
@@ -93,6 +93,9 @@ Display color format is `RGB565_SWAPPED` (ST77916 byte order, set in `lv_conf.h`
AIDA64 secondary screen (18 styles), mic spectrum, MP3 player (`/music` on SD), photo frame (`/pic`, 360×360 JPEG), MJPEG player (`/mjpeg`), weather, theme clocks, WiFi provisioning AP "My App"/12345678 + web config at device IP. AIDA64 secondary screen (18 styles), mic spectrum, MP3 player (`/music` on SD), photo frame (`/pic`, 360×360 JPEG), MJPEG player (`/mjpeg`), weather, theme clocks, WiFi provisioning AP "My App"/12345678 + web config at device IP.
Re-flash it with Flash Download Tool: ESP32-S3, `1.85_demo.bin` @ 0x0. Re-flash it with Flash Download Tool: ESP32-S3, `1.85_demo.bin` @ 0x0.
## Car battery / idle
Cheap ELM327 clones draw ~20–50 mA permanently (OBD pin 16 = always 12 V); the display ~180 mA if its USB stays powered. `12-OBD-Speed` idle mode stops OBD polling after N min without data so the car's ECUs can sleep; `ATRV` (adapter-internal) every 30 s detects an engine start (> 13.2 V).
## 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, with the since-removed BLE scanner tool; log in `12-OBD-Speed/docs/2026-10-23_ble_scanner_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`):**
@@ -123,5 +126,6 @@ AliExpress "ELM327 Mini V2.1 Bluetooth OBD **Dual Mode** 5.1" — dual mode = Bl
- 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. - 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). - 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. - Python edit scripts: never pass C strings containing `\n` through a bash heredoc into Python — it turned into a real line break twice; write the script with the Write tool instead.
- Serial output from the background task (core 0) is often lost, not only right after reset; don't rely on it as proof. 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. - `Preferences` prints `nvs_open failed: NOT_FOUND` the first time a namespace is read before it was written — harmless.