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:
1 parent
0777db3498
commit
be69bda4f1
5 files changed
+258
-16
No files matched your search
@@ -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
@@ -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())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in new issue
Block a user