Skip to content

Commit 6840f80

Browse files
authored
Merge pull request #139 from squazaryu/release/stable-089-039-resume
fix(desktop): finalize stable 089-039 animation resume
2 parents 6804779 + 549c503 commit 6840f80

10 files changed

Lines changed: 207 additions & 26 deletions

File tree

ReadMe.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ t-dev-<unleashed>-<build>-<iteration>
5656
- `t-dev`: Tumoflip development build prefix for unstable builds.
5757
- `089`: upstream Unleashed base version.
5858
- `039`: tumoflip internal build version.
59-
- `<iteration>`: monotonically increasing revision used only by `t-dev` builds.
59+
- `002`: development iteration inside the tumoflip internal build version.
6060

6161
`main` should only receive builds that are stable enough to publish as tagged
6262
releases. Active firmware work lands on `dev` first. When the Unleashed base
@@ -98,6 +98,8 @@ The four-page post-update splash screen is generated automatically from
9898
- Rebranded firmware origin to `tumoflip` and distribution/version suffix to
9999
`t-flppr-fw-089-039`.
100100
- Added custom Desktop main menu styles inspired by Momentum-style layouts.
101+
- Keeps a bounded compressed Desktop animation preview while external FAPs exit,
102+
reducing the visible SD reload pause without retaining the full wallpaper in RAM.
101103
- Added `RX Mode: AUTO/DUAL` and a direct Read-screen toggle to the system
102104
Sub-GHz app. With an external CC1101 connected, both radios decode
103105
independently and duplicate frames are merged while retaining the stronger

applications/services/desktop/animations/animation_manager.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,7 @@ void animation_manager_load_and_continue_animation(AnimationManager* animation_m
714714
(animation_manager->state == AnimationManagerStateFreezedIdle) ||
715715
(animation_manager->state == AnimationManagerStateFreezedBlocked));
716716

717+
bubble_animation_start_resume_preview(animation_manager->animation_view);
717718
const bool profile_changed = animation_manager_reload_profile(animation_manager);
718719

719720
if(animation_manager->state == AnimationManagerStateFreezedBlocked) {

applications/services/desktop/animations/views/bubble_animation_view.c

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
#include <stdint.h>
1313
#include <core/dangerous_defines.h>
1414

15-
#define ACTIVE_SHIFT 2
15+
#define ACTIVE_SHIFT 2
16+
#define BUBBLE_ANIMATION_RESUME_PREVIEW_MAX_BYTES (4U * 1024U)
17+
#define COMPRESS_ICON_HEADER_SIZE 4U
1618

1719
typedef struct {
1820
const BubbleAnimation* current;
@@ -24,6 +26,7 @@ typedef struct {
2426
uint8_t active_shift;
2527
uint32_t active_ended_at;
2628
Icon* freeze_frame;
29+
uint8_t freeze_frame_index;
2730
} BubbleAnimationViewModel;
2831

2932
struct BubbleAnimationView {
@@ -62,7 +65,13 @@ static void bubble_animation_draw_callback(Canvas* canvas, void* model_) {
6265

6366
if(model->freeze_frame) {
6467
uint8_t y_offset = canvas_height(canvas) - icon_get_height(model->freeze_frame);
65-
canvas_draw_icon(canvas, 0, y_offset, model->freeze_frame);
68+
canvas_draw_bitmap(
69+
canvas,
70+
0,
71+
y_offset,
72+
icon_get_width(model->freeze_frame),
73+
icon_get_height(model->freeze_frame),
74+
icon_get_frame_data(model->freeze_frame, model->freeze_frame_index));
6675
return;
6776
}
6877

@@ -235,11 +244,20 @@ static void bubble_animation_timer_callback(void* context) {
235244

236245
BubbleAnimationViewModel* model = view_get_model(view->view);
237246

247+
if(model->freeze_frame) {
248+
const uint8_t frame_count = icon_get_frame_count(model->freeze_frame);
249+
if(frame_count > 1U) {
250+
model->freeze_frame_index = (model->freeze_frame_index + 1U) % frame_count;
251+
}
252+
view_commit_model(view->view, true);
253+
return;
254+
}
255+
238256
if(model->active_shift > 0) {
239257
activate = (--model->active_shift == 0);
240258
}
241259

242-
if(!model->freeze_frame && !activate) {
260+
if(!activate) {
243261
bubble_animation_next_frame(model);
244262
}
245263

@@ -250,28 +268,57 @@ static void bubble_animation_timer_callback(void* context) {
250268
}
251269
}
252270

253-
/* always freeze first passive frame, because
254-
* animation is always activated at unfreezing and played
255-
* passive frame first, and 2 frames after - active
256-
*/
257-
static Icon* bubble_animation_clone_first_frame(const Icon* icon_orig) {
258-
furi_assert(icon_orig);
271+
static size_t bubble_animation_frame_data_size(const uint8_t* frame, size_t bitmap_size) {
272+
furi_assert(frame);
273+
274+
if(frame[0] == 0U) {
275+
return bitmap_size + 1U;
276+
}
277+
278+
uint16_t compressed_size = 0U;
279+
memcpy(&compressed_size, &frame[2], sizeof(compressed_size));
280+
return COMPRESS_ICON_HEADER_SIZE + compressed_size;
281+
}
282+
283+
static Icon* bubble_animation_clone_preview(const BubbleAnimation* animation) {
284+
furi_assert(animation);
285+
const Icon* icon_orig = &animation->icon_animation;
259286
furi_assert(icon_orig->frames);
260-
furi_assert(icon_orig->frames[0]);
287+
furi_assert(animation->frame_order);
288+
const uint8_t preview_frames = animation->passive_frames ? animation->passive_frames :
289+
animation->active_frames;
290+
furi_assert(preview_frames > 0U);
291+
292+
const size_t bitmap_size = ROUND_UP_TO(icon_orig->width, 8) * icon_orig->height;
293+
size_t preview_size = 0U;
294+
uint8_t frame_count = 0U;
295+
for(uint8_t i = 0U; i < preview_frames; ++i) {
296+
const uint8_t source_index = animation->frame_order[i];
297+
furi_assert(source_index < icon_orig->frame_count);
298+
const size_t frame_size =
299+
bubble_animation_frame_data_size(icon_orig->frames[source_index], bitmap_size);
300+
if(frame_count &&
301+
((preview_size + frame_size) > BUBBLE_ANIMATION_RESUME_PREVIEW_MAX_BYTES)) {
302+
break;
303+
}
304+
preview_size += frame_size;
305+
++frame_count;
306+
}
307+
furi_assert(frame_count > 0U);
261308

262309
Icon* icon_clone = malloc(sizeof(Icon));
263310
memcpy(icon_clone, icon_orig, sizeof(Icon));
264311

265-
icon_clone->frames = malloc(sizeof(uint8_t*));
266-
/* icon bitmap can be either compressed or not. It is compressed if
267-
* compressed size is less than original, so max size for bitmap is
268-
* uncompressed (width * height) + 1 byte (in uncompressed case)
269-
* for compressed header
270-
*/
271-
size_t max_bitmap_size = ROUND_UP_TO(icon_orig->width, 8) * icon_orig->height + 1;
272-
FURI_CONST_ASSIGN_PTR(icon_clone->frames[0], malloc(max_bitmap_size));
273-
memcpy((void*)icon_clone->frames[0], icon_orig->frames[0], max_bitmap_size);
274-
FURI_CONST_ASSIGN(icon_clone->frame_count, 1);
312+
icon_clone->frames = malloc(sizeof(uint8_t*) * frame_count);
313+
for(uint8_t i = 0U; i < frame_count; ++i) {
314+
const uint8_t source_index = animation->frame_order[i];
315+
const uint8_t* source_frame = icon_orig->frames[source_index];
316+
const size_t frame_size = bubble_animation_frame_data_size(source_frame, bitmap_size);
317+
uint8_t* frame = malloc(frame_size);
318+
memcpy(frame, source_frame, frame_size);
319+
FURI_CONST_ASSIGN_PTR(icon_clone->frames[i], frame);
320+
}
321+
FURI_CONST_ASSIGN(icon_clone->frame_count, frame_count);
275322

276323
return icon_clone;
277324
}
@@ -280,7 +327,9 @@ static void bubble_animation_release_frame(Icon** icon) {
280327
furi_assert(icon);
281328
furi_assert(*icon);
282329

283-
free((void*)(*icon)->frames[0]);
330+
for(uint8_t i = 0U; i < (*icon)->frame_count; ++i) {
331+
free((void*)(*icon)->frames[i]);
332+
}
284333
free((void*)(*icon)->frames);
285334
free(*icon);
286335
*icon = NULL;
@@ -372,9 +421,12 @@ void bubble_animation_view_set_animation(
372421
model->current_bubble = bubble_animation_pick_bubble(model, false);
373422
model->current_frame = 0;
374423
model->active_cycle = 0;
424+
const bool frozen = model->freeze_frame != NULL;
375425
view_commit_model(view->view, true);
376426

377-
furi_timer_start(view->timer, 1000 / new_animation->icon_animation.frame_rate);
427+
if(!frozen) {
428+
furi_timer_start(view->timer, 1000 / new_animation->icon_animation.frame_rate);
429+
}
378430
}
379431

380432
void bubble_animation_freeze(BubbleAnimationView* view) {
@@ -383,20 +435,44 @@ void bubble_animation_freeze(BubbleAnimationView* view) {
383435
BubbleAnimationViewModel* model = view_get_model(view->view);
384436
furi_assert(model->current);
385437
furi_assert(!model->freeze_frame);
386-
model->freeze_frame = bubble_animation_clone_first_frame(&model->current->icon_animation);
438+
model->freeze_frame = bubble_animation_clone_preview(model->current);
439+
model->freeze_frame_index = 0U;
387440
model->current = NULL;
388441
view_commit_model(view->view, false);
389442
furi_timer_stop(view->timer);
390443
}
391444

445+
void bubble_animation_start_resume_preview(BubbleAnimationView* view) {
446+
furi_assert(view);
447+
448+
BubbleAnimationViewModel* model = view_get_model(view->view);
449+
furi_assert(model->freeze_frame);
450+
model->freeze_frame_index = 0U;
451+
const uint8_t frame_count = icon_get_frame_count(model->freeze_frame);
452+
const uint8_t frame_rate = model->freeze_frame->frame_rate;
453+
view_commit_model(view->view, true);
454+
455+
if((frame_count > 1U) && frame_rate) {
456+
furi_timer_start(view->timer, 1000U / frame_rate);
457+
}
458+
}
459+
392460
void bubble_animation_unfreeze(BubbleAnimationView* view) {
393461
furi_assert(view);
394462
uint8_t frame_rate;
395463

464+
furi_timer_stop(view->timer);
465+
396466
BubbleAnimationViewModel* model = view_get_model(view->view);
397467
furi_assert(model->freeze_frame);
398-
bubble_animation_release_frame(&model->freeze_frame);
399468
furi_assert(model->current);
469+
const uint8_t playback_frames = model->current->passive_frames ?
470+
model->current->passive_frames :
471+
model->current->active_frames;
472+
if(playback_frames) {
473+
model->current_frame = model->freeze_frame_index % playback_frames;
474+
}
475+
bubble_animation_release_frame(&model->freeze_frame);
400476
frame_rate = model->current->icon_animation.frame_rate;
401477
view_commit_model(view->view, true);
402478

applications/services/desktop/animations/views/bubble_animation_view.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ View* bubble_animation_get_view(BubbleAnimationView* view);
8181
*/
8282
void bubble_animation_freeze(BubbleAnimationView* view);
8383

84+
/**
85+
* Animate the small in-memory preview while the full animation is restored.
86+
*
87+
* @view bubble animation view instance
88+
*/
89+
void bubble_animation_start_resume_preview(BubbleAnimationView* view);
90+
8491
/**
8592
* Starts bubble animation after freezing.
8693
*
6 Bytes
Loading
-3 Bytes
Loading
5 Bytes
Loading

tools/tumoflip/sync_readme_version.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ def sync_readme_text(
8383
)
8484
if iteration:
8585
updated = re.sub(
86-
r"- `\d{3}`: development iteration inside the tumoflip internal build version\.",
86+
r"- (?:`\d{3}`: development iteration inside the tumoflip internal build version|"
87+
r"`<iteration>`: monotonically increasing revision used only by `t-dev` builds)\.",
8788
f"- `{iteration}`: development iteration inside the tumoflip internal build version.",
8889
updated,
8990
count=1,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
3+
import unittest
4+
from pathlib import Path
5+
6+
7+
REPO_ROOT = Path(__file__).resolve().parents[2]
8+
9+
10+
class DesktopAnimationResumeTest(unittest.TestCase):
11+
def test_freeze_keeps_a_bounded_sequential_preview(self) -> None:
12+
source = (
13+
REPO_ROOT
14+
/ "applications/services/desktop/animations/views/bubble_animation_view.c"
15+
).read_text(encoding="utf-8")
16+
17+
clone = source.split("static Icon* bubble_animation_clone_preview", 1)[1].split(
18+
"static void bubble_animation_release_frame", 1
19+
)[0]
20+
self.assertIn("BUBBLE_ANIMATION_RESUME_PREVIEW_MAX_BYTES", clone)
21+
self.assertIn("animation->passive_frames ?", clone)
22+
self.assertIn("animation->active_frames", clone)
23+
self.assertIn("animation->frame_order[i]", clone)
24+
self.assertIn("memcpy(frame, source_frame, frame_size)", clone)
25+
self.assertNotIn("compress_icon_decode", clone)
26+
27+
freeze = source.split("void bubble_animation_freeze", 1)[1].split(
28+
"void bubble_animation_start_resume_preview", 1
29+
)[0]
30+
self.assertIn("bubble_animation_clone_preview(model->current)", freeze)
31+
self.assertIn("furi_timer_stop(view->timer)", freeze)
32+
33+
def test_preview_runs_only_during_desktop_resume(self) -> None:
34+
view_source = (
35+
REPO_ROOT
36+
/ "applications/services/desktop/animations/views/bubble_animation_view.c"
37+
).read_text(encoding="utf-8")
38+
manager_source = (
39+
REPO_ROOT
40+
/ "applications/services/desktop/animations/animation_manager.c"
41+
).read_text(encoding="utf-8")
42+
43+
preview = view_source.split("void bubble_animation_start_resume_preview", 1)[1].split(
44+
"void bubble_animation_unfreeze", 1
45+
)[0]
46+
self.assertIn("frame_count > 1U", preview)
47+
self.assertIn("furi_timer_start", preview)
48+
49+
restore = manager_source.split(
50+
"void animation_manager_load_and_continue_animation", 1
51+
)[1].split("static void animation_manager_switch_to_one_shot_view", 1)[0]
52+
self.assertLess(
53+
restore.index("bubble_animation_start_resume_preview"),
54+
restore.index("animation_manager_reload_profile"),
55+
)
56+
self.assertLess(
57+
restore.index("bubble_animation_start_resume_preview"),
58+
restore.index("animation_storage_find_animation"),
59+
)
60+
self.assertLess(
61+
restore.index("animation_storage_find_animation"),
62+
restore.index("bubble_animation_unfreeze"),
63+
)
64+
65+
unfreeze = view_source.split("void bubble_animation_unfreeze", 1)[1]
66+
self.assertIn("model->freeze_frame_index % playback_frames", unfreeze)
67+
self.assertLess(
68+
unfreeze.index("model->current_frame ="),
69+
unfreeze.index("bubble_animation_release_frame"),
70+
)
71+
72+
73+
if __name__ == "__main__":
74+
unittest.main()

tools/tumoflip/test_readme_version_sync.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,26 @@ def test_sync_updates_dev_versions(self) -> None:
9595
)
9696
self.assertNotIn("tmwhflpprarf089-034", updated)
9797

98+
def test_sync_starts_dev_iteration_from_stable_readme(self) -> None:
99+
original = """# tumoflip
100+
- Firmware version: `t-flppr-fw-089-039`
101+
- Release channel: `main stable line`
102+
- Release package: `flipper-z-f7-update-t-flppr-fw-089-039.tgz`
103+
- `t-dev`: Tumoflip development build prefix for unstable builds.
104+
- `089`: upstream Unleashed base version.
105+
- `039`: tumoflip internal build version.
106+
- `<iteration>`: monotonically increasing revision used only by `t-dev` builds.
107+
"""
108+
updated = sync_readme_text(original, "t-dev-089-039-001")
109+
110+
self.assertIn("Firmware version: `t-dev-089-039-001`", updated)
111+
self.assertIn("Release channel: `dev experimental line`", updated)
112+
self.assertIn(
113+
"- `001`: development iteration inside the tumoflip internal build version.",
114+
updated,
115+
)
116+
self.assertNotIn("<iteration>", updated)
117+
98118
def test_readme_is_synced_with_dist_suffix(self) -> None:
99119
readme = (REPO_ROOT / "ReadMe.md").read_text(encoding="utf-8")
100120
self.assertEqual(sync_readme_text(readme, fbt_options.DIST_SUFFIX), readme)

0 commit comments

Comments
 (0)