[FL-2627] Flipper applications: SDK, build and debug system (#1387)

* Added support for running applications from SD card (FAPs - Flipper Application Packages)
* Added plugin_dist target for fbt to build FAPs
* All apps of type FlipperAppType.EXTERNAL and FlipperAppType.PLUGIN are built as FAPs by default
* Updated VSCode configuration for new fbt features - re-deploy stock configuration to use them
* Added debugging support for FAPs with fbt debug & VSCode
* Added public firmware API with automated versioning

Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: SG <who.just.the.doctor@gmail.com>
Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
SG
2022-09-15 02:11:38 +10:00
committed by Aleksandr Kutuzov
parent 0f6f9ad52e
commit b9a766d909
895 changed files with 8862 additions and 1465 deletions

View File

@@ -0,0 +1,575 @@
#include <gui/view_stack.h>
#include <stdint.h>
#include <furi.h>
#include <furi_hal.h>
#include <m-string.h>
#include <portmacro.h>
#include <dolphin/dolphin.h>
#include <power/power_service/power.h>
#include <storage/storage.h>
#include <assets_icons.h>
#include "views/bubble_animation_view.h"
#include "views/one_shot_animation_view.h"
#include "animation_storage.h"
#include "animation_manager.h"
#define TAG "AnimationManager"
#define HARDCODED_ANIMATION_NAME "L1_Tv_128x47"
#define NO_SD_ANIMATION_NAME "L1_NoSd_128x49"
#define BAD_BATTERY_ANIMATION_NAME "L1_BadBattery_128x47"
#define NO_DB_ANIMATION_NAME "L0_NoDb_128x51"
#define BAD_SD_ANIMATION_NAME "L0_SdBad_128x51"
#define SD_OK_ANIMATION_NAME "L0_SdOk_128x51"
#define URL_ANIMATION_NAME "L0_Url_128x51"
#define NEW_MAIL_ANIMATION_NAME "L0_NewMail_128x51"
typedef enum {
AnimationManagerStateIdle,
AnimationManagerStateBlocked,
AnimationManagerStateFreezedIdle,
AnimationManagerStateFreezedBlocked,
} AnimationManagerState;
struct AnimationManager {
bool sd_show_url;
bool sd_shown_no_db;
bool sd_shown_sd_ok;
bool levelup_pending;
bool levelup_active;
AnimationManagerState state;
FuriPubSubSubscription* pubsub_subscription_storage;
FuriPubSubSubscription* pubsub_subscription_dolphin;
BubbleAnimationView* animation_view;
OneShotView* one_shot_view;
FuriTimer* idle_animation_timer;
StorageAnimation* current_animation;
AnimationManagerInteractCallback interact_callback;
AnimationManagerSetNewIdleAnimationCallback new_idle_callback;
AnimationManagerSetNewIdleAnimationCallback check_blocking_callback;
void* context;
string_t freezed_animation_name;
int32_t freezed_animation_time_left;
ViewStack* view_stack;
};
static StorageAnimation*
animation_manager_select_idle_animation(AnimationManager* animation_manager);
static void animation_manager_replace_current_animation(
AnimationManager* animation_manager,
StorageAnimation* storage_animation);
static void animation_manager_start_new_idle(AnimationManager* animation_manager);
static bool animation_manager_check_blocking(AnimationManager* animation_manager);
static bool animation_manager_is_valid_idle_animation(
const StorageAnimationManifestInfo* info,
const DolphinStats* stats);
static void animation_manager_switch_to_one_shot_view(AnimationManager* animation_manager);
static void animation_manager_switch_to_animation_view(AnimationManager* animation_manager);
void animation_manager_set_context(AnimationManager* animation_manager, void* context) {
furi_assert(animation_manager);
animation_manager->context = context;
}
void animation_manager_set_new_idle_callback(
AnimationManager* animation_manager,
AnimationManagerSetNewIdleAnimationCallback callback) {
furi_assert(animation_manager);
animation_manager->new_idle_callback = callback;
}
void animation_manager_set_check_callback(
AnimationManager* animation_manager,
AnimationManagerCheckBlockingCallback callback) {
furi_assert(animation_manager);
animation_manager->check_blocking_callback = callback;
}
void animation_manager_set_interact_callback(
AnimationManager* animation_manager,
AnimationManagerInteractCallback callback) {
furi_assert(animation_manager);
animation_manager->interact_callback = callback;
}
static void animation_manager_check_blocking_callback(const void* message, void* context) {
const StorageEvent* storage_event = message;
switch(storage_event->type) {
case StorageEventTypeCardMount:
case StorageEventTypeCardUnmount:
case StorageEventTypeCardMountError:
furi_assert(context);
AnimationManager* animation_manager = context;
if(animation_manager->check_blocking_callback) {
animation_manager->check_blocking_callback(animation_manager->context);
}
break;
default:
break;
}
}
static void animation_manager_timer_callback(void* context) {
furi_assert(context);
AnimationManager* animation_manager = context;
if(animation_manager->new_idle_callback) {
animation_manager->new_idle_callback(animation_manager->context);
}
}
static void animation_manager_interact_callback(void* context) {
furi_assert(context);
AnimationManager* animation_manager = context;
if(animation_manager->interact_callback) {
animation_manager->interact_callback(animation_manager->context);
}
}
/* reaction to animation_manager->check_blocking_callback() */
void animation_manager_check_blocking_process(AnimationManager* animation_manager) {
furi_assert(animation_manager);
if(animation_manager->state == AnimationManagerStateIdle) {
bool blocked = animation_manager_check_blocking(animation_manager);
if(!blocked) {
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
DolphinStats stats = dolphin_stats(dolphin);
furi_record_close(RECORD_DOLPHIN);
const StorageAnimationManifestInfo* manifest_info =
animation_storage_get_meta(animation_manager->current_animation);
bool valid = animation_manager_is_valid_idle_animation(manifest_info, &stats);
if(!valid) {
animation_manager_start_new_idle(animation_manager);
}
}
}
}
/* reaction to animation_manager->new_idle_callback() */
void animation_manager_new_idle_process(AnimationManager* animation_manager) {
furi_assert(animation_manager);
if(animation_manager->state == AnimationManagerStateIdle) {
animation_manager_start_new_idle(animation_manager);
}
}
/* reaction to animation_manager->interact_callback() */
bool animation_manager_interact_process(AnimationManager* animation_manager) {
furi_assert(animation_manager);
bool consumed = true;
if(animation_manager->levelup_pending) {
animation_manager->levelup_pending = false;
animation_manager->levelup_active = true;
animation_manager_switch_to_one_shot_view(animation_manager);
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
dolphin_upgrade_level(dolphin);
furi_record_close(RECORD_DOLPHIN);
} else if(animation_manager->levelup_active) {
animation_manager->levelup_active = false;
animation_manager_start_new_idle(animation_manager);
animation_manager_switch_to_animation_view(animation_manager);
} else if(animation_manager->state == AnimationManagerStateBlocked) {
bool blocked = animation_manager_check_blocking(animation_manager);
if(!blocked) {
animation_manager_start_new_idle(animation_manager);
}
} else {
consumed = false;
}
return consumed;
}
static void animation_manager_start_new_idle(AnimationManager* animation_manager) {
furi_assert(animation_manager);
StorageAnimation* new_animation = animation_manager_select_idle_animation(animation_manager);
animation_manager_replace_current_animation(animation_manager, new_animation);
const BubbleAnimation* bubble_animation =
animation_storage_get_bubble_animation(animation_manager->current_animation);
animation_manager->state = AnimationManagerStateIdle;
furi_timer_start(animation_manager->idle_animation_timer, bubble_animation->duration * 1000);
}
static bool animation_manager_check_blocking(AnimationManager* animation_manager) {
furi_assert(animation_manager);
StorageAnimation* blocking_animation = NULL;
Storage* storage = furi_record_open(RECORD_STORAGE);
FS_Error sd_status = storage_sd_status(storage);
if(sd_status == FSE_INTERNAL) {
blocking_animation = animation_storage_find_animation(BAD_SD_ANIMATION_NAME);
furi_assert(blocking_animation);
} else if(sd_status == FSE_NOT_READY) {
animation_manager->sd_shown_sd_ok = false;
animation_manager->sd_shown_no_db = false;
} else if(sd_status == FSE_OK) {
if(!animation_manager->sd_shown_sd_ok) {
blocking_animation = animation_storage_find_animation(SD_OK_ANIMATION_NAME);
furi_assert(blocking_animation);
animation_manager->sd_shown_sd_ok = true;
} else if(!animation_manager->sd_shown_no_db) {
if(!storage_file_exists(storage, EXT_PATH("Manifest"))) {
blocking_animation = animation_storage_find_animation(NO_DB_ANIMATION_NAME);
furi_assert(blocking_animation);
animation_manager->sd_shown_no_db = true;
animation_manager->sd_show_url = true;
}
} else if(animation_manager->sd_show_url) {
blocking_animation = animation_storage_find_animation(URL_ANIMATION_NAME);
furi_assert(blocking_animation);
animation_manager->sd_show_url = false;
}
}
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
DolphinStats stats = dolphin_stats(dolphin);
furi_record_close(RECORD_DOLPHIN);
if(!blocking_animation && stats.level_up_is_pending) {
blocking_animation = animation_storage_find_animation(NEW_MAIL_ANIMATION_NAME);
furi_assert(blocking_animation);
if(blocking_animation) {
animation_manager->levelup_pending = true;
}
}
if(blocking_animation) {
furi_timer_stop(animation_manager->idle_animation_timer);
animation_manager_replace_current_animation(animation_manager, blocking_animation);
/* no timer starting because this is blocking animation */
animation_manager->state = AnimationManagerStateBlocked;
}
furi_record_close(RECORD_STORAGE);
return !!blocking_animation;
}
static void animation_manager_replace_current_animation(
AnimationManager* animation_manager,
StorageAnimation* storage_animation) {
furi_assert(storage_animation);
StorageAnimation* previous_animation = animation_manager->current_animation;
const BubbleAnimation* animation = animation_storage_get_bubble_animation(storage_animation);
bubble_animation_view_set_animation(animation_manager->animation_view, animation);
const char* new_name = animation_storage_get_meta(storage_animation)->name;
FURI_LOG_I(TAG, "Select \'%s\' animation", new_name);
animation_manager->current_animation = storage_animation;
if(previous_animation) {
animation_storage_free_storage_animation(&previous_animation);
}
}
AnimationManager* animation_manager_alloc(void) {
AnimationManager* animation_manager = malloc(sizeof(AnimationManager));
animation_manager->animation_view = bubble_animation_view_alloc();
animation_manager->view_stack = view_stack_alloc();
View* animation_view = bubble_animation_get_view(animation_manager->animation_view);
view_stack_add_view(animation_manager->view_stack, animation_view);
string_init(animation_manager->freezed_animation_name);
animation_manager->idle_animation_timer =
furi_timer_alloc(animation_manager_timer_callback, FuriTimerTypeOnce, animation_manager);
bubble_animation_view_set_interact_callback(
animation_manager->animation_view, animation_manager_interact_callback, animation_manager);
Storage* storage = furi_record_open(RECORD_STORAGE);
animation_manager->pubsub_subscription_storage = furi_pubsub_subscribe(
storage_get_pubsub(storage), animation_manager_check_blocking_callback, animation_manager);
furi_record_close(RECORD_STORAGE);
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
animation_manager->pubsub_subscription_dolphin = furi_pubsub_subscribe(
dolphin_get_pubsub(dolphin), animation_manager_check_blocking_callback, animation_manager);
furi_record_close(RECORD_DOLPHIN);
animation_manager->sd_shown_sd_ok = true;
if(!animation_manager_check_blocking(animation_manager)) {
animation_manager_start_new_idle(animation_manager);
}
return animation_manager;
}
void animation_manager_free(AnimationManager* animation_manager) {
furi_assert(animation_manager);
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
furi_pubsub_unsubscribe(
dolphin_get_pubsub(dolphin), animation_manager->pubsub_subscription_dolphin);
furi_record_close(RECORD_DOLPHIN);
Storage* storage = furi_record_open(RECORD_STORAGE);
furi_pubsub_unsubscribe(
storage_get_pubsub(storage), animation_manager->pubsub_subscription_storage);
furi_record_close(RECORD_STORAGE);
string_clear(animation_manager->freezed_animation_name);
View* animation_view = bubble_animation_get_view(animation_manager->animation_view);
view_stack_remove_view(animation_manager->view_stack, animation_view);
bubble_animation_view_free(animation_manager->animation_view);
furi_timer_free(animation_manager->idle_animation_timer);
}
View* animation_manager_get_animation_view(AnimationManager* animation_manager) {
furi_assert(animation_manager);
return view_stack_get_view(animation_manager->view_stack);
}
static bool animation_manager_is_valid_idle_animation(
const StorageAnimationManifestInfo* info,
const DolphinStats* stats) {
furi_assert(info);
furi_assert(info->name);
bool result = true;
if(!strcmp(info->name, BAD_BATTERY_ANIMATION_NAME)) {
Power* power = furi_record_open(RECORD_POWER);
bool battery_is_well = power_is_battery_healthy(power);
furi_record_close(RECORD_POWER);
result = !battery_is_well;
}
if(!strcmp(info->name, NO_SD_ANIMATION_NAME)) {
Storage* storage = furi_record_open(RECORD_STORAGE);
FS_Error sd_status = storage_sd_status(storage);
furi_record_close(RECORD_STORAGE);
result = (sd_status == FSE_NOT_READY);
}
if((stats->butthurt < info->min_butthurt) || (stats->butthurt > info->max_butthurt)) {
result = false;
}
if((stats->level < info->min_level) || (stats->level > info->max_level)) {
result = false;
}
return result;
}
static StorageAnimation*
animation_manager_select_idle_animation(AnimationManager* animation_manager) {
UNUSED(animation_manager);
StorageAnimationList_t animation_list;
StorageAnimationList_init(animation_list);
animation_storage_fill_animation_list(&animation_list);
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
DolphinStats stats = dolphin_stats(dolphin);
furi_record_close(RECORD_DOLPHIN);
uint32_t whole_weight = 0;
StorageAnimationList_it_t it;
for(StorageAnimationList_it(it, animation_list); !StorageAnimationList_end_p(it);) {
StorageAnimation* storage_animation = *StorageAnimationList_ref(it);
const StorageAnimationManifestInfo* manifest_info =
animation_storage_get_meta(storage_animation);
bool valid = animation_manager_is_valid_idle_animation(manifest_info, &stats);
if(valid) {
whole_weight += manifest_info->weight;
StorageAnimationList_next(it);
} else {
animation_storage_free_storage_animation(&storage_animation);
/* remove and increase iterator */
StorageAnimationList_remove(animation_list, it);
}
}
uint32_t lucky_number = furi_hal_random_get() % whole_weight;
uint32_t weight = 0;
StorageAnimation* selected = NULL;
for
M_EACH(item, animation_list, StorageAnimationList_t) {
if(lucky_number < weight) {
break;
}
weight += animation_storage_get_meta(*item)->weight;
selected = *item;
}
for
M_EACH(item, animation_list, StorageAnimationList_t) {
if(*item != selected) {
animation_storage_free_storage_animation(item);
}
}
StorageAnimationList_clear(animation_list);
/* cache animation, if failed - choose reliable animation */
if(!animation_storage_get_bubble_animation(selected)) {
const char* name = animation_storage_get_meta(selected)->name;
FURI_LOG_E(TAG, "Can't upload animation described in manifest: \'%s\'", name);
animation_storage_free_storage_animation(&selected);
selected = animation_storage_find_animation(HARDCODED_ANIMATION_NAME);
}
furi_assert(selected);
return selected;
}
bool animation_manager_is_animation_loaded(AnimationManager* animation_manager) {
furi_assert(animation_manager);
return animation_manager->current_animation;
}
void animation_manager_unload_and_stall_animation(AnimationManager* animation_manager) {
furi_assert(animation_manager);
furi_assert(animation_manager->current_animation);
furi_assert(!string_size(animation_manager->freezed_animation_name));
furi_assert(
(animation_manager->state == AnimationManagerStateIdle) ||
(animation_manager->state == AnimationManagerStateBlocked));
if(animation_manager->state == AnimationManagerStateBlocked) {
animation_manager->state = AnimationManagerStateFreezedBlocked;
} else if(animation_manager->state == AnimationManagerStateIdle) {
animation_manager->state = AnimationManagerStateFreezedIdle;
animation_manager->freezed_animation_time_left =
xTimerGetExpiryTime(animation_manager->idle_animation_timer) - xTaskGetTickCount();
if(animation_manager->freezed_animation_time_left < 0) {
animation_manager->freezed_animation_time_left = 0;
}
furi_timer_stop(animation_manager->idle_animation_timer);
} else {
furi_assert(0);
}
FURI_LOG_I(
TAG,
"Unload animation \'%s\'",
animation_storage_get_meta(animation_manager->current_animation)->name);
StorageAnimationManifestInfo* meta =
animation_storage_get_meta(animation_manager->current_animation);
/* copy str, not move, because it can be internal animation */
string_set_str(animation_manager->freezed_animation_name, meta->name);
bubble_animation_freeze(animation_manager->animation_view);
animation_storage_free_storage_animation(&animation_manager->current_animation);
}
void animation_manager_load_and_continue_animation(AnimationManager* animation_manager) {
furi_assert(animation_manager);
furi_assert(!animation_manager->current_animation);
furi_assert(string_size(animation_manager->freezed_animation_name));
furi_assert(
(animation_manager->state == AnimationManagerStateFreezedIdle) ||
(animation_manager->state == AnimationManagerStateFreezedBlocked));
if(animation_manager->state == AnimationManagerStateFreezedBlocked) {
StorageAnimation* restore_animation = animation_storage_find_animation(
string_get_cstr(animation_manager->freezed_animation_name));
/* all blocked animations must be in flipper -> we can
* always find blocking animation */
furi_assert(restore_animation);
animation_manager_replace_current_animation(animation_manager, restore_animation);
animation_manager->state = AnimationManagerStateBlocked;
} else if(animation_manager->state == AnimationManagerStateFreezedIdle) {
/* check if we missed some system notifications, and set current_animation */
bool blocked = animation_manager_check_blocking(animation_manager);
if(!blocked) {
/* if no blocking - try restore last one idle */
StorageAnimation* restore_animation = animation_storage_find_animation(
string_get_cstr(animation_manager->freezed_animation_name));
if(restore_animation) {
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
DolphinStats stats = dolphin_stats(dolphin);
furi_record_close(RECORD_DOLPHIN);
const StorageAnimationManifestInfo* manifest_info =
animation_storage_get_meta(restore_animation);
bool valid = animation_manager_is_valid_idle_animation(manifest_info, &stats);
if(valid) {
animation_manager_replace_current_animation(
animation_manager, restore_animation);
animation_manager->state = AnimationManagerStateIdle;
if(animation_manager->freezed_animation_time_left) {
furi_timer_start(
animation_manager->idle_animation_timer,
animation_manager->freezed_animation_time_left);
} else {
const BubbleAnimation* animation = animation_storage_get_bubble_animation(
animation_manager->current_animation);
furi_timer_start(
animation_manager->idle_animation_timer, animation->duration * 1000);
}
}
} else {
FURI_LOG_E(
TAG,
"Failed to restore \'%s\'",
string_get_cstr(animation_manager->freezed_animation_name));
}
}
} else {
/* Unknown state is an error. But not in release version.*/
furi_assert(0);
}
/* if can't restore previous animation - select new */
if(!animation_manager->current_animation) {
animation_manager_start_new_idle(animation_manager);
}
FURI_LOG_I(
TAG,
"Load animation \'%s\'",
animation_storage_get_meta(animation_manager->current_animation)->name);
bubble_animation_unfreeze(animation_manager->animation_view);
string_reset(animation_manager->freezed_animation_name);
furi_assert(animation_manager->current_animation);
}
static void animation_manager_switch_to_one_shot_view(AnimationManager* animation_manager) {
furi_assert(animation_manager);
furi_assert(!animation_manager->one_shot_view);
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
DolphinStats stats = dolphin_stats(dolphin);
furi_record_close(RECORD_DOLPHIN);
animation_manager->one_shot_view = one_shot_view_alloc();
one_shot_view_set_interact_callback(
animation_manager->one_shot_view, animation_manager_interact_callback, animation_manager);
View* prev_view = bubble_animation_get_view(animation_manager->animation_view);
View* next_view = one_shot_view_get_view(animation_manager->one_shot_view);
view_stack_remove_view(animation_manager->view_stack, prev_view);
view_stack_add_view(animation_manager->view_stack, next_view);
if(stats.level == 1) {
one_shot_view_start_animation(animation_manager->one_shot_view, &A_Levelup1_128x64);
} else if(stats.level == 2) {
one_shot_view_start_animation(animation_manager->one_shot_view, &A_Levelup2_128x64);
} else {
furi_assert(0);
}
}
static void animation_manager_switch_to_animation_view(AnimationManager* animation_manager) {
furi_assert(animation_manager);
furi_assert(animation_manager->one_shot_view);
View* prev_view = one_shot_view_get_view(animation_manager->one_shot_view);
View* next_view = bubble_animation_get_view(animation_manager->animation_view);
view_stack_remove_view(animation_manager->view_stack, prev_view);
view_stack_add_view(animation_manager->view_stack, next_view);
one_shot_view_free(animation_manager->one_shot_view);
animation_manager->one_shot_view = NULL;
}

View File

@@ -0,0 +1,159 @@
#pragma once
#include <gui/view.h>
#include <gui/icon_i.h>
#include <stdint.h>
#include <dolphin/dolphin.h>
typedef struct AnimationManager AnimationManager;
typedef struct {
uint8_t x;
uint8_t y;
const char* text;
Align align_h;
Align align_v;
} Bubble;
typedef struct FrameBubble {
Bubble bubble;
uint8_t start_frame;
uint8_t end_frame;
const struct FrameBubble* next_bubble;
} FrameBubble;
typedef struct {
const FrameBubble* const* frame_bubble_sequences;
uint8_t frame_bubble_sequences_count;
const Icon icon_animation;
const uint8_t* frame_order;
uint8_t passive_frames;
uint8_t active_frames;
uint8_t active_cycles;
uint16_t duration;
uint16_t active_cooldown;
} BubbleAnimation;
typedef void (*AnimationManagerSetNewIdleAnimationCallback)(void* context);
typedef void (*AnimationManagerCheckBlockingCallback)(void* context);
typedef void (*AnimationManagerInteractCallback)(void*);
/**
* Allocate Animation Manager
*
* @return animation manager instance
*/
AnimationManager* animation_manager_alloc(void);
/**
* Free Animation Manager
*
* @animation_manager instance
*/
void animation_manager_free(AnimationManager* animation_manager);
/**
* Get View of Animation Manager
*
* @animation_manager instance
* @return view
*/
View* animation_manager_get_animation_view(AnimationManager* animation_manager);
/**
* Set context for all callbacks for Animation Manager
*
* @animation_manager instance
* @context context
*/
void animation_manager_set_context(AnimationManager* animation_manager, void* context);
/**
* Set callback for Animation Manager for defered calls
* for animation_manager_new_idle_process().
* Animation Manager doesn't have it's own thread, so main thread gives
* callbacks to A.M. to call when it should perform some inner manipulations.
* This callback is called from other threads and should notify main thread
* when to call animation_manager_new_idle_process().
* So scheme is this:
* A.M. sets callbacks,
* callbacks notifies main thread
* main thread in its own context calls appropriate *_process() function.
*
* @animation_manager instance
* @callback callback
*/
void animation_manager_set_new_idle_callback(
AnimationManager* animation_manager,
AnimationManagerSetNewIdleAnimationCallback callback);
/**
* Function to call in main thread as a response to
* set_new_idle_callback's call.
*
* @animation_manager instance
*/
void animation_manager_new_idle_process(AnimationManager* animation_manager);
/**
* Set callback for Animation Manager for defered calls
* for animation_manager_check_blocking_process().
*
* @animation_manager instance
* @callback callback
*/
void animation_manager_set_check_callback(
AnimationManager* animation_manager,
AnimationManagerCheckBlockingCallback callback);
/**
* Function to call in main thread as a response to
* set_new_idle_callback's call.
*
* @animation_manager instance
*/
void animation_manager_check_blocking_process(AnimationManager* animation_manager);
/**
* Set callback for Animation Manager for defered calls
* for animation_manager_interact_process().
*
* @animation_manager instance
* @callback callback
*/
void animation_manager_set_interact_callback(
AnimationManager* animation_manager,
AnimationManagerInteractCallback callback);
/**
* Function to call in main thread as a response to
* set_new_idle_callback's call.
*
* @animation_manager instance
* @return true if event was consumed
*/
bool animation_manager_interact_process(AnimationManager* animation_manager);
/** Check if animation loaded
*
* @animation_manager instance
*/
bool animation_manager_is_animation_loaded(AnimationManager* animation_manager);
/**
* Unload and Stall animation actions. Draw callback in view
* paints first frame of current animation until
* animation_manager_load_and_continue_animation() is called.
* Can't be called multiple times. Every Stall has to be finished
* with Continue.
*
* @animation_manager instance
*/
void animation_manager_unload_and_stall_animation(AnimationManager* animation_manager);
/**
* Load and Contunue execution of animation manager.
*
* @animation_manager instance
*/
void animation_manager_load_and_continue_animation(AnimationManager* animation_manager);

View File

@@ -0,0 +1,535 @@
#include <stdint.h>
#include <flipper_format/flipper_format.h>
#include <furi.h>
#include <core/dangerous_defines.h>
#include <storage/storage.h>
#include <gui/icon_i.h>
#include <m-string.h>
#include "animation_manager.h"
#include "animation_storage.h"
#include "animation_storage_i.h"
#include <assets_dolphin_internal.h>
#include <assets_dolphin_blocking.h>
#define ANIMATION_META_FILE "meta.txt"
#define ANIMATION_DIR EXT_PATH("dolphin")
#define ANIMATION_MANIFEST_FILE ANIMATION_DIR "/manifest.txt"
#define TAG "AnimationStorage"
static void animation_storage_free_bubbles(BubbleAnimation* animation);
static void animation_storage_free_frames(BubbleAnimation* animation);
static void animation_storage_free_animation(BubbleAnimation** storage_animation);
static BubbleAnimation* animation_storage_load_animation(const char* name);
static bool animation_storage_load_single_manifest_info(
StorageAnimationManifestInfo* manifest_info,
const char* name) {
furi_assert(manifest_info);
bool result = false;
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* file = flipper_format_file_alloc(storage);
flipper_format_set_strict_mode(file, true);
string_t read_string;
string_init(read_string);
do {
uint32_t u32value;
if(FSE_OK != storage_sd_status(storage)) break;
if(!flipper_format_file_open_existing(file, ANIMATION_MANIFEST_FILE)) break;
if(!flipper_format_read_header(file, read_string, &u32value)) break;
if(string_cmp_str(read_string, "Flipper Animation Manifest")) break;
manifest_info->name = NULL;
/* skip other animation names */
flipper_format_set_strict_mode(file, false);
while(flipper_format_read_string(file, "Name", read_string) &&
string_cmp_str(read_string, name))
;
if(string_cmp_str(read_string, name)) break;
flipper_format_set_strict_mode(file, true);
manifest_info->name = malloc(string_size(read_string) + 1);
strcpy((char*)manifest_info->name, string_get_cstr(read_string));
if(!flipper_format_read_uint32(file, "Min butthurt", &u32value, 1)) break;
manifest_info->min_butthurt = u32value;
if(!flipper_format_read_uint32(file, "Max butthurt", &u32value, 1)) break;
manifest_info->max_butthurt = u32value;
if(!flipper_format_read_uint32(file, "Min level", &u32value, 1)) break;
manifest_info->min_level = u32value;
if(!flipper_format_read_uint32(file, "Max level", &u32value, 1)) break;
manifest_info->max_level = u32value;
if(!flipper_format_read_uint32(file, "Weight", &u32value, 1)) break;
manifest_info->weight = u32value;
result = true;
} while(0);
if(!result && manifest_info->name) {
free((void*)manifest_info->name);
}
string_clear(read_string);
flipper_format_free(file);
furi_record_close(RECORD_STORAGE);
return result;
}
void animation_storage_fill_animation_list(StorageAnimationList_t* animation_list) {
furi_assert(sizeof(StorageAnimationList_t) == sizeof(void*));
furi_assert(!StorageAnimationList_size(*animation_list));
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* file = flipper_format_file_alloc(storage);
/* Forbid skipping fields */
flipper_format_set_strict_mode(file, true);
string_t read_string;
string_init(read_string);
do {
uint32_t u32value;
StorageAnimation* storage_animation = NULL;
if(FSE_OK != storage_sd_status(storage)) break;
if(!flipper_format_file_open_existing(file, ANIMATION_MANIFEST_FILE)) break;
if(!flipper_format_read_header(file, read_string, &u32value)) break;
if(string_cmp_str(read_string, "Flipper Animation Manifest")) break;
do {
storage_animation = malloc(sizeof(StorageAnimation));
storage_animation->external = true;
storage_animation->animation = NULL;
storage_animation->manifest_info.name = NULL;
if(!flipper_format_read_string(file, "Name", read_string)) break;
storage_animation->manifest_info.name = malloc(string_size(read_string) + 1);
strcpy((char*)storage_animation->manifest_info.name, string_get_cstr(read_string));
if(!flipper_format_read_uint32(file, "Min butthurt", &u32value, 1)) break;
storage_animation->manifest_info.min_butthurt = u32value;
if(!flipper_format_read_uint32(file, "Max butthurt", &u32value, 1)) break;
storage_animation->manifest_info.max_butthurt = u32value;
if(!flipper_format_read_uint32(file, "Min level", &u32value, 1)) break;
storage_animation->manifest_info.min_level = u32value;
if(!flipper_format_read_uint32(file, "Max level", &u32value, 1)) break;
storage_animation->manifest_info.max_level = u32value;
if(!flipper_format_read_uint32(file, "Weight", &u32value, 1)) break;
storage_animation->manifest_info.weight = u32value;
StorageAnimationList_push_back(*animation_list, storage_animation);
} while(1);
animation_storage_free_storage_animation(&storage_animation);
} while(0);
string_clear(read_string);
flipper_format_free(file);
// add hard-coded animations
for(size_t i = 0; i < dolphin_internal_size; ++i) {
StorageAnimationList_push_back(*animation_list, (StorageAnimation*)&dolphin_internal[i]);
}
furi_record_close(RECORD_STORAGE);
}
StorageAnimation* animation_storage_find_animation(const char* name) {
furi_assert(name);
furi_assert(strlen(name));
StorageAnimation* storage_animation = NULL;
for(size_t i = 0; i < dolphin_blocking_size; ++i) {
if(!strcmp(dolphin_blocking[i].manifest_info.name, name)) {
storage_animation = (StorageAnimation*)&dolphin_blocking[i];
break;
}
}
if(!storage_animation) {
for(size_t i = 0; i < dolphin_internal_size; ++i) {
if(!strcmp(dolphin_internal[i].manifest_info.name, name)) {
storage_animation = (StorageAnimation*)&dolphin_internal[i];
break;
}
}
}
/* look through external animations */
if(!storage_animation) {
storage_animation = malloc(sizeof(StorageAnimation));
storage_animation->external = true;
bool result = false;
result =
animation_storage_load_single_manifest_info(&storage_animation->manifest_info, name);
if(result) {
storage_animation->animation = animation_storage_load_animation(name);
result = !!storage_animation->animation;
}
if(!result) {
animation_storage_free_storage_animation(&storage_animation);
}
}
return storage_animation;
}
StorageAnimationManifestInfo* animation_storage_get_meta(StorageAnimation* storage_animation) {
furi_assert(storage_animation);
return &storage_animation->manifest_info;
}
const BubbleAnimation*
animation_storage_get_bubble_animation(StorageAnimation* storage_animation) {
furi_assert(storage_animation);
animation_storage_cache_animation(storage_animation);
return storage_animation->animation;
}
void animation_storage_cache_animation(StorageAnimation* storage_animation) {
furi_assert(storage_animation);
if(storage_animation->external) {
if(!storage_animation->animation) {
storage_animation->animation =
animation_storage_load_animation(storage_animation->manifest_info.name);
}
}
}
static void animation_storage_free_animation(BubbleAnimation** animation) {
furi_assert(animation);
if(*animation) {
animation_storage_free_bubbles(*animation);
animation_storage_free_frames(*animation);
if((*animation)->frame_order) {
free((void*)(*animation)->frame_order);
}
free(*animation);
*animation = NULL;
}
}
void animation_storage_free_storage_animation(StorageAnimation** storage_animation) {
furi_assert(storage_animation);
furi_assert(*storage_animation);
if((*storage_animation)->external) {
animation_storage_free_animation((BubbleAnimation**)&(*storage_animation)->animation);
if((*storage_animation)->manifest_info.name) {
free((void*)(*storage_animation)->manifest_info.name);
}
free(*storage_animation);
}
*storage_animation = NULL;
}
static bool animation_storage_cast_align(string_t align_str, Align* align) {
if(!string_cmp_str(align_str, "Bottom")) {
*align = AlignBottom;
} else if(!string_cmp_str(align_str, "Top")) {
*align = AlignTop;
} else if(!string_cmp_str(align_str, "Left")) {
*align = AlignLeft;
} else if(!string_cmp_str(align_str, "Right")) {
*align = AlignRight;
} else if(!string_cmp_str(align_str, "Center")) {
*align = AlignCenter;
} else {
return false;
}
return true;
}
static void animation_storage_free_frames(BubbleAnimation* animation) {
furi_assert(animation);
const Icon* icon = &animation->icon_animation;
for(int i = 0; i < icon->frame_count; ++i) {
if(icon->frames[i]) {
free((void*)icon->frames[i]);
}
}
free((void*)icon->frames);
}
static bool animation_storage_load_frames(
Storage* storage,
const char* name,
BubbleAnimation* animation,
uint32_t* frame_order,
uint8_t width,
uint8_t height) {
uint16_t frame_order_count = animation->passive_frames + animation->active_frames;
/* The frames should go in order (0...N), without omissions */
size_t max_frame_count = 0;
for(int i = 0; i < frame_order_count; ++i) {
max_frame_count = MAX(max_frame_count, frame_order[i]);
}
if((max_frame_count >= frame_order_count) || (max_frame_count >= 256 /* max uint8_t */)) {
return false;
}
Icon* icon = (Icon*)&animation->icon_animation;
FURI_CONST_ASSIGN(icon->frame_count, max_frame_count + 1);
FURI_CONST_ASSIGN(icon->frame_rate, 0);
FURI_CONST_ASSIGN(icon->height, height);
FURI_CONST_ASSIGN(icon->width, width);
icon->frames = malloc(sizeof(const uint8_t*) * icon->frame_count);
bool frames_ok = false;
File* file = storage_file_alloc(storage);
FileInfo file_info;
string_t filename;
string_init(filename);
size_t max_filesize = ROUND_UP_TO(width, 8) * height + 1;
for(int i = 0; i < icon->frame_count; ++i) {
frames_ok = false;
string_printf(filename, ANIMATION_DIR "/%s/frame_%d.bm", name, i);
if(storage_common_stat(storage, string_get_cstr(filename), &file_info) != FSE_OK) break;
if(file_info.size > max_filesize) {
FURI_LOG_E(
TAG,
"Filesize %d, max: %d (width %d, height %d)",
file_info.size,
max_filesize,
width,
height);
break;
}
if(!storage_file_open(file, string_get_cstr(filename), FSAM_READ, FSOM_OPEN_EXISTING)) {
FURI_LOG_E(TAG, "Can't open file \'%s\'", string_get_cstr(filename));
break;
}
FURI_CONST_ASSIGN_PTR(icon->frames[i], malloc(file_info.size));
if(storage_file_read(file, (void*)icon->frames[i], file_info.size) != file_info.size) {
FURI_LOG_E(TAG, "Read failed: \'%s\'", string_get_cstr(filename));
break;
}
storage_file_close(file);
frames_ok = true;
}
if(!frames_ok) {
FURI_LOG_E(
TAG,
"Load \'%s\' failed, %dx%d, size: %d",
string_get_cstr(filename),
width,
height,
file_info.size);
animation_storage_free_frames(animation);
} else {
furi_check(animation->icon_animation.frames);
for(int i = 0; i < animation->icon_animation.frame_count; ++i) {
furi_check(animation->icon_animation.frames[i]);
}
}
storage_file_free(file);
string_clear(filename);
return frames_ok;
}
static bool animation_storage_load_bubbles(BubbleAnimation* animation, FlipperFormat* ff) {
uint32_t u32value;
string_t str;
string_init(str);
bool success = false;
furi_assert(!animation->frame_bubble_sequences);
do {
if(!flipper_format_read_uint32(ff, "Bubble slots", &u32value, 1)) break;
if(u32value > 20) break;
animation->frame_bubble_sequences_count = u32value;
if(animation->frame_bubble_sequences_count == 0) {
animation->frame_bubble_sequences = NULL;
success = true;
break;
}
animation->frame_bubble_sequences =
malloc(sizeof(FrameBubble*) * animation->frame_bubble_sequences_count);
int32_t current_slot = 0;
for(int i = 0; i < animation->frame_bubble_sequences_count; ++i) {
FURI_CONST_ASSIGN_PTR(
animation->frame_bubble_sequences[i], malloc(sizeof(FrameBubble)));
}
const FrameBubble* bubble = animation->frame_bubble_sequences[0];
int8_t index = -1;
for(;;) {
if(!flipper_format_read_int32(ff, "Slot", &current_slot, 1)) break;
if((current_slot != 0) && (index == -1)) break;
if(current_slot == index) {
FURI_CONST_ASSIGN_PTR(bubble->next_bubble, malloc(sizeof(FrameBubble)));
bubble = bubble->next_bubble;
} else if(current_slot == index + 1) {
++index;
bubble = animation->frame_bubble_sequences[index];
} else {
/* slots have to start from 0, be ascending sorted, and
* have exact number of slots as specified in "Bubble slots" */
break;
}
if(index >= animation->frame_bubble_sequences_count) break;
if(!flipper_format_read_uint32(ff, "X", &u32value, 1)) break;
FURI_CONST_ASSIGN(bubble->bubble.x, u32value);
if(!flipper_format_read_uint32(ff, "Y", &u32value, 1)) break;
FURI_CONST_ASSIGN(bubble->bubble.y, u32value);
if(!flipper_format_read_string(ff, "Text", str)) break;
if(string_size(str) > 100) break;
string_replace_all_str(str, "\\n", "\n");
FURI_CONST_ASSIGN_PTR(bubble->bubble.text, malloc(string_size(str) + 1));
strcpy((char*)bubble->bubble.text, string_get_cstr(str));
if(!flipper_format_read_string(ff, "AlignH", str)) break;
if(!animation_storage_cast_align(str, (Align*)&bubble->bubble.align_h)) break;
if(!flipper_format_read_string(ff, "AlignV", str)) break;
if(!animation_storage_cast_align(str, (Align*)&bubble->bubble.align_v)) break;
if(!flipper_format_read_uint32(ff, "StartFrame", &u32value, 1)) break;
FURI_CONST_ASSIGN(bubble->start_frame, u32value);
if(!flipper_format_read_uint32(ff, "EndFrame", &u32value, 1)) break;
FURI_CONST_ASSIGN(bubble->end_frame, u32value);
}
success = (index + 1) == animation->frame_bubble_sequences_count;
} while(0);
if(!success) {
if(animation->frame_bubble_sequences) {
FURI_LOG_E(TAG, "Failed to load animation bubbles");
animation_storage_free_bubbles(animation);
}
}
string_clear(str);
return success;
}
static BubbleAnimation* animation_storage_load_animation(const char* name) {
furi_assert(name);
BubbleAnimation* animation = malloc(sizeof(BubbleAnimation));
uint32_t height = 0;
uint32_t width = 0;
uint32_t* u32array = NULL;
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* ff = flipper_format_file_alloc(storage);
/* Forbid skipping fields */
flipper_format_set_strict_mode(ff, true);
string_t str;
string_init(str);
animation->frame_bubble_sequences = NULL;
bool success = false;
do {
uint32_t u32value;
if(FSE_OK != storage_sd_status(storage)) break;
string_printf(str, ANIMATION_DIR "/%s/" ANIMATION_META_FILE, name);
if(!flipper_format_file_open_existing(ff, string_get_cstr(str))) break;
if(!flipper_format_read_header(ff, str, &u32value)) break;
if(string_cmp_str(str, "Flipper Animation")) break;
if(!flipper_format_read_uint32(ff, "Width", &width, 1)) break;
if(!flipper_format_read_uint32(ff, "Height", &height, 1)) break;
if(!flipper_format_read_uint32(ff, "Passive frames", &u32value, 1)) break;
animation->passive_frames = u32value;
if(!flipper_format_read_uint32(ff, "Active frames", &u32value, 1)) break;
animation->active_frames = u32value;
uint8_t frames = animation->passive_frames + animation->active_frames;
uint32_t count = 0;
if(!flipper_format_get_value_count(ff, "Frames order", &count)) break;
if(count != frames) {
FURI_LOG_E(TAG, "Error loading animation: frames order");
break;
}
u32array = malloc(sizeof(uint32_t) * frames);
if(!flipper_format_read_uint32(ff, "Frames order", u32array, frames)) break;
animation->frame_order = malloc(sizeof(uint8_t) * frames);
for(int i = 0; i < frames; ++i) {
FURI_CONST_ASSIGN(animation->frame_order[i], u32array[i]);
}
/* passive and active frames must be loaded up to this point */
if(!animation_storage_load_frames(storage, name, animation, u32array, width, height))
break;
if(!flipper_format_read_uint32(ff, "Active cycles", &u32value, 1)) break;
animation->active_cycles = u32value;
if(!flipper_format_read_uint32(ff, "Frame rate", &u32value, 1)) break;
FURI_CONST_ASSIGN(animation->icon_animation.frame_rate, u32value);
if(!flipper_format_read_uint32(ff, "Duration", &u32value, 1)) break;
animation->duration = u32value;
if(!flipper_format_read_uint32(ff, "Active cooldown", &u32value, 1)) break;
animation->active_cooldown = u32value;
if(!animation_storage_load_bubbles(animation, ff)) break;
success = true;
} while(0);
string_clear(str);
flipper_format_free(ff);
if(u32array) {
free(u32array);
}
if(!success) {
if(animation->frame_order) {
free((void*)animation->frame_order);
}
free(animation);
animation = NULL;
}
return animation;
}
static void animation_storage_free_bubbles(BubbleAnimation* animation) {
if(!animation->frame_bubble_sequences) return;
for(int i = 0; i < animation->frame_bubble_sequences_count;) {
const FrameBubble* const* bubble = &animation->frame_bubble_sequences[i];
if((*bubble) == NULL) break;
while((*bubble)->next_bubble != NULL) {
bubble = &(*bubble)->next_bubble;
}
if((*bubble)->bubble.text) {
free((void*)(*bubble)->bubble.text);
}
if((*bubble) == animation->frame_bubble_sequences[i]) {
++i;
}
free((void*)*bubble);
FURI_CONST_ASSIGN_PTR(*bubble, NULL);
}
free((void*)animation->frame_bubble_sequences);
animation->frame_bubble_sequences = NULL;
}

View File

@@ -0,0 +1,89 @@
#pragma once
#include <stdint.h>
#include <m-list.h>
#include "views/bubble_animation_view.h"
#include <m-string.h>
/** Main structure to handle animation data.
* Contains all, including animation playing data (BubbleAnimation),
* data for random animation selection (StorageAnimationMeta) and
* flag of location internal/external */
typedef struct StorageAnimation StorageAnimation;
typedef struct {
const char* name;
uint8_t min_butthurt;
uint8_t max_butthurt;
uint8_t min_level;
uint8_t max_level;
uint8_t weight;
} StorageAnimationManifestInfo;
/** Container to return available animations list */
LIST_DEF(StorageAnimationList, StorageAnimation*, M_PTR_OPLIST)
#define M_OPL_StorageAnimationList_t() LIST_OPLIST(StorageAnimationList)
/**
* Fill list of available animations.
* List will contain all idle animations on inner flash
* and all available on SD-card, mentioned in manifest.txt.
* Performs caching of animation. If fail - falls back to
* inner animation.
* List has to be initialized.
*
* @list list to fill with animations data
*/
void animation_storage_fill_animation_list(StorageAnimationList_t* list);
/**
* Get bubble animation of storage animation.
* Bubble Animation is a structure which describes animation
* independent of it's place of storage and meta data.
* It contain all what is need to be played.
* If storage_animation is not cached - caches it.
*
* @storage_animation animation from which extract bubble animation
* @return bubble_animation, NULL if failed to cache data.
*/
const BubbleAnimation* animation_storage_get_bubble_animation(StorageAnimation* storage_animation);
/**
* Performs caching animation data (Bubble Animation)
* if this is not done yet.
*
* @storage_animation animation to cache
*/
void animation_storage_cache_animation(StorageAnimation* storage_animation);
/**
* Find animation by name.
* Search through the inner flash, and SD-card if has.
*
* @name name of animation
* @return found animation. NULL if nothing found.
*/
StorageAnimation* animation_storage_find_animation(const char* name);
/**
* Get meta information of storage animation.
* This information allows to randomly select animation.
* Also it contains name. Never returns NULL.
*
* @storage_animation item of whom we have to extract meta.
* @return meta itself
*/
StorageAnimationManifestInfo* animation_storage_get_meta(StorageAnimation* storage_animation);
/**
* Free storage_animation, which previously acquired
* by Animation Storage.
*
* @storage_animation item to free. NULL-ed after all.
*/
void animation_storage_free_storage_animation(StorageAnimation** storage_animation);
/**
* Has to be called at least 1 time to initialize runtime structures
* of animations in inner flash.
*/
void animation_storage_initialize_internal_animations(void);

View File

@@ -0,0 +1,9 @@
#pragma once
#include "animation_storage.h"
#include "animation_manager.h"
struct StorageAnimation {
const BubbleAnimation* animation;
bool external;
StorageAnimationManifestInfo manifest_info;
};

View File

@@ -0,0 +1,409 @@
#include "../animation_manager.h"
#include "../animation_storage.h"
#include "bubble_animation_view.h"
#include <furi_hal.h>
#include <furi.h>
#include <gui/canvas.h>
#include <gui/elements.h>
#include <gui/view.h>
#include <gui/icon_i.h>
#include <input/input.h>
#include <stdint.h>
#include <core/dangerous_defines.h>
#define ACTIVE_SHIFT 2
typedef struct {
const BubbleAnimation* current;
const FrameBubble* current_bubble;
uint8_t current_frame;
uint8_t active_cycle;
uint8_t active_bubbles;
uint8_t passive_bubbles;
uint8_t active_shift;
TickType_t active_ended_at;
Icon* freeze_frame;
} BubbleAnimationViewModel;
struct BubbleAnimationView {
View* view;
FuriTimer* timer;
BubbleAnimationInteractCallback interact_callback;
void* interact_callback_context;
};
static void bubble_animation_activate(BubbleAnimationView* view, bool force);
static void bubble_animation_activate_right_now(BubbleAnimationView* view);
static uint8_t bubble_animation_get_frame_index(BubbleAnimationViewModel* model) {
furi_assert(model);
uint8_t icon_index = 0;
const BubbleAnimation* animation = model->current;
if(model->current_frame < animation->passive_frames) {
icon_index = model->current_frame;
} else {
icon_index =
(model->current_frame - animation->passive_frames) % animation->active_frames +
animation->passive_frames;
}
furi_assert(icon_index < (animation->passive_frames + animation->active_frames));
return animation->frame_order[icon_index];
}
static void bubble_animation_draw_callback(Canvas* canvas, void* model_) {
furi_assert(model_);
furi_assert(canvas);
BubbleAnimationViewModel* model = model_;
const BubbleAnimation* animation = model->current;
if(model->freeze_frame) {
uint8_t y_offset = canvas_height(canvas) - icon_get_height(model->freeze_frame);
canvas_draw_icon(canvas, 0, y_offset, model->freeze_frame);
return;
}
if(!animation) {
return;
}
furi_assert(model->current_frame < 255);
uint8_t index = bubble_animation_get_frame_index(model);
uint8_t width = icon_get_width(&animation->icon_animation);
uint8_t height = icon_get_height(&animation->icon_animation);
uint8_t y_offset = canvas_height(canvas) - height;
canvas_draw_bitmap(
canvas, 0, y_offset, width, height, animation->icon_animation.frames[index]);
const FrameBubble* bubble = model->current_bubble;
if(bubble) {
if((model->current_frame >= bubble->start_frame) &&
(model->current_frame <= bubble->end_frame)) {
const Bubble* b = &bubble->bubble;
elements_bubble_str(canvas, b->x, b->y, b->text, b->align_h, b->align_v);
}
}
}
static const FrameBubble*
bubble_animation_pick_bubble(BubbleAnimationViewModel* model, bool active) {
const FrameBubble* bubble = NULL;
if((model->active_bubbles == 0) && (model->passive_bubbles == 0)) {
return NULL;
}
uint8_t index =
furi_hal_random_get() % (active ? model->active_bubbles : model->passive_bubbles);
const BubbleAnimation* animation = model->current;
for(int i = 0; i < animation->frame_bubble_sequences_count; ++i) {
if((animation->frame_bubble_sequences[i]->start_frame < animation->passive_frames) ^
active) {
if(!index) {
bubble = animation->frame_bubble_sequences[i];
}
--index;
}
}
return bubble;
}
static bool bubble_animation_input_callback(InputEvent* event, void* context) {
furi_assert(context);
furi_assert(event);
BubbleAnimationView* animation_view = context;
bool consumed = false;
if(event->type == InputTypePress) {
bubble_animation_activate(animation_view, false);
}
if(event->key == InputKeyRight) {
/* Right button reserved for animation activation, so consume */
consumed = true;
if(event->type == InputTypeShort) {
if(animation_view->interact_callback) {
animation_view->interact_callback(animation_view->interact_callback_context);
}
}
}
return consumed;
}
static void bubble_animation_activate(BubbleAnimationView* view, bool force) {
furi_assert(view);
bool activate = true;
BubbleAnimationViewModel* model = view_get_model(view->view);
if(model->current == NULL) {
activate = false;
} else if(model->freeze_frame) {
activate = false;
} else if(model->current->active_frames == 0) {
activate = false;
}
if(model->current != NULL) {
if(!force) {
if((model->active_ended_at + model->current->active_cooldown * 1000) >
xTaskGetTickCount()) {
activate = false;
} else if(model->active_shift) {
activate = false;
} else if(model->current_frame >= model->current->passive_frames) {
activate = false;
}
}
}
view_commit_model(view->view, false);
if(!activate && !force) {
return;
}
if(ACTIVE_SHIFT > 0) {
BubbleAnimationViewModel* model = view_get_model(view->view);
model->active_shift = ACTIVE_SHIFT;
view_commit_model(view->view, false);
} else {
bubble_animation_activate_right_now(view);
}
}
static void bubble_animation_activate_right_now(BubbleAnimationView* view) {
furi_assert(view);
uint8_t frame_rate = 0;
BubbleAnimationViewModel* model = view_get_model(view->view);
if(model->current && (model->current->active_frames > 0) && (!model->freeze_frame)) {
model->current_frame = model->current->passive_frames;
model->current_bubble = bubble_animation_pick_bubble(model, true);
frame_rate = model->current->icon_animation.frame_rate;
}
view_commit_model(view->view, true);
if(frame_rate) {
furi_timer_start(view->timer, 1000 / frame_rate);
}
}
static void bubble_animation_next_frame(BubbleAnimationViewModel* model) {
furi_assert(model);
if(!model->current) {
return;
}
if(model->current_frame < model->current->passive_frames) {
model->current_frame = (model->current_frame + 1) % model->current->passive_frames;
} else {
++model->current_frame;
model->active_cycle +=
!((model->current_frame - model->current->passive_frames) %
model->current->active_frames);
if(model->active_cycle >= model->current->active_cycles) {
// switch to passive
model->active_cycle = 0;
model->current_frame = 0;
model->current_bubble = bubble_animation_pick_bubble(model, false);
model->active_ended_at = xTaskGetTickCount();
}
if(model->current_bubble) {
if(model->current_frame > model->current_bubble->end_frame) {
model->current_bubble = model->current_bubble->next_bubble;
}
}
}
}
static void bubble_animation_timer_callback(void* context) {
furi_assert(context);
BubbleAnimationView* view = context;
bool activate = false;
BubbleAnimationViewModel* model = view_get_model(view->view);
if(model->active_shift > 0) {
activate = (--model->active_shift == 0);
}
if(!model->freeze_frame && !activate) {
bubble_animation_next_frame(model);
}
view_commit_model(view->view, !activate);
if(activate) {
bubble_animation_activate_right_now(view);
}
}
/* always freeze first passive frame, because
* animation is always activated at unfreezing and played
* passive frame first, and 2 frames after - active
*/
static Icon* bubble_animation_clone_first_frame(const Icon* icon_orig) {
furi_assert(icon_orig);
furi_assert(icon_orig->frames);
furi_assert(icon_orig->frames[0]);
Icon* icon_clone = malloc(sizeof(Icon));
memcpy(icon_clone, icon_orig, sizeof(Icon));
icon_clone->frames = malloc(sizeof(uint8_t*));
/* icon bitmap can be either compressed or not. It is compressed if
* compressed size is less than original, so max size for bitmap is
* uncompressed (width * height) + 1 byte (in uncompressed case)
* for compressed header
*/
size_t max_bitmap_size = ROUND_UP_TO(icon_orig->width, 8) * icon_orig->height + 1;
FURI_CONST_ASSIGN_PTR(icon_clone->frames[0], malloc(max_bitmap_size));
memcpy((void*)icon_clone->frames[0], icon_orig->frames[0], max_bitmap_size);
FURI_CONST_ASSIGN(icon_clone->frame_count, 1);
return icon_clone;
}
static void bubble_animation_release_frame(Icon** icon) {
furi_assert(icon);
furi_assert(*icon);
free((void*)(*icon)->frames[0]);
free((void*)(*icon)->frames);
free(*icon);
*icon = NULL;
}
static void bubble_animation_enter(void* context) {
furi_assert(context);
BubbleAnimationView* view = context;
bubble_animation_activate(view, false);
BubbleAnimationViewModel* model = view_get_model(view->view);
uint8_t frame_rate = 0;
if(model->current != NULL) {
frame_rate = model->current->icon_animation.frame_rate;
}
view_commit_model(view->view, false);
if(frame_rate) {
furi_timer_start(view->timer, 1000 / frame_rate);
}
}
static void bubble_animation_exit(void* context) {
furi_assert(context);
BubbleAnimationView* view = context;
furi_timer_stop(view->timer);
}
BubbleAnimationView* bubble_animation_view_alloc(void) {
BubbleAnimationView* view = malloc(sizeof(BubbleAnimationView));
view->view = view_alloc();
view->interact_callback = NULL;
view->timer = furi_timer_alloc(bubble_animation_timer_callback, FuriTimerTypePeriodic, view);
view_allocate_model(view->view, ViewModelTypeLocking, sizeof(BubbleAnimationViewModel));
view_set_context(view->view, view);
view_set_draw_callback(view->view, bubble_animation_draw_callback);
view_set_input_callback(view->view, bubble_animation_input_callback);
view_set_enter_callback(view->view, bubble_animation_enter);
view_set_exit_callback(view->view, bubble_animation_exit);
return view;
}
void bubble_animation_view_free(BubbleAnimationView* view) {
furi_assert(view);
view_set_draw_callback(view->view, NULL);
view_set_input_callback(view->view, NULL);
view_set_context(view->view, NULL);
view_free(view->view);
view->view = NULL;
free(view);
}
void bubble_animation_view_set_interact_callback(
BubbleAnimationView* view,
BubbleAnimationInteractCallback callback,
void* context) {
furi_assert(view);
view->interact_callback_context = context;
view->interact_callback = callback;
}
void bubble_animation_view_set_animation(
BubbleAnimationView* view,
const BubbleAnimation* new_animation) {
furi_assert(view);
furi_assert(new_animation);
BubbleAnimationViewModel* model = view_get_model(view->view);
furi_assert(model);
model->current = new_animation;
model->active_ended_at = xTaskGetTickCount() - (model->current->active_cooldown * 1000);
model->active_bubbles = 0;
model->passive_bubbles = 0;
for(int i = 0; i < new_animation->frame_bubble_sequences_count; ++i) {
if(new_animation->frame_bubble_sequences[i]->start_frame < new_animation->passive_frames) {
++model->passive_bubbles;
} else {
++model->active_bubbles;
}
}
/* select bubble sequence */
model->current_bubble = bubble_animation_pick_bubble(model, false);
model->current_frame = 0;
model->active_cycle = 0;
view_commit_model(view->view, true);
furi_timer_start(view->timer, 1000 / new_animation->icon_animation.frame_rate);
}
void bubble_animation_freeze(BubbleAnimationView* view) {
furi_assert(view);
BubbleAnimationViewModel* model = view_get_model(view->view);
furi_assert(model->current);
furi_assert(!model->freeze_frame);
model->freeze_frame = bubble_animation_clone_first_frame(&model->current->icon_animation);
model->current = NULL;
view_commit_model(view->view, false);
furi_timer_stop(view->timer);
}
void bubble_animation_unfreeze(BubbleAnimationView* view) {
furi_assert(view);
uint8_t frame_rate;
BubbleAnimationViewModel* model = view_get_model(view->view);
furi_assert(model->freeze_frame);
bubble_animation_release_frame(&model->freeze_frame);
furi_assert(model->current);
frame_rate = model->current->icon_animation.frame_rate;
view_commit_model(view->view, true);
furi_timer_start(view->timer, 1000 / frame_rate);
bubble_animation_activate(view, false);
}
View* bubble_animation_get_view(BubbleAnimationView* view) {
furi_assert(view);
return view->view;
}

View File

@@ -0,0 +1,89 @@
#pragma once
#include <gui/view.h>
#include "../animation_manager.h"
/** Bubble Animation instance */
typedef struct BubbleAnimationView BubbleAnimationView;
/** Callback type to be called when interact button pressed */
typedef void (*BubbleAnimationInteractCallback)(void*);
/**
* Allocate bubble animation view.
* This is animation with bubbles, and 2 phases:
* active and passive.
*
* @return instance of new bubble animation
*/
BubbleAnimationView* bubble_animation_view_alloc(void);
/**
* Free bubble animation view.
*
* @view bubble animation view instance
*/
void bubble_animation_view_free(BubbleAnimationView* view);
/**
* Set callback for interact action for animation.
* Currently this is right button.
*
* @view bubble animation view instance
* @callback callback to call when button pressed
* @context context
*/
void bubble_animation_view_set_interact_callback(
BubbleAnimationView* view,
BubbleAnimationInteractCallback callback,
void* context);
/**
* Set new animation.
* BubbleAnimation doesn't posses Bubble Animation object
* so it doesn't handle any memory manipulation on Bubble Animations.
*
* @view bubble animation view instance
* @new_bubble_animation new animation to set
*/
void bubble_animation_view_set_animation(
BubbleAnimationView* view,
const BubbleAnimation* new_bubble_animation);
/**
* Get view of bubble animation.
*
* @view bubble animation view instance
* @return view
*/
View* bubble_animation_get_view(BubbleAnimationView* view);
/**
* Freeze current playing animation. Saves a frame to be shown
* during next unfreeze called.
* bubble_animation_freeze() stops any reference to 'current' animation
* so it can be freed. Therefore lock unfreeze should be preceeded with
* new animation set.
*
* Freeze/Unfreeze usage example:
*
* animation_view_alloc()
* set_animation()
* ...
* freeze_animation()
* // release animation
* ...
* // allocate animation
* set_animation()
* unfreeze()
*
* @view bubble animation view instance
*/
void bubble_animation_freeze(BubbleAnimationView* view);
/**
* Starts bubble animation after freezing.
*
* @view bubble animation view instance
*/
void bubble_animation_unfreeze(BubbleAnimationView* view);

View File

@@ -0,0 +1,130 @@
#include "one_shot_animation_view.h"
#include <furi.h>
#include <portmacro.h>
#include <gui/canvas.h>
#include <gui/view.h>
#include <gui/icon_i.h>
#include <stdint.h>
typedef void (*OneShotInteractCallback)(void*);
struct OneShotView {
View* view;
TimerHandle_t update_timer;
OneShotInteractCallback interact_callback;
void* interact_callback_context;
};
typedef struct {
const Icon* icon;
uint32_t index;
bool block_input;
} OneShotViewModel;
static void one_shot_view_update_timer_callback(TimerHandle_t xTimer) {
OneShotView* view = (void*)pvTimerGetTimerID(xTimer);
OneShotViewModel* model = view_get_model(view->view);
if((model->index + 1) < model->icon->frame_count) {
++model->index;
} else {
model->block_input = false;
model->index = model->icon->frame_count - 2;
}
view_commit_model(view->view, true);
}
static void one_shot_view_draw(Canvas* canvas, void* model_) {
furi_assert(canvas);
furi_assert(model_);
OneShotViewModel* model = model_;
furi_check(model->index < model->icon->frame_count);
uint8_t y_offset = canvas_height(canvas) - model->icon->height;
canvas_draw_bitmap(
canvas,
0,
y_offset,
model->icon->width,
model->icon->height,
model->icon->frames[model->index]);
}
static bool one_shot_view_input(InputEvent* event, void* context) {
furi_assert(context);
furi_assert(event);
OneShotView* view = context;
bool consumed = false;
OneShotViewModel* model = view_get_model(view->view);
consumed = model->block_input;
view_commit_model(view->view, false);
if(!consumed) {
if(event->key == InputKeyRight) {
/* Right button reserved for animation activation, so consume */
consumed = true;
if(event->type == InputTypeShort) {
if(view->interact_callback) {
view->interact_callback(view->interact_callback_context);
}
}
}
}
return consumed;
}
OneShotView* one_shot_view_alloc(void) {
OneShotView* view = malloc(sizeof(OneShotView));
view->view = view_alloc();
view->update_timer =
xTimerCreate(NULL, 1000, pdTRUE, view, one_shot_view_update_timer_callback);
view_allocate_model(view->view, ViewModelTypeLocking, sizeof(OneShotViewModel));
view_set_context(view->view, view);
view_set_draw_callback(view->view, one_shot_view_draw);
view_set_input_callback(view->view, one_shot_view_input);
return view;
}
void one_shot_view_free(OneShotView* view) {
furi_assert(view);
xTimerDelete(view->update_timer, portMAX_DELAY);
view_free(view->view);
view->view = NULL;
free(view);
}
void one_shot_view_set_interact_callback(
OneShotView* view,
OneShotInteractCallback callback,
void* context) {
furi_assert(view);
view->interact_callback_context = context;
view->interact_callback = callback;
}
void one_shot_view_start_animation(OneShotView* view, const Icon* icon) {
furi_assert(view);
furi_assert(icon);
furi_check(icon->frame_count >= 2);
OneShotViewModel* model = view_get_model(view->view);
model->index = 0;
model->icon = icon;
model->block_input = true;
view_commit_model(view->view, true);
xTimerChangePeriod(view->update_timer, 1000 / model->icon->frame_rate, portMAX_DELAY);
}
View* one_shot_view_get_view(OneShotView* view) {
furi_assert(view);
return view->view;
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <furi.h>
#include <gui/view.h>
#include <stdint.h>
typedef void (*OneShotInteractCallback)(void*);
typedef struct OneShotView OneShotView;
OneShotView* one_shot_view_alloc(void);
void one_shot_view_free(OneShotView* view);
void one_shot_view_set_interact_callback(
OneShotView* view,
OneShotInteractCallback callback,
void* context);
void one_shot_view_start_animation(OneShotView* view, const Icon* icon);
View* one_shot_view_get_view(OneShotView* view);