12-OBD-Speed: choose the background GIF from the SD card
- background page (4th tile): built-in or any GIF in /gifs on the SD card - chosen GIF is copied into a raw 11.9 MB 'bggif' flash partition (header written last) and played memory-mapped from flash; SD only needed when choosing or when the file changed (checked at boot in the background) - own partitions.csv: 4 MB app + bggif + NVS - tools/gif_to_360.py: convert GIFs to 360x360 keeping frame timing; converted output (12-OBD-Speed/sd/) is git-ignored Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d5561be5a
commit
c9fb70ad10
9 files changed
+651
-17
No files matched your search
+17
-2
@@ -12,6 +12,10 @@ Uses the shared board library and LVGL 9.6 (`../shared`), NimBLE-Arduino 2.5.1.
|
||||
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.
|
||||
- **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.
|
||||
- **Swipe left once more → background** — `Built-in` plus every GIF in `/gifs` on the SD card (size and
|
||||
dimensions shown; not 360×360 in orange). Tap one: it is copied into the device's flash (progress shown) and
|
||||
played from there, so the SD card is not needed afterwards. At boot the device checks (in the background)
|
||||
whether the chosen file on the SD card changed (size/date) and re-copies it. **Rescan** re-reads the card.
|
||||
- **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).
|
||||
@@ -31,6 +35,17 @@ Serial (115200) prints status changes as `[obd] ...`.
|
||||
| `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` |
|
||||
| `src/bg_store.cpp/.h` | Background GIF from the SD card: list `/gifs`, copy into the `bggif` flash partition (header written last), mmap for playback, change check at boot (background task) |
|
||||
| `partitions.csv` | 4 MB app (no OTA) + 11.9 MB `bggif` raw area + NVS |
|
||||
| `data/bg.gif` | Built-in background (`ezgif-5-cb56986bc7.gif`, 360×360), linked into the firmware via `board_build.embed_files` |
|
||||
| `sd/gifs/` | (not in git) GIFs converted with `../tools/gif_to_360.py`, ready to copy to the SD card |
|
||||
|
||||
Replace `data/bg.gif` with any 360×360 GIF to change the background (keep it under ~3 MB).
|
||||
## GIFs for the SD card
|
||||
Convert any GIF to 360×360 (scale to cover, center crop, frame timing kept) with
|
||||
```
|
||||
pip install pillow
|
||||
python ../tools/gif_to_360.py <gif files or folders> -o sd/gifs
|
||||
```
|
||||
and copy the files into a folder `gifs` on the SD card. Max ~11.5 MB per GIF. GIFs are played from flash at their own
|
||||
frame rate (360×360: ~10–12 ms decode per frame).
|
||||
Replace `data/bg.gif` to change the built-in background.
|
||||
@@ -0,0 +1,6 @@
|
||||
# JC3636W518 OBD speed: one 4 MB app (no OTA) + 11.9 MB raw area for the background GIF (written/mmapped by bg_store.cpp)
|
||||
# Name, Type, SubType, Offset, Size
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
app0, app, factory, 0x10000, 0x400000,
|
||||
bggif, data, 0x40, 0x410000, 0xBE0000,
|
||||
coredump, data, coredump, 0xFF0000, 0x10000,
|
||||
|
@@ -9,8 +9,8 @@ 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
|
||||
; 4 MB app (no OTA) + 11.9 MB raw 'bggif' area for the background GIF chosen from the SD card
|
||||
board_build.partitions = partitions.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 =
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
#include "bg_store.h"
|
||||
|
||||
#include <FS.h>
|
||||
#include <Preferences.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <esp_partition.h>
|
||||
|
||||
#include "pincfg.h"
|
||||
|
||||
#define BG_MAGIC 0x46494742 // "BGIF"
|
||||
#define BG_DATA_OFFSET 4096 // header in the first sector, GIF data after it
|
||||
#define COPY_CHUNK (32 * 1024)
|
||||
|
||||
struct BgHeader
|
||||
{
|
||||
uint32_t magic;
|
||||
uint32_t size;
|
||||
uint32_t mtime;
|
||||
char name[64];
|
||||
};
|
||||
|
||||
// ---------- shared state (protected by `mtx`) ----------
|
||||
static SemaphoreHandle_t mtx;
|
||||
static String selected; // "" = built-in
|
||||
static String status = "";
|
||||
static std::vector<BgFile> files;
|
||||
static uint32_t files_version = 1;
|
||||
static volatile uint32_t source_version = 1;
|
||||
static volatile bool busy = false;
|
||||
|
||||
// stored GIF (worker writes, UI reads after source_version changes)
|
||||
static const esp_partition_t *part = nullptr;
|
||||
static esp_partition_mmap_handle_t map_handle;
|
||||
static const uint8_t *map_ptr = nullptr;
|
||||
static volatile bool map_valid = false;
|
||||
static BgHeader header;
|
||||
|
||||
// requests / handshake
|
||||
static volatile bool req_list = false, req_select = false, req_boot_check = false;
|
||||
static String req_name;
|
||||
static volatile bool release_req = false, release_ack = false;
|
||||
|
||||
static void lock() { xSemaphoreTake(mtx, portMAX_DELAY); }
|
||||
static void unlock() { xSemaphoreGive(mtx); }
|
||||
|
||||
static void set_status(const String &s)
|
||||
{
|
||||
lock();
|
||||
status = s;
|
||||
unlock();
|
||||
Serial.println("[bg] " + s);
|
||||
}
|
||||
|
||||
// ---------- flash partition ----------
|
||||
static void map_partition()
|
||||
{
|
||||
map_valid = false;
|
||||
map_ptr = nullptr;
|
||||
if (!part)
|
||||
return;
|
||||
const void *ptr;
|
||||
if (esp_partition_mmap(part, 0, part->size, ESP_PARTITION_MMAP_DATA, &ptr, &map_handle) != ESP_OK)
|
||||
{
|
||||
Serial.println("[bg] mmap failed");
|
||||
return;
|
||||
}
|
||||
map_ptr = (const uint8_t *)ptr;
|
||||
memcpy(&header, map_ptr, sizeof(header));
|
||||
header.name[sizeof(header.name) - 1] = 0;
|
||||
map_valid = header.magic == BG_MAGIC && header.size > 0 && header.size <= part->size - BG_DATA_OFFSET;
|
||||
}
|
||||
|
||||
static void unmap_partition()
|
||||
{
|
||||
if (map_ptr)
|
||||
esp_partition_munmap(map_handle);
|
||||
map_ptr = nullptr;
|
||||
map_valid = false;
|
||||
}
|
||||
|
||||
// ---------- SD card ----------
|
||||
static bool sd_mount()
|
||||
{
|
||||
SD_MMC.setPins(SD_MMC_CLK_PIN, SD_MMC_CMD_PIN, SD_MMC_D0_PIN, SD_MMC_D1_PIN, SD_MMC_D2_PIN, SD_MMC_D3_PIN);
|
||||
return SD_MMC.begin() && SD_MMC.cardType() != CARD_NONE;
|
||||
}
|
||||
|
||||
static void sd_unmount() { SD_MMC.end(); }
|
||||
|
||||
static void do_list()
|
||||
{
|
||||
set_status("Reading SD card...");
|
||||
if (!sd_mount())
|
||||
{
|
||||
set_status("SD card not found");
|
||||
return;
|
||||
}
|
||||
std::vector<BgFile> found;
|
||||
File dir = SD_MMC.open(BG_SD_FOLDER);
|
||||
if (dir && dir.isDirectory())
|
||||
{
|
||||
for (File f = dir.openNextFile(); f; f = dir.openNextFile())
|
||||
{
|
||||
String n = f.name();
|
||||
String lower = n;
|
||||
lower.toLowerCase();
|
||||
if (f.isDirectory() || !lower.endsWith(".gif") || n.startsWith("."))
|
||||
continue;
|
||||
BgFile b;
|
||||
b.name = n;
|
||||
b.size = f.size();
|
||||
uint8_t hdr[10] = {0};
|
||||
f.read(hdr, sizeof(hdr));
|
||||
b.w = hdr[6] | (hdr[7] << 8);
|
||||
b.h = hdr[8] | (hdr[9] << 8);
|
||||
found.push_back(b);
|
||||
}
|
||||
}
|
||||
sd_unmount();
|
||||
lock();
|
||||
files = found;
|
||||
files_version++;
|
||||
unlock();
|
||||
set_status(dir ? String(found.size()) + " GIFs on the SD card" : String("No " BG_SD_FOLDER " folder on the SD card"));
|
||||
}
|
||||
|
||||
// Copies /gifs/<name> into the flash partition (SD must be mounted). Returns true on success.
|
||||
static bool copy_to_flash(const String &name, File &f)
|
||||
{
|
||||
uint32_t size = f.size();
|
||||
if (size + BG_DATA_OFFSET > part->size)
|
||||
{
|
||||
set_status("Too large: " + String(size / 1048576.0f, 1) + " MB");
|
||||
return false;
|
||||
}
|
||||
|
||||
// the UI is playing the stored GIF straight from this flash: make it switch to the built-in one first
|
||||
release_ack = false;
|
||||
release_req = true;
|
||||
while (!release_ack)
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
release_req = false;
|
||||
unmap_partition();
|
||||
|
||||
set_status("Erasing flash...");
|
||||
uint32_t erase_size = (BG_DATA_OFFSET + size + 0xFFFF) & ~0xFFFF; // whole 64 KB blocks (fast erase)
|
||||
if (erase_size > part->size)
|
||||
erase_size = part->size;
|
||||
if (esp_partition_erase_range(part, 0, erase_size) != ESP_OK)
|
||||
{
|
||||
set_status("Flash erase failed");
|
||||
map_partition();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)heap_caps_malloc(COPY_CHUNK, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
uint32_t done = 0;
|
||||
bool ok = buf != nullptr;
|
||||
int last_pct = -1;
|
||||
while (ok && done < size)
|
||||
{
|
||||
size_t n = f.read(buf, min((uint32_t)COPY_CHUNK, size - done));
|
||||
if (n == 0 || esp_partition_write(part, BG_DATA_OFFSET + done, buf, n) != ESP_OK)
|
||||
ok = false;
|
||||
done += n;
|
||||
int pct = done * 100 / size;
|
||||
if (pct / 5 != last_pct / 5)
|
||||
{
|
||||
last_pct = pct;
|
||||
set_status("Copying " + String(pct) + " %");
|
||||
}
|
||||
}
|
||||
free(buf);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
// header last: an interrupted copy never looks valid
|
||||
BgHeader h = {};
|
||||
h.magic = BG_MAGIC;
|
||||
h.size = size;
|
||||
h.mtime = (uint32_t)f.getLastWrite();
|
||||
strncpy(h.name, name.c_str(), sizeof(h.name) - 1);
|
||||
ok = esp_partition_write(part, 0, &h, sizeof(h)) == ESP_OK;
|
||||
}
|
||||
map_partition();
|
||||
set_status(ok ? "Stored: " + name : String("Copy failed"));
|
||||
return ok && map_valid;
|
||||
}
|
||||
|
||||
// Makes sure the flash holds the current version of /gifs/<name>. Returns false if it could not.
|
||||
static bool sync_file(const String &name, bool quiet)
|
||||
{
|
||||
if (!quiet)
|
||||
set_status("Reading SD card...");
|
||||
if (!sd_mount())
|
||||
{
|
||||
set_status(map_valid && name == header.name ? "SD card not found, using stored copy" : "SD card not found");
|
||||
return map_valid && name == header.name;
|
||||
}
|
||||
bool ok;
|
||||
File f = SD_MMC.open(String(BG_SD_FOLDER) + "/" + name);
|
||||
if (!f)
|
||||
{
|
||||
set_status(map_valid && name == header.name ? "File gone from SD, using stored copy" : "File not found");
|
||||
ok = map_valid && name == header.name;
|
||||
}
|
||||
else if (map_valid && name == header.name && f.size() == header.size && (uint32_t)f.getLastWrite() == header.mtime)
|
||||
{
|
||||
if (!quiet)
|
||||
set_status("Up to date: " + name);
|
||||
ok = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = copy_to_flash(name, f);
|
||||
}
|
||||
if (f)
|
||||
f.close();
|
||||
sd_unmount();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void save_selected(const String &name)
|
||||
{
|
||||
Preferences p;
|
||||
p.begin("ui", false);
|
||||
p.putString("bg", name);
|
||||
p.end();
|
||||
lock();
|
||||
selected = name;
|
||||
unlock();
|
||||
source_version++;
|
||||
}
|
||||
|
||||
static void worker_task(void *)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
if (req_boot_check)
|
||||
{
|
||||
req_boot_check = false;
|
||||
busy = true;
|
||||
String name = bg_selected();
|
||||
if (name.length())
|
||||
{
|
||||
uint32_t before = header.size ^ header.mtime;
|
||||
sync_file(name, true);
|
||||
if ((header.size ^ header.mtime) != before)
|
||||
source_version++;
|
||||
}
|
||||
busy = false;
|
||||
}
|
||||
if (req_select)
|
||||
{
|
||||
req_select = false;
|
||||
busy = true;
|
||||
String name = req_name;
|
||||
if (name.length() == 0 || sync_file(name, false))
|
||||
{
|
||||
save_selected(name);
|
||||
if (name.length() == 0)
|
||||
set_status("Built-in background");
|
||||
}
|
||||
busy = false;
|
||||
}
|
||||
if (req_list)
|
||||
{
|
||||
req_list = false;
|
||||
busy = true;
|
||||
do_list();
|
||||
busy = false;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- public API ----------
|
||||
void bg_begin()
|
||||
{
|
||||
mtx = xSemaphoreCreateMutex();
|
||||
Preferences p;
|
||||
p.begin("ui", true);
|
||||
selected = p.getString("bg", "");
|
||||
p.end();
|
||||
part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, (esp_partition_subtype_t)0x40, "bggif");
|
||||
if (!part)
|
||||
Serial.println("[bg] partition 'bggif' not found");
|
||||
map_partition();
|
||||
Serial.printf("[bg] selected '%s', stored '%s' (%s)\n", selected.c_str(), map_valid ? header.name : "",
|
||||
map_valid ? "valid" : "none");
|
||||
req_boot_check = true; // look for an updated version on the SD card in the background
|
||||
xTaskCreatePinnedToCore(worker_task, "bg_store", 6144, nullptr, 1, nullptr, 0);
|
||||
}
|
||||
|
||||
String bg_selected()
|
||||
{
|
||||
lock();
|
||||
String s = selected;
|
||||
unlock();
|
||||
return s;
|
||||
}
|
||||
|
||||
const uint8_t *bg_stored_data(uint32_t *size)
|
||||
{
|
||||
if (!map_valid)
|
||||
return nullptr;
|
||||
*size = header.size;
|
||||
return map_ptr + BG_DATA_OFFSET;
|
||||
}
|
||||
|
||||
String bg_stored_name() { return map_valid ? String(header.name) : String(); }
|
||||
|
||||
void bg_request_list() { req_list = true; }
|
||||
|
||||
bool bg_get_list(std::vector<BgFile> &out, uint32_t &version)
|
||||
{
|
||||
lock();
|
||||
bool changed = version != files_version;
|
||||
if (changed)
|
||||
{
|
||||
out = files;
|
||||
version = files_version;
|
||||
}
|
||||
unlock();
|
||||
return changed;
|
||||
}
|
||||
|
||||
void bg_request_select(const String &name)
|
||||
{
|
||||
req_name = name;
|
||||
req_select = true;
|
||||
}
|
||||
|
||||
String bg_status()
|
||||
{
|
||||
lock();
|
||||
String s = status;
|
||||
unlock();
|
||||
return s;
|
||||
}
|
||||
|
||||
bool bg_busy() { return busy || req_select || req_list; }
|
||||
|
||||
bool bg_release_requested() { return release_req && !release_ack; }
|
||||
void bg_release_done() { release_ack = true; }
|
||||
|
||||
bool bg_source_changed(uint32_t &version)
|
||||
{
|
||||
if (version == source_version)
|
||||
return false;
|
||||
version = source_version;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Background GIF storage: GIFs from the SD card (/gifs) are copied into a raw flash partition ("bggif")
|
||||
// and played straight from flash (memory-mapped), so the SD card is only needed when choosing / updating.
|
||||
// SD and flash work runs in a background task; all functions are safe to call from the UI (loop) task.
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
#define BG_SD_FOLDER "/gifs"
|
||||
|
||||
struct BgFile
|
||||
{
|
||||
String name; // file name in /gifs
|
||||
uint32_t size; // bytes
|
||||
uint16_t w, h; // GIF logical screen size
|
||||
};
|
||||
|
||||
void bg_begin(); // maps the stored GIF and checks the SD card for an updated version
|
||||
|
||||
// Currently selected background: "" = built-in, otherwise a file name from /gifs
|
||||
String bg_selected();
|
||||
|
||||
// Stored GIF in flash, nullptr if none. Only valid for bg_selected() when bg_stored_name() matches.
|
||||
const uint8_t *bg_stored_data(uint32_t *size);
|
||||
String bg_stored_name();
|
||||
|
||||
// Menu
|
||||
void bg_request_list(); // mount SD and list /gifs
|
||||
bool bg_get_list(std::vector<BgFile> &out, uint32_t &version);
|
||||
void bg_request_select(const String &name); // "" = built-in; copies to flash if needed
|
||||
String bg_status(); // "Copying 45 %", "SD card not found", ...
|
||||
bool bg_busy();
|
||||
|
||||
// UI handshake: the UI must stop using the stored GIF before it is overwritten,
|
||||
// and reload the background when the source changed.
|
||||
bool bg_release_requested();
|
||||
void bg_release_done();
|
||||
bool bg_source_changed(uint32_t &version);
|
||||
+159
-12
@@ -1,17 +1,20 @@
|
||||
// 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 again for settings (screen rotation, idle timeout; remembered).
|
||||
// Swipe left again for settings (screen rotation, idle timeout; remembered), and once more for the
|
||||
// background page (built-in GIF or one from /gifs on the SD card, copied into flash).
|
||||
// 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 <Preferences.h>
|
||||
#include "jc3636w518.h"
|
||||
#include "obd_link.h"
|
||||
#include "bg_store.h"
|
||||
|
||||
#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_BG_ITEMS 40
|
||||
|
||||
LV_FONT_DECLARE(font_speed_120);
|
||||
|
||||
@@ -25,7 +28,12 @@ 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, *tile_settings;
|
||||
static lv_obj_t *tileview, *tile_speed, *tile_menu, *tile_settings, *tile_bg;
|
||||
static lv_obj_t *bg_status_label, *bg_list;
|
||||
static std::vector<BgFile> bg_files;
|
||||
static uint32_t bg_list_version = 0, bg_source_version = 0;
|
||||
static String bg_list_key; // files + selection currently shown in the list
|
||||
static lv_image_dsc_t builtin_dsc, stored_dsc;
|
||||
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;
|
||||
@@ -67,6 +75,8 @@ static bool menu_visible()
|
||||
static void on_tile_changed(lv_event_t *e)
|
||||
{
|
||||
obd_scan_enable(menu_visible()); // scan only while the menu is open
|
||||
if (lv_tileview_get_tile_active(tileview) == tile_bg)
|
||||
bg_request_list();
|
||||
}
|
||||
|
||||
static void on_device_clicked(lv_event_t *e)
|
||||
@@ -147,20 +157,67 @@ 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)
|
||||
}
|
||||
|
||||
// ---------- background ----------
|
||||
static void fill_gif_dsc(lv_image_dsc_t *dsc, const uint8_t *data, uint32_t size)
|
||||
{
|
||||
dsc->header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
dsc->header.cf = LV_COLOR_FORMAT_RAW;
|
||||
dsc->header.w = data[6] | (data[7] << 8); // GIF logical screen size
|
||||
dsc->header.h = data[8] | (data[9] << 8);
|
||||
dsc->data_size = size;
|
||||
dsc->data = data;
|
||||
}
|
||||
|
||||
static void set_gif_source(const lv_image_dsc_t *dsc)
|
||||
{
|
||||
lv_gif_set_src(bg_gif, dsc);
|
||||
if (obd_is_idle())
|
||||
lv_gif_pause(bg_gif); // keep the animation stopped while the screen is dark
|
||||
}
|
||||
|
||||
// Selected GIF from flash if it is stored there, otherwise the built-in one
|
||||
static void apply_background()
|
||||
{
|
||||
String sel = bg_selected();
|
||||
uint32_t size;
|
||||
const uint8_t *data = bg_stored_data(&size);
|
||||
if (sel.length() && data && bg_stored_name() == sel)
|
||||
{
|
||||
fill_gif_dsc(&stored_dsc, data, size);
|
||||
set_gif_source(&stored_dsc);
|
||||
}
|
||||
else
|
||||
{
|
||||
set_gif_source(&builtin_dsc);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_bg_item_clicked(lv_event_t *e)
|
||||
{
|
||||
if (bg_busy())
|
||||
return;
|
||||
int idx = (int)(intptr_t)lv_event_get_user_data(e); // -1 = built-in
|
||||
if (idx < 0)
|
||||
bg_request_select("");
|
||||
else if (idx < (int)bg_files.size())
|
||||
bg_request_select(bg_files[idx].name);
|
||||
}
|
||||
|
||||
static void on_bg_rescan_clicked(lv_event_t *e)
|
||||
{
|
||||
if (!bg_busy())
|
||||
bg_request_list();
|
||||
}
|
||||
|
||||
// ---------- 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;
|
||||
// background GIF (built-in until apply_background() picks the stored one),
|
||||
// decoded straight into the display format (no transparency needed on black)
|
||||
fill_gif_dsc(&builtin_dsc, bg_gif_start, bg_gif_end - bg_gif_start);
|
||||
bg_gif = lv_gif_create(tile_speed);
|
||||
lv_gif_set_color_format(bg_gif, lv_display_get_color_format(lv_display_get_default()));
|
||||
lv_gif_set_src(bg_gif, &gif_dsc);
|
||||
lv_gif_set_src(bg_gif, &builtin_dsc);
|
||||
lv_obj_center(bg_gif);
|
||||
|
||||
// arc gauge along the round edge: 270° sweep, gap at the bottom
|
||||
@@ -321,6 +378,33 @@ static void build_settings_tile()
|
||||
make_button(tile_settings, "Done", 0, 296, 110, on_done_clicked);
|
||||
}
|
||||
|
||||
static void build_bg_tile()
|
||||
{
|
||||
lv_obj_t *title = lv_label_create(tile_bg);
|
||||
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, "Background");
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
|
||||
|
||||
bg_status_label = lv_label_create(tile_bg);
|
||||
lv_obj_set_style_text_color(bg_status_label, COLOR_DIM, 0);
|
||||
lv_obj_set_width(bg_status_label, 260);
|
||||
lv_label_set_long_mode(bg_status_label, LV_LABEL_LONG_MODE_DOTS);
|
||||
lv_obj_set_style_text_align(bg_status_label, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_label_set_text(bg_status_label, "");
|
||||
lv_obj_align(bg_status_label, LV_ALIGN_TOP_MID, 0, 48);
|
||||
|
||||
bg_list = lv_list_create(tile_bg);
|
||||
lv_obj_set_size(bg_list, 276, 214);
|
||||
lv_obj_align(bg_list, LV_ALIGN_TOP_MID, 0, 74);
|
||||
lv_obj_set_style_bg_color(bg_list, COLOR_PANEL, 0);
|
||||
lv_obj_set_style_border_width(bg_list, 0, 0);
|
||||
lv_obj_set_style_pad_all(bg_list, 4, 0);
|
||||
|
||||
make_button(tile_bg, "Rescan", -50, 296, 92, on_bg_rescan_clicked);
|
||||
make_button(tile_bg, "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);
|
||||
@@ -329,11 +413,13 @@ static void build_ui()
|
||||
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_t)(LV_DIR_LEFT | LV_DIR_RIGHT));
|
||||
tile_settings = lv_tileview_add_tile(tileview, 2, 0, LV_DIR_LEFT);
|
||||
tile_settings = lv_tileview_add_tile(tileview, 2, 0, (lv_dir_t)(LV_DIR_LEFT | LV_DIR_RIGHT));
|
||||
tile_bg = lv_tileview_add_tile(tileview, 3, 0, LV_DIR_LEFT);
|
||||
lv_obj_add_event_cb(tileview, on_tile_changed, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
build_speed_tile();
|
||||
build_menu_tile();
|
||||
build_settings_tile();
|
||||
build_bg_tile();
|
||||
|
||||
idle_overlay = lv_obj_create(lv_layer_top());
|
||||
lv_obj_remove_style_all(idle_overlay);
|
||||
@@ -427,6 +513,62 @@ static void update_speed()
|
||||
}
|
||||
}
|
||||
|
||||
static lv_obj_t *add_bg_item(const String &text, lv_color_t color, int idx)
|
||||
{
|
||||
lv_obj_t *btn = lv_list_add_button(bg_list, NULL, text.c_str());
|
||||
lv_obj_add_event_cb(btn, on_bg_item_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)idx);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(0x222A33), 0);
|
||||
lv_obj_set_style_pad_ver(btn, 6, 0);
|
||||
lv_obj_set_style_text_color(btn, color, 0);
|
||||
return btn;
|
||||
}
|
||||
|
||||
static void update_bg_page()
|
||||
{
|
||||
String st = bg_status();
|
||||
if (st != lv_label_get_text(bg_status_label))
|
||||
lv_label_set_text(bg_status_label, st.c_str());
|
||||
|
||||
bg_get_list(bg_files, bg_list_version);
|
||||
String sel = bg_selected();
|
||||
String key = sel + "|" + bg_list_version;
|
||||
if (key == bg_list_key)
|
||||
return;
|
||||
bg_list_key = key;
|
||||
lv_obj_clean(bg_list);
|
||||
add_bg_item(String("Built-in") + (sel.length() == 0 ? " (active)" : ""), sel.length() == 0 ? COLOR_ARC : COLOR_TEXT, -1);
|
||||
for (int i = 0; i < (int)bg_files.size() && i < MAX_BG_ITEMS; i++)
|
||||
{
|
||||
const BgFile &f = bg_files[i];
|
||||
bool active = f.name == sel;
|
||||
String text = f.name + (active ? " (active)" : "") + "\n" + String(f.size / 1048576.0f, 2) + " MB " + f.w + "x" + f.h;
|
||||
lv_color_t c = active ? COLOR_ARC : COLOR_TEXT;
|
||||
if (f.w > 480)
|
||||
{
|
||||
text += " too wide";
|
||||
c = COLOR_DIM;
|
||||
}
|
||||
else if (f.w != 360 || f.h != 360)
|
||||
{
|
||||
text += " not 360x360";
|
||||
c = lv_color_hex(0xFF9500);
|
||||
}
|
||||
add_bg_item(text, c, i);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_bg_source()
|
||||
{
|
||||
if (bg_release_requested())
|
||||
{
|
||||
// the stored GIF in flash is about to be overwritten: play the built-in one meanwhile
|
||||
set_gif_source(&builtin_dsc);
|
||||
bg_release_done();
|
||||
}
|
||||
if (bg_source_changed(bg_source_version))
|
||||
apply_background();
|
||||
}
|
||||
|
||||
static void update_menu()
|
||||
{
|
||||
String saved = obd_saved_name();
|
||||
@@ -493,6 +635,8 @@ void setup()
|
||||
build_ui();
|
||||
obd_set_idle_timeout(load_idle_minutes());
|
||||
obd_begin();
|
||||
bg_begin();
|
||||
apply_background();
|
||||
if (obd_saved_address().length() == 0)
|
||||
{
|
||||
// first start: go straight to the adapter menu
|
||||
@@ -512,6 +656,9 @@ void loop()
|
||||
update_speed();
|
||||
update_voltage();
|
||||
update_idle();
|
||||
update_bg_source();
|
||||
if (lv_tileview_get_tile_active(tileview) == tile_bg)
|
||||
update_bg_page();
|
||||
}
|
||||
if (now - last_menu >= 500 && menu_visible())
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user