12-OBD-Speed: settings page with screen rotation (0/90/180/270)
- third tile (swipe left from the menu): rotation buttons, saved in NVS, applied at boot - shared lib: setRotation() rotates only the panel and redraws; touch points are converted in touchpad_read() for the current rotation; getRotation() added Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
733306a9c0
commit
4fc70f13d0
5 files changed
+107
-21
No files matched your search
@@ -9,6 +9,7 @@ Uses the shared board library and LVGL 9.6 (`../shared`), NimBLE-Arduino 2.5.1.
|
|||||||
- **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).
|
||||||
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// 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).
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <Preferences.h>
|
||||||
#include "jc3636w518.h"
|
#include "jc3636w518.h"
|
||||||
#include "obd_link.h"
|
#include "obd_link.h"
|
||||||
|
|
||||||
@@ -19,7 +21,8 @@ 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_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;
|
static lv_obj_t *tileview, *tile_speed, *tile_menu, *tile_settings;
|
||||||
|
static lv_obj_t *rot_buttons[4];
|
||||||
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 *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];
|
||||||
@@ -79,6 +82,33 @@ static void on_done_clicked(lv_event_t *e)
|
|||||||
show_tile(tile_speed);
|
show_tile(tile_speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- settings: screen rotation ----------
|
||||||
|
static uint8_t load_rotation()
|
||||||
|
{
|
||||||
|
Preferences p;
|
||||||
|
p.begin("ui", true);
|
||||||
|
uint8_t r = p.getUChar("rot", 0);
|
||||||
|
p.end();
|
||||||
|
return r > 3 ? 0 : r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void highlight_rotation(uint8_t rot)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
lv_obj_set_style_bg_color(rot_buttons[i], i == rot ? COLOR_ARC : lv_color_hex(0x222A33), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_rotation_clicked(lv_event_t *e)
|
||||||
|
{
|
||||||
|
uint8_t rot = (uint8_t)(intptr_t)lv_event_get_user_data(e);
|
||||||
|
setRotation(rot);
|
||||||
|
highlight_rotation(rot);
|
||||||
|
Preferences p;
|
||||||
|
p.begin("ui", false);
|
||||||
|
p.putUChar("rot", rot);
|
||||||
|
p.end();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- speed page ----------
|
// ---------- speed page ----------
|
||||||
static void build_speed_tile()
|
static void build_speed_tile()
|
||||||
{
|
{
|
||||||
@@ -178,6 +208,38 @@ static void build_menu_tile()
|
|||||||
make_button(tile_menu, "Done", 50, 296, 92, on_done_clicked);
|
make_button(tile_menu, "Done", 50, 296, 92, on_done_clicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void build_settings_tile()
|
||||||
|
{
|
||||||
|
lv_obj_t *title = lv_label_create(tile_settings);
|
||||||
|
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, "Settings");
|
||||||
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
|
||||||
|
|
||||||
|
lv_obj_t *label = lv_label_create(tile_settings);
|
||||||
|
lv_obj_set_style_text_font(label, &lv_font_montserrat_16, 0);
|
||||||
|
lv_obj_set_style_text_color(label, COLOR_DIM, 0);
|
||||||
|
lv_label_set_text(label, "Screen rotation (degrees)");
|
||||||
|
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 86);
|
||||||
|
|
||||||
|
static const char *names[4] = {"0", "90", "180", "270"};
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
lv_obj_t *b = lv_button_create(tile_settings);
|
||||||
|
lv_obj_set_size(b, 104, 48);
|
||||||
|
lv_obj_align(b, LV_ALIGN_TOP_MID, (i % 2) ? 58 : -58, 118 + (i / 2) * 60);
|
||||||
|
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_label_set_text(l, names[i]);
|
||||||
|
lv_obj_set_style_text_font(l, &lv_font_montserrat_20, 0);
|
||||||
|
lv_obj_center(l);
|
||||||
|
rot_buttons[i] = b;
|
||||||
|
}
|
||||||
|
highlight_rotation(getRotation());
|
||||||
|
|
||||||
|
make_button(tile_settings, "Done", 0, 296, 110, on_done_clicked);
|
||||||
|
}
|
||||||
|
|
||||||
static void build_ui()
|
static void build_ui()
|
||||||
{
|
{
|
||||||
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x000000), 0);
|
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x000000), 0);
|
||||||
@@ -185,10 +247,12 @@ static void build_ui()
|
|||||||
lv_obj_set_style_bg_color(tileview, lv_color_hex(0x000000), 0);
|
lv_obj_set_style_bg_color(tileview, lv_color_hex(0x000000), 0);
|
||||||
lv_obj_set_scrollbar_mode(tileview, LV_SCROLLBAR_MODE_OFF);
|
lv_obj_set_scrollbar_mode(tileview, LV_SCROLLBAR_MODE_OFF);
|
||||||
tile_speed = lv_tileview_add_tile(tileview, 0, 0, LV_DIR_RIGHT);
|
tile_speed = lv_tileview_add_tile(tileview, 0, 0, LV_DIR_RIGHT);
|
||||||
tile_menu = lv_tileview_add_tile(tileview, 1, 0, LV_DIR_LEFT);
|
tile_menu = lv_tileview_add_tile(tileview, 1, 0, (lv_dir_t)(LV_DIR_LEFT | LV_DIR_RIGHT));
|
||||||
|
tile_settings = lv_tileview_add_tile(tileview, 2, 0, LV_DIR_LEFT);
|
||||||
lv_obj_add_event_cb(tileview, on_tile_changed, LV_EVENT_VALUE_CHANGED, NULL);
|
lv_obj_add_event_cb(tileview, on_tile_changed, LV_EVENT_VALUE_CHANGED, NULL);
|
||||||
build_speed_tile();
|
build_speed_tile();
|
||||||
build_menu_tile();
|
build_menu_tile();
|
||||||
|
build_settings_tile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- periodic updates ----------
|
// ---------- periodic updates ----------
|
||||||
@@ -279,6 +343,7 @@ void setup()
|
|||||||
delay(200);
|
delay(200);
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
scr_lvgl_init();
|
scr_lvgl_init();
|
||||||
|
setRotation(load_rotation());
|
||||||
build_ui();
|
build_ui();
|
||||||
obd_begin();
|
obd_begin();
|
||||||
if (obd_saved_address().length() == 0)
|
if (obd_saved_address().length() == 0)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ If upload fails: hold BOOT (GPIO0) while plugging in / resetting.
|
|||||||
|
|
||||||
## Display driver pattern (shared board library `shared/lib/JC3636W518`, was the vendor `scr_st77916.h`)
|
## Display driver pattern (shared board library `shared/lib/JC3636W518`, was the vendor `scr_st77916.h`)
|
||||||
`scr_lvgl_init()` does everything: LEDC backlight → CST816S on I2C @400 kHz → QSPI bus @**80 MHz** (vendor: 50) → `ESP_PanelLcd_ST77916(bus, 16bpp, RST)` → `invertColor(true)` → `lv_init()` + tick → LVGL display with **two 72-row draw buffers** (2 × ~52 KB internal RAM) → flush callback with DMA-done callback → touch indev. Returns the `lv_display_t*`.
|
`scr_lvgl_init()` does everything: LEDC backlight → CST816S on I2C @400 kHz → QSPI bus @**80 MHz** (vendor: 50) → `ESP_PanelLcd_ST77916(bus, 16bpp, RST)` → `invertColor(true)` → `lv_init()` + tick → LVGL display with **two 72-row draw buffers** (2 × ~52 KB internal RAM) → flush callback with DMA-done callback → touch indev. Returns the `lv_display_t*`.
|
||||||
Helpers: `setRotation(0..3)`, `set_brightness(0..100)`, `screen_switch(bool)`; `perf_monitor.h` prints FPS/refresh/other/CPU once per second.
|
Helpers: `setRotation(0..3)` / `getRotation()` (panel rotates via MADCTL swap/mirror like the vendor table; touch converted in software in `touchpad_read`), `set_brightness(0..100)`, `screen_switch(bool)`; `perf_monitor.h` prints FPS/refresh/other/CPU once per second.
|
||||||
Display color format is `RGB565_SWAPPED` (ST77916 byte order, set in `lv_conf.h`).
|
Display color format is `RGB565_SWAPPED` (ST77916 byte order, set in `lv_conf.h`).
|
||||||
|
|
||||||
## Folder contents
|
## Folder contents
|
||||||
|
|||||||
@@ -272,11 +272,15 @@ IRAM_ATTR bool onRefreshFinishCallback(void *user_data)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint8_t rotation = 0; // 0..3, clockwise in 90° steps
|
||||||
|
|
||||||
|
// The panel rotates the image itself (MADCTL swap/mirror, same combinations as the vendor code);
|
||||||
|
// touch points are converted in touchpad_read() so they always match what is shown.
|
||||||
void setRotation(uint8_t rot)
|
void setRotation(uint8_t rot)
|
||||||
{
|
{
|
||||||
if (rot > 3)
|
if (rot > 3)
|
||||||
return;
|
return;
|
||||||
if (lcd == NULL || touch == NULL)
|
if (lcd == NULL)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
switch (rot)
|
switch (rot)
|
||||||
@@ -285,35 +289,31 @@ void setRotation(uint8_t rot)
|
|||||||
lcd->swapXY(true);
|
lcd->swapXY(true);
|
||||||
lcd->mirrorX(true);
|
lcd->mirrorX(true);
|
||||||
lcd->mirrorY(false);
|
lcd->mirrorY(false);
|
||||||
touch->swapXY(true);
|
|
||||||
touch->mirrorX(true);
|
|
||||||
touch->mirrorY(false);
|
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2: // 180°
|
||||||
lcd->swapXY(false);
|
lcd->swapXY(false);
|
||||||
lcd->mirrorX(true);
|
lcd->mirrorX(true);
|
||||||
lcd->mirrorY(true);
|
lcd->mirrorY(true);
|
||||||
touch->swapXY(false);
|
|
||||||
touch->mirrorX(true);
|
|
||||||
touch->mirrorY(true);
|
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3: // 270° clockwise
|
||||||
lcd->swapXY(true);
|
lcd->swapXY(true);
|
||||||
lcd->mirrorX(false);
|
lcd->mirrorX(false);
|
||||||
lcd->mirrorY(true);
|
lcd->mirrorY(true);
|
||||||
touch->swapXY(true);
|
|
||||||
touch->mirrorX(false);
|
|
||||||
touch->mirrorY(true);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
lcd->swapXY(false);
|
lcd->swapXY(false);
|
||||||
lcd->mirrorX(false);
|
lcd->mirrorX(false);
|
||||||
lcd->mirrorY(false);
|
lcd->mirrorY(false);
|
||||||
touch->swapXY(false);
|
|
||||||
touch->mirrorX(false);
|
|
||||||
touch->mirrorY(false);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
rotation = rot;
|
||||||
|
if (disp)
|
||||||
|
lv_obj_invalidate(lv_display_get_screen_active(disp)); // redraw everything in the new orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getRotation()
|
||||||
|
{
|
||||||
|
return rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
void screen_switch(bool on)
|
void screen_switch(bool on)
|
||||||
@@ -341,8 +341,27 @@ static void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data)
|
|||||||
int read_touch_result = tp->readPoints(&point, 1);
|
int read_touch_result = tp->readPoints(&point, 1);
|
||||||
if (read_touch_result > 0)
|
if (read_touch_result > 0)
|
||||||
{
|
{
|
||||||
data->point.x = point.x;
|
// physical panel coordinates -> coordinates of the rotated image
|
||||||
data->point.y = point.y;
|
const int max = SCREEN_RES_HOR - 1; // square display
|
||||||
|
switch (rotation)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
data->point.x = point.y;
|
||||||
|
data->point.y = max - point.x;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
data->point.x = max - point.x;
|
||||||
|
data->point.y = max - point.y;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
data->point.x = max - point.y;
|
||||||
|
data->point.y = point.x;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
data->point.x = point.x;
|
||||||
|
data->point.y = point.y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
data->state = LV_INDEV_STATE_PRESSED;
|
data->state = LV_INDEV_STATE_PRESSED;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
// Initializes backlight, touch, LCD and LVGL (lv_init, tick, display, touch input). Returns the display.
|
// Initializes backlight, touch, LCD and LVGL (lv_init, tick, display, touch input). Returns the display.
|
||||||
lv_display_t *scr_lvgl_init();
|
lv_display_t *scr_lvgl_init();
|
||||||
|
|
||||||
// 0 = normal, 1 = 90° clockwise, 2 = 180°, 3 = 270° (LCD and touch together)
|
// 0 = normal, 1 = 90° clockwise, 2 = 180°, 3 = 270° (LCD and touch together; call after scr_lvgl_init)
|
||||||
void setRotation(uint8_t rot);
|
void setRotation(uint8_t rot);
|
||||||
|
uint8_t getRotation();
|
||||||
|
|
||||||
void screen_switch(bool on);
|
void screen_switch(bool on);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user