diff --git a/Documentation/ideas.md b/Documentation/ideas.md index cc1e4cada..b700f6d67 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -9,11 +9,9 @@ - Make tt.py work without an app project. Ping the device for info. - LVGL spinner not working on cl32 v04 (possibly anything monochrome) -- Export platform-esp32 with TactilitySDK - Find out why the first build of an app with a clean build cache folder always fails - Improve posix support: use user home path and system /tmp folder. - Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module. -- Improve SPI kernel driver (implement read, write, transactions) - External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update) - minitar/untarFile(): "entry->metadata.path" can escape its confined path (e.g. "../something") diff --git a/Documentation/releasing.md b/Documentation/releasing.md index 066354e72..303f0235b 100644 --- a/Documentation/releasing.md +++ b/Documentation/releasing.md @@ -6,25 +6,25 @@ 2. Merge the branch and wait for a build. 3. Push a tag e.g. `v1.2.3` - the `v` prefix is crucial for the pipelines. 4. Wait for the SDK and firmwares to be published on the CDN. - Only tags publish SDKs of a release version: `main` only publishes SDKs of a `-dev` version. -5. Test the firmwares on all devices with the web installer + (Only tags publish SDKs of a release version: `main` only publishes SDKs of a `-dev` version.) +5. Test the firmwares on several different devices with the web installer. + Try different architectures, input methods, screen sizes. 6. Run `Buildscripts/gh-download-artifacts.py [actionId] --token [token]` and let it run in the background. Get a token from [here](https://github.com/settings/personal-access-tokens) 7. Update the [TactilityApps](https://github.com/TactilityProject/TactilityApps) with the new SDK version and bump the app versions. -8. After merging the apps, manually upload to CDN, then test them. -9. Make a new version of the docs available at [TactilityDocs](https://github.com/TactilityProject/TactilityDocs) +8. Make a new version of the docs available at [TactilityDocs](https://github.com/TactilityProject/TactilityDocs) 1. Copy `docs/versions/main/` into new version folder 2. Edit `index.html` to include the new version 3. Push code directly to `main` branch -10. Make a new [GitHub release](https://github.com/TactilityProject/Tactility/releases/new) +9. Make a new [GitHub release](https://github.com/TactilityProject/Tactility/releases/new) Run `Buildscripts/release-summary.py` to generate a release, then review it -11. Double-check that all CDN/TactilityApps/Tactility repository changes are merged. -12. Add the `-dev` postfix from `version.txt` and bump the version. Merge it to `main`. -13. Ensure that the CDN is not in development mode anymore. +10. Double-check that all CDN/TactilityApps/Tactility repository changes are merged. +11. Add the `-dev` postfix from `version.txt` and bump the version. Merge it to `main`. +12. Ensure that the CDN is not in development mode anymore. ### Post-release 1. Mention on Discord -2. Consider making a video with one of the devices, showcasing new features +2. Publish on social media 3. Consider notifying vendors/stakeholders \ No newline at end of file diff --git a/Drivers/ssd16xx-module/source/ssd16xx.cpp b/Drivers/ssd16xx-module/source/ssd16xx.cpp index 2002d2413..a327948d5 100644 --- a/Drivers/ssd16xx-module/source/ssd16xx.cpp +++ b/Drivers/ssd16xx-module/source/ssd16xx.cpp @@ -241,14 +241,23 @@ static bool full_refresh(Ssd16xxInternal* internal) { } // Drives the pixels that differ from the displayed image +// Without a known displayed image, every pixel is driven by describing the old image as the inverse of the new one. // Returns false when a transfer failed, which leaves the base image invalid. static bool partial_update(Ssd16xxInternal* internal) { const size_t size = snapshot_frame(internal); + const bool displayed_unknown = !internal->base_valid; internal->update_failed = false; spi_controller_lock(internal->spi_controller); set_full_window(internal); write_command_with_buffer(internal, 0x24, internal->snapshot, size); + if (displayed_unknown) { + for (size_t i = 0; i < size; i++) { + internal->snapshot[i] = ~internal->snapshot[i]; + } + set_full_window(internal); + write_command_with_buffer(internal, 0x26, internal->snapshot, size); + } if (internal->update_failed) { spi_controller_unlock(internal->spi_controller); internal->base_valid = false; @@ -355,7 +364,7 @@ static void update_task(void* arg) { portEXIT_CRITICAL(&internal->lock); if (dirty) { - const bool ok = internal->base_valid ? partial_update(internal) : full_refresh(internal); + const bool ok = partial_update(internal); if (!ok) { retry_update(internal); } diff --git a/Modules/lvgl-module/source/devices/devices.cpp b/Modules/lvgl-module/source/devices/devices.cpp index 904103a04..f2594c43a 100644 --- a/Modules/lvgl-module/source/devices/devices.cpp +++ b/Modules/lvgl-module/source/devices/devices.cpp @@ -49,10 +49,15 @@ struct LvglDeviceList { */ constexpr uint32_t IDLE_REPAINT_MS = 5000; +constexpr uint32_t ACTIVITY_MARGIN_MS = 20; +constexpr uint32_t MAX_ACTIVITY_READ_SKEW_MS = 5; + struct IdleRepaintContext { lv_display_t* lvgl_display; struct Device* kernel_display_device; lv_timer_t* timer; + // Tick of the last input that the flags below already account for + uint32_t handled_activity_at; // Set once the invalidate+lv_refr_now() repaint has run for the current idle stretch, so it // isn't redone every tick while still idle. bool repaint_done_since_activity; @@ -161,11 +166,33 @@ static void lvgl_device_listener(struct Device* device, enum DeviceEvent event, lvgl_unlock(); } +// Reads the tick of the last input, which the display only exposes as time since then. +// Fails when the task was preempted during the read, which would skew the result beyond ACTIVITY_MARGIN_MS. +static bool read_activity(lv_display_t* display, uint32_t* inactive_ms, uint32_t* activity_at) { + const uint32_t start = lv_tick_get(); + *inactive_ms = lv_display_get_inactive_time(display); + if (lv_tick_get() - start > MAX_ACTIVITY_READ_SKEW_MS) { + return false; + } + *activity_at = start - *inactive_ms; + return true; +} + static void idle_repaint_timer_cb(lv_timer_t* timer) { auto* ctx = static_cast(lv_timer_get_user_data(timer)); - if (lv_display_get_inactive_time(ctx->lvgl_display) < IDLE_REPAINT_MS) { + uint32_t inactive_ms; + uint32_t activity_at; + if (!read_activity(ctx->lvgl_display, &inactive_ms, &activity_at)) { + return; + } + // Any difference beyond the margin is new input, also when the tick counter wrapped in between + const uint32_t activity_delta = activity_at - ctx->handled_activity_at; + if (activity_delta > ACTIVITY_MARGIN_MS && activity_delta < 0U - ACTIVITY_MARGIN_MS) { + ctx->handled_activity_at = activity_at; ctx->repaint_done_since_activity = false; ctx->refresh_done_since_activity = false; + } + if (inactive_ms < IDLE_REPAINT_MS) { return; } if (!ctx->repaint_done_since_activity) { @@ -237,13 +264,21 @@ void lvgl_devices_attach() { // boot and whenever an app that stopped LVGL closes, so clearing here covers both // cases without extra plumbing at app-close time specifically. if (display_has_capability(kernel_display_device, DISPLAY_CAPABILITY_SLOW_REFRESH)) { - display_clear(kernel_display_device); + // The first attach follows the boot logo, which the first LVGL render overdraws completely + static bool first_attach = true; + if (!first_attach) { + display_clear(kernel_display_device); + } + first_attach = false; IdleRepaintContext* ctx = &idle_repaint_contexts()[i]; if (ctx->timer == nullptr) { ctx->lvgl_display = added_display; ctx->kernel_display_device = kernel_display_device; - ctx->repaint_done_since_activity = false; - ctx->refresh_done_since_activity = false; + // The panel was just cleared or fully drawn, so the first idle stretch needs no repaint + uint32_t inactive_ms; + while (!read_activity(added_display, &inactive_ms, &ctx->handled_activity_at)) {} + ctx->repaint_done_since_activity = true; + ctx->refresh_done_since_activity = true; ctx->timer = lv_timer_create(idle_repaint_timer_cb, 1000, ctx); } } diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 16be21f4c..09f40b58d 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -193,22 +193,22 @@ int32_t appMain(int argc, char* argv[]) { check(file_ops_subscribe(&file_ops_sub, &event_group) == ERROR_NONE); Context ctx; - WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); + WindowId window = 0; + bool windowCreated = false; - // The file operations run in order: the favourites are available when auto-start runs + // The window is created once the favourites are loaded, so its first render has the final order check(file_ops_queue(loadFavourites, nullptr, &file_ops_sub) == ERROR_NONE); - check(file_ops_queue(runAutoStart, nullptr, nullptr) == ERROR_NONE); // The launcher is meant to stay resident (it's the home screen) - it only gives up its // thread when app-module's scheduler asks it to (e.g. another new-model app is started). while (true) { task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); - if (file_ops_poll(&file_ops_sub) == ERROR_NONE) { - // The favourites were loaded - lvgl_lock(); - ctx.grid.requestRepopulate(false); - lvgl_unlock(); + if (!windowCreated && file_ops_poll(&file_ops_sub) == ERROR_NONE) { + window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); + windowCreated = true; + // The file operations run in order: the favourites are available when auto-start runs + check(file_ops_queue(runAutoStart, nullptr, nullptr) == ERROR_NONE); } bool shouldClose = false; @@ -222,7 +222,9 @@ int32_t appMain(int argc, char* argv[]) { if (shouldClose) break; } - window_manager_remove(window); + if (windowCreated) { + window_manager_remove(window); + } file_ops_unsubscribe(&file_ops_sub); check(app_event_unsubscribe(&sub) == ERROR_NONE); task_event_group_destruct(&event_group);