timed-remote

Flipper Zero app for sending delayed IR commands
git clone git://src.adamsgaard.dk/timed-remote # fast
git clone https://src.adamsgaard.dk/timed-remote.git # slow
Log | Files | Refs | README | LICENSE Back to index

commit e6560e7f271134addcbadcf3c326feec4b76b0d8
parent 3609fbfe951db5e1fac604ce4e0570bb7b7a926e
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date:   Tue, 17 Feb 2026 21:13:58 +0100

refactor: simplify code structure and style

Diffstat:
MMakefile | 15+++++++++++++--
Mhelpers/ir_helper.c | 376++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mhelpers/ir_helper.h | 72++++++++++++++++++------------------------------------------------------
Mhelpers/time_helper.c | 75++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mhelpers/time_helper.h | 46++++------------------------------------------
Mscenes/scene_confirm.c | 66++++++++++++++++++++++++++++++------------------------------------
Mscenes/scene_ir_browse.c | 104++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mscenes/scene_ir_select.c | 123++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mscenes/scene_timer_config.c | 389+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mscenes/scene_timer_running.c | 238+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mscenes/timed_remote_scene.c | 49+++++++++++++++++++++++--------------------------
Mscenes/timed_remote_scene.h | 92+++++++++++++++++++++++++++++++------------------------------------------------
Mtimed_remote.c | 173+++++++++++++++++++++++++++++++++++++------------------------------------------
Mtimed_remote.h | 78++++++++++++++++++++++++++++++++----------------------------------------------
14 files changed, 916 insertions(+), 980 deletions(-)

diff --git a/Makefile b/Makefile @@ -7,6 +7,18 @@ BIN = dist/timed_remote.fap SRC = \ application.fam \ timed_remote.c \ + timed_remote.h \ + helpers/ir_helper.c \ + helpers/ir_helper.h \ + helpers/time_helper.c \ + helpers/time_helper.h \ + scenes/timed_remote_scene.c \ + scenes/timed_remote_scene.h \ + scenes/scene_ir_browse.c \ + scenes/scene_ir_select.c \ + scenes/scene_timer_config.c \ + scenes/scene_timer_running.c \ + scenes/scene_confirm.c \ dist/timed_remote.fap: ${BUILDER} ${SRC} ${PYENV} ${BUILDER} @@ -25,4 +37,4 @@ start: ${BIN} clean: rm -rf dist -.PHONY: start clean -\ No newline at end of file +.PHONY: start clean diff --git a/helpers/ir_helper.c b/helpers/ir_helper.c @@ -6,175 +6,237 @@ #include <lib/infrared/signal/infrared_error_code.h> #include <storage/storage.h> -#define IR_FILE_HEADER "IR signals file" -#define IR_FILE_VERSION 1 - -/* ========== Signal List ========== */ +IrSignalList * +ir_list_alloc(void) +{ + IrSignalList *list = malloc(sizeof(IrSignalList)); + if (!list) + return NULL; + list->items = NULL; + list->count = 0; + list->capacity = 0; + return list; +} -IrSignalList *ir_signal_list_alloc(void) { - IrSignalList *list = malloc(sizeof(IrSignalList)); - list->items = NULL; - list->count = 0; - list->capacity = 0; - return list; +void +ir_list_free(IrSignalList *list) +{ + if (!list) + return; + + for (size_t i = 0; i < list->count; i++) + { + if (list->items[i].signal) + { + infrared_signal_free(list->items[i].signal); + } + if (list->items[i].name) + { + furi_string_free(list->items[i].name); + } + } + if (list->items) + { + free(list->items); + } + free(list); } -void ir_signal_list_free(IrSignalList *list) { - if (!list) - return; - - for (size_t i = 0; i < list->count; i++) { - if (list->items[i].signal) { - infrared_signal_free(list->items[i].signal); - } - if (list->items[i].name) { - furi_string_free(list->items[i].name); - } - } - if (list->items) { - free(list->items); - } - free(list); +static bool +add_sig(IrSignalList *list, InfraredSignal *signal, const char *name) +{ + if (list->count >= list->capacity) + { + size_t new_capacity = list->capacity == 0 ? 8 : list->capacity * 2; + IrSignalItem *items = realloc(list->items, new_capacity * sizeof(IrSignalItem)); + if (!items) + return false; + list->items = items; + list->capacity = new_capacity; + } + + list->items[list->count].signal = signal; + list->items[list->count].name = furi_string_alloc_set(name); + if (!list->items[list->count].name) + return false; + list->count++; + return true; } -static void ir_signal_list_add(IrSignalList *list, InfraredSignal *signal, - const char *name) { - if (list->count >= list->capacity) { - size_t new_capacity = list->capacity == 0 ? 8 : list->capacity * 2; - list->items = realloc(list->items, new_capacity * sizeof(IrSignalItem)); - list->capacity = new_capacity; - } - - list->items[list->count].signal = signal; - list->items[list->count].name = furi_string_alloc_set(name); - list->count++; +bool +ir_load(const char *path, IrSignalList *list) +{ + if (!path || !list) + return false; + + Storage *storage = furi_record_open(RECORD_STORAGE); + if (!storage) + return false; + + FlipperFormat *ff = flipper_format_file_alloc(storage); + if (!ff) + { + furi_record_close(RECORD_STORAGE); + return false; + } + + bool success = false; + FuriString *filetype = NULL; + FuriString *sig = NULL; + + do + { + if (!flipper_format_file_open_existing(ff, path)) + break; + + filetype = furi_string_alloc(); + if (!filetype) + break; + + uint32_t version; + bool header_ok = flipper_format_read_header(ff, filetype, &version); + if (!header_ok) + break; + + sig = furi_string_alloc(); + if (!sig) + break; + + while (true) + { + InfraredSignal *signal = infrared_signal_alloc(); + if (!signal) + break; + + InfraredErrorCode err = infrared_signal_read(signal, ff, sig); + if (err == InfraredErrorCodeNone) + { + if (!add_sig(list, signal, furi_string_get_cstr(sig))) + { + infrared_signal_free(signal); + break; + } + } else + { + infrared_signal_free(signal); + break; + } + } + success = true; + } while (false); + + if (filetype) + furi_string_free(filetype); + if (sig) + furi_string_free(sig); + flipper_format_free(ff); + furi_record_close(RECORD_STORAGE); + return success; } -/* ========== File I/O ========== */ - -bool ir_helper_load_file(const char *path, IrSignalList *list) { - Storage *storage = furi_record_open(RECORD_STORAGE); - FlipperFormat *ff = flipper_format_file_alloc(storage); - bool success = false; - - do { - if (!flipper_format_file_open_existing(ff, path)) - break; - - /* Verify header */ - FuriString *filetype = furi_string_alloc(); - uint32_t version; - bool header_ok = flipper_format_read_header(ff, filetype, &version); - furi_string_free(filetype); - if (!header_ok) - break; - - /* Read all signals */ - FuriString *signal_name = furi_string_alloc(); - while (true) { - InfraredSignal *signal = infrared_signal_alloc(); - InfraredErrorCode err = infrared_signal_read(signal, ff, signal_name); - if (err == InfraredErrorCodeNone) { - ir_signal_list_add(list, signal, furi_string_get_cstr(signal_name)); - } else { - infrared_signal_free(signal); - break; - } - } - furi_string_free(signal_name); - - success = true; - } while (false); - - flipper_format_free(ff); - furi_record_close(RECORD_STORAGE); - return success; +void +ir_tx(InfraredSignal *signal) +{ + infrared_signal_transmit(signal); } -/* ========== Transmit ========== */ +static bool +is_ir(const FileInfo *file_info, const char *name) +{ + if ((file_info->flags & FSF_DIRECTORY) || !name) + return false; -void ir_helper_transmit(InfraredSignal *signal) { - infrared_signal_transmit(signal); + size_t len = strlen(name); + return len > 3 && strcmp(name + len - 3, ".ir") == 0; } -/* ========== File Browser ========== */ - -bool ir_helper_list_files(const char *dir_path, FuriString ***files, - size_t *count) { - Storage *storage = furi_record_open(RECORD_STORAGE); - File *dir = storage_file_alloc(storage); - - *files = NULL; - *count = 0; - - if (!storage_dir_open(dir, dir_path)) { - storage_file_free(dir); - furi_record_close(RECORD_STORAGE); - return false; - } - - /* First pass: count .ir files */ - FileInfo file_info; - char name_buf[256]; - size_t file_count = 0; - - while (storage_dir_read(dir, &file_info, name_buf, sizeof(name_buf))) { - if (!(file_info.flags & FSF_DIRECTORY)) { - size_t len = strlen(name_buf); - if (len > 3 && strcmp(name_buf + len - 3, ".ir") == 0) { - file_count++; - } - } - } - - if (file_count == 0) { - storage_dir_close(dir); - storage_file_free(dir); - furi_record_close(RECORD_STORAGE); - return true; /* Success, but no files */ - } - - /* Allocate array */ - *files = malloc(file_count * sizeof(FuriString *)); - *count = file_count; - - /* Second pass: collect filenames - close and reopen directory */ - storage_dir_close(dir); - if (!storage_dir_open(dir, dir_path)) { - free(*files); - *files = NULL; - *count = 0; - storage_file_free(dir); - furi_record_close(RECORD_STORAGE); - return false; - } - - size_t idx = 0; - - while (storage_dir_read(dir, &file_info, name_buf, sizeof(name_buf)) && - idx < file_count) { - if (!(file_info.flags & FSF_DIRECTORY)) { - size_t len = strlen(name_buf); - if (len > 3 && strcmp(name_buf + len - 3, ".ir") == 0) { - (*files)[idx] = furi_string_alloc_set(name_buf); - idx++; - } - } - } - - storage_dir_close(dir); - storage_file_free(dir); - furi_record_close(RECORD_STORAGE); - return true; +bool +ir_files(const char *dir_path, FuriString ***files, + size_t *count) +{ + if (!dir_path || !files || !count) + return false; + + *files = NULL; + *count = 0; + + Storage *storage = furi_record_open(RECORD_STORAGE); + if (!storage) + return false; + + File *dir = storage_file_alloc(storage); + if (!dir) + { + furi_record_close(RECORD_STORAGE); + return false; + } + + if (!storage_dir_open(dir, dir_path)) + { + storage_file_free(dir); + furi_record_close(RECORD_STORAGE); + return false; + } + + FileInfo file_info; + char name_buf[256]; + size_t capacity = 0; + + while (storage_dir_read(dir, &file_info, name_buf, sizeof(name_buf))) + { + if (!is_ir(&file_info, name_buf)) + continue; + + if (*count >= capacity) + { + size_t new_capacity = capacity == 0 ? 8 : capacity * 2; + FuriString **next_files = realloc(*files, new_capacity * sizeof(FuriString *)); + if (!next_files) + { + ir_files_free(*files, *count); + *files = NULL; + *count = 0; + storage_dir_close(dir); + storage_file_free(dir); + furi_record_close(RECORD_STORAGE); + return false; + } + *files = next_files; + capacity = new_capacity; + } + + (*files)[*count] = furi_string_alloc_set(name_buf); + if (!(*files)[*count]) + { + ir_files_free(*files, *count); + *files = NULL; + *count = 0; + storage_dir_close(dir); + storage_file_free(dir); + furi_record_close(RECORD_STORAGE); + return false; + } + (*count)++; + } + + storage_dir_close(dir); + storage_file_free(dir); + furi_record_close(RECORD_STORAGE); + return true; } -void ir_helper_free_file_list(FuriString **files, size_t count) { - if (!files) - return; - for (size_t i = 0; i < count; i++) { - if (files[i]) { - furi_string_free(files[i]); - } - } - free(files); +void +ir_files_free(FuriString **files, size_t count) +{ + if (!files) + return; + + for (size_t i = 0; i < count; i++) + { + if (files[i]) + { + furi_string_free(files[i]); + } + } + free(files); } diff --git a/helpers/ir_helper.h b/helpers/ir_helper.h @@ -1,64 +1,28 @@ #pragma once +#include <stdbool.h> +#include <stddef.h> + #include <furi.h> #include <infrared.h> #include <lib/infrared/signal/infrared_signal.h> -/** - * IR signal container with name. - */ -typedef struct { - InfraredSignal *signal; - FuriString *name; +typedef struct +{ + InfraredSignal *signal; + FuriString *name; } IrSignalItem; -/** - * List of signals parsed from an .ir file. - */ -typedef struct { - IrSignalItem *items; - size_t count; - size_t capacity; +typedef struct +{ + IrSignalItem *items; + size_t count; + size_t capacity; } IrSignalList; -/** - * Allocate an IR signal list. - */ -IrSignalList *ir_signal_list_alloc(void); - -/** - * Free an IR signal list and all contained signals. - */ -void ir_signal_list_free(IrSignalList *list); - -/** - * Load all signals from an .ir file. - * - * @param path File path to load - * @param list Output list of signals - * @return true on success - */ -bool ir_helper_load_file(const char *path, IrSignalList *list); - -/** - * Transmit an IR signal. - * - * @param signal The signal to transmit - */ -void ir_helper_transmit(InfraredSignal *signal); - -/** - * List .ir files in a directory. - * - * @param dir_path Directory path (e.g., "/ext/infrared") - * @param files Output: array of FuriString* (caller frees) - * @param count Output: number of files - * @return true on success - */ -bool ir_helper_list_files(const char *dir_path, FuriString ***files, - size_t *count); - -/** - * Free a list of file paths. - */ -void ir_helper_free_file_list(FuriString **files, size_t count); +IrSignalList *ir_list_alloc(void); +void ir_list_free(IrSignalList *list); +bool ir_load(const char *path, IrSignalList *list); +void ir_tx(InfraredSignal *signal); +bool ir_files(const char *dir_path, FuriString ***files, size_t *count); +void ir_files_free(FuriString **files, size_t count); diff --git a/helpers/time_helper.c b/helpers/time_helper.c @@ -5,46 +5,55 @@ #include <stdio.h> #endif -uint32_t time_helper_hms_to_seconds(uint8_t h, uint8_t m, uint8_t s) { - return (uint32_t)h * 3600 + (uint32_t)m * 60 + (uint32_t)s; +uint32_t +time_hms_sec(uint8_t h, uint8_t m, uint8_t s) +{ + return (uint32_t) h * 3600 + (uint32_t) m * 60 + (uint32_t) s; } -void time_helper_seconds_to_hms(uint32_t total_seconds, uint8_t *h, uint8_t *m, - uint8_t *s) { - /* Handle times >= 24 hours by wrapping */ - total_seconds = total_seconds % (24 * 3600); - - *h = (uint8_t)(total_seconds / 3600); - total_seconds %= 3600; - *m = (uint8_t)(total_seconds / 60); - *s = (uint8_t)(total_seconds % 60); +void +time_sec_hms(uint32_t total_seconds, uint8_t *h, uint8_t *m, + uint8_t *s) +{ + /* Handle times >= 24 hours by wrapping */ + total_seconds = total_seconds % (24 * 3600); + + *h = (uint8_t) (total_seconds / 3600); + total_seconds %= 3600; + *m = (uint8_t) (total_seconds / 60); + *s = (uint8_t) (total_seconds % 60); } #ifndef TIMED_REMOTE_TEST_BUILD -uint32_t time_helper_seconds_until(uint8_t target_h, uint8_t target_m, - uint8_t target_s) { - DateTime now; - furi_hal_rtc_get_datetime(&now); - - uint32_t now_seconds = - time_helper_hms_to_seconds(now.hour, now.minute, now.second); - uint32_t target_seconds = - time_helper_hms_to_seconds(target_h, target_m, target_s); - - if (target_seconds < now_seconds) { - /* Roll to next day */ - return (24 * 3600 - now_seconds) + target_seconds; - } - - /* Target time is now or later today */ - return target_seconds - now_seconds; +uint32_t +time_until(uint8_t target_h, uint8_t target_m, + uint8_t target_s) +{ + DateTime now; + furi_hal_rtc_get_datetime(&now); + + uint32_t now_seconds = + time_hms_sec(now.hour, now.minute, now.second); + uint32_t target_seconds = + time_hms_sec(target_h, target_m, target_s); + + if (target_seconds < now_seconds) + { + /* Roll to next day */ + return (24 * 3600 - now_seconds) + target_seconds; + } + + /* Target time is now or later today */ + return target_seconds - now_seconds; } -void time_helper_generate_signal_name(char *buffer, size_t buffer_size) { - DateTime now; - furi_hal_rtc_get_datetime(&now); +void +time_name(char *buffer, size_t buffer_size) +{ + DateTime now; + furi_hal_rtc_get_datetime(&now); - snprintf(buffer, buffer_size, "IR_%04d%02d%02d_%02d%02d%02d", now.year, - now.month, now.day, now.hour, now.minute, now.second); + snprintf(buffer, buffer_size, "IR_%04d%02d%02d_%02d%02d%02d", now.year, + now.month, now.day, now.hour, now.minute, now.second); } #endif diff --git a/helpers/time_helper.h b/helpers/time_helper.h @@ -1,50 +1,12 @@ #pragma once -#include <stdbool.h> #include <stddef.h> #include <stdint.h> -/** - * Convert hours, minutes, seconds to total seconds. - * - * @param h Hours (0-23) - * @param m Minutes (0-59) - * @param s Seconds (0-59) - * @return Total seconds - */ -uint32_t time_helper_hms_to_seconds(uint8_t h, uint8_t m, uint8_t s); - -/** - * Convert total seconds to hours, minutes, seconds. - * - * @param total_seconds Total seconds to convert - * @param h Output: hours (0-23, wraps at 24) - * @param m Output: minutes (0-59) - * @param s Output: seconds (0-59) - */ -void time_helper_seconds_to_hms(uint32_t total_seconds, uint8_t *h, uint8_t *m, - uint8_t *s); +uint32_t time_hms_sec(uint8_t h, uint8_t m, uint8_t s); +void time_sec_hms(uint32_t total_seconds, uint8_t *h, uint8_t *m, uint8_t *s); #ifndef TIMED_REMOTE_TEST_BUILD -/** - * Calculate seconds remaining until a target time. - * Uses Flipper's RTC. If target time has passed, rolls to next day. - * If target time is exactly now, returns 0. - * - * @param target_h Target hour (0-23) - * @param target_m Target minute (0-59) - * @param target_s Target second (0-59) - * @return Seconds until target (today or next day), or 0 if now - */ -uint32_t time_helper_seconds_until(uint8_t target_h, uint8_t target_m, - uint8_t target_s); - -/** - * Generate a timestamp-based signal name placeholder. - * Format: IR_YYYYMMDD_HHMMSS - * - * @param buffer Output buffer (must be at least 20 bytes) - * @param buffer_size Size of buffer - */ -void time_helper_generate_signal_name(char *buffer, size_t buffer_size); +uint32_t time_until(uint8_t target_h, uint8_t target_m, uint8_t target_s); +void time_name(char *buffer, size_t buffer_size); #endif diff --git a/scenes/scene_confirm.c b/scenes/scene_confirm.c @@ -1,46 +1,40 @@ #include "../timed_remote.h" #include "timed_remote_scene.h" -static void confirm_popup_callback(void *context) { - TimedRemoteApp *app = context; - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventConfirmDone); +static void +done_cb(void *context) +{ + TimedRemoteApp *a = context; + view_dispatcher_send_custom_event(a->vd, EvDone); } -void timed_remote_scene_confirm_on_enter(void *context) { - TimedRemoteApp *app = context; - - popup_reset(app->popup); - popup_set_header(app->popup, "Signal Sent!", 64, 20, AlignCenter, - AlignCenter); - popup_set_text(app->popup, app->signal_name, 64, 35, AlignCenter, - AlignCenter); - popup_set_timeout(app->popup, 2000); /* 2 seconds */ - popup_set_context(app->popup, app); - popup_set_callback(app->popup, confirm_popup_callback); - popup_enable_timeout(app->popup); - - view_dispatcher_switch_to_view(app->view_dispatcher, TimedRemoteViewPopup); +void +scene_done_enter(void *context) +{ + TimedRemoteApp *a = context; + popup_reset(a->popup); + popup_set_header(a->popup, "Signal Sent!", 64, 20, AlignCenter, AlignCenter); + popup_set_text(a->popup, a->sig, 64, 35, AlignCenter, AlignCenter); + popup_set_timeout(a->popup, 2000); + popup_set_context(a->popup, a); + popup_set_callback(a->popup, done_cb); + popup_enable_timeout(a->popup); + view_dispatcher_switch_to_view(a->vd, ViewPop); } -bool timed_remote_scene_confirm_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventConfirmDone) { - /* Return to file browser */ - scene_manager_search_and_switch_to_previous_scene( - app->scene_manager, TimedRemoteSceneIrBrowse); - consumed = true; - } - } - - return consumed; +bool +scene_done_event(void *context, SceneManagerEvent event) +{ + TimedRemoteApp *a = context; + if (event.type != SceneManagerEventTypeCustom || event.event != EvDone) + return false; + scene_manager_search_and_switch_to_previous_scene(a->sm, ScBrowse); + return true; } -void timed_remote_scene_confirm_on_exit(void *context) { - TimedRemoteApp *app = context; - popup_reset(app->popup); +void +scene_done_exit(void *context) +{ + TimedRemoteApp *a = context; + popup_reset(a->popup); } diff --git a/scenes/scene_ir_browse.c b/scenes/scene_ir_browse.c @@ -2,68 +2,64 @@ #include "../timed_remote.h" #include "timed_remote_scene.h" -#define IR_DIR_PATH "/ext/infrared" +#define IR_DIR "/ext/infrared" -static FuriString **file_list = NULL; -static size_t file_count = 0; +static FuriString **files; +static size_t nfiles; -static void ir_browse_callback(void *context, uint32_t index) { - TimedRemoteApp *app = context; - if (index < file_count) { - /* Store selected file path */ - snprintf(app->selected_file_path, sizeof(app->selected_file_path), "%s/%s", - IR_DIR_PATH, furi_string_get_cstr(file_list[index])); - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventFileSelected); - } +static void +pick_file(void *context, uint32_t i) +{ + TimedRemoteApp *a = context; + if (i >= nfiles) + return; + snprintf(a->file, sizeof(a->file), "%s/%s", IR_DIR, furi_string_get_cstr(files[i])); + view_dispatcher_send_custom_event(a->vd, EvFile); } -void timed_remote_scene_ir_browse_on_enter(void *context) { - TimedRemoteApp *app = context; +void +scene_browse_enter(void *context) +{ + TimedRemoteApp *a = context; + submenu_reset(a->submenu); + submenu_set_header(a->submenu, "Select IR File"); - submenu_reset(app->submenu); - submenu_set_header(app->submenu, "Select IR File"); + if (ir_files(IR_DIR, &files, &nfiles)) + { + if (nfiles == 0) + { + submenu_add_item(a->submenu, "(No IR files found)", 0, NULL, NULL); + } else + { + for (size_t i = 0; i < nfiles; i++) + submenu_add_item(a->submenu, furi_string_get_cstr(files[i]), i, pick_file, a); + } + } else + { + submenu_add_item(a->submenu, "(Error reading directory)", 0, NULL, NULL); + } - /* Get list of .ir files */ - if (ir_helper_list_files(IR_DIR_PATH, &file_list, &file_count)) { - if (file_count == 0) { - submenu_add_item(app->submenu, "(No IR files found)", 0, NULL, NULL); - } else { - for (size_t i = 0; i < file_count; i++) { - submenu_add_item(app->submenu, furi_string_get_cstr(file_list[i]), i, - ir_browse_callback, app); - } - } - } else { - submenu_add_item(app->submenu, "(Error reading directory)", 0, NULL, NULL); - } - - view_dispatcher_switch_to_view(app->view_dispatcher, TimedRemoteViewSubmenu); + view_dispatcher_switch_to_view(a->vd, ViewMenu); } -bool timed_remote_scene_ir_browse_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventFileSelected) { - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneIrSelect); - consumed = true; - } - } - - return consumed; +bool +scene_browse_event(void *context, SceneManagerEvent event) +{ + TimedRemoteApp *a = context; + if (event.type != SceneManagerEventTypeCustom || event.event != EvFile) + return false; + scene_manager_next_scene(a->sm, ScSelect); + return true; } -void timed_remote_scene_ir_browse_on_exit(void *context) { - TimedRemoteApp *app = context; - submenu_reset(app->submenu); - - /* Free file list */ - if (file_list) { - ir_helper_free_file_list(file_list, file_count); - file_list = NULL; - file_count = 0; - } +void +scene_browse_exit(void *context) +{ + TimedRemoteApp *a = context; + submenu_reset(a->submenu); + if (!files) + return; + ir_files_free(files, nfiles); + files = NULL; + nfiles = 0; } diff --git a/scenes/scene_ir_select.c b/scenes/scene_ir_select.c @@ -2,79 +2,74 @@ #include "../timed_remote.h" #include "timed_remote_scene.h" -static IrSignalList *signal_list = NULL; +static IrSignalList *sigs; -static void ir_select_callback(void *context, uint32_t index) { - TimedRemoteApp *app = context; - if (signal_list && index < signal_list->count) { - /* Free any previous signal */ - if (app->ir_signal) { - infrared_signal_free(app->ir_signal); - } - /* Copy the selected signal */ - app->ir_signal = infrared_signal_alloc(); - infrared_signal_set_signal(app->ir_signal, - signal_list->items[index].signal); +static void +pick_sig(void *context, uint32_t i) +{ + TimedRemoteApp *a = context; + if (!sigs || i >= sigs->count) + return; - /* Copy signal name */ - strncpy(app->signal_name, - furi_string_get_cstr(signal_list->items[index].name), - sizeof(app->signal_name) - 1); - app->signal_name[sizeof(app->signal_name) - 1] = '\0'; + if (a->ir) + infrared_signal_free(a->ir); + a->ir = infrared_signal_alloc(); + if (!a->ir) + return; - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventSignalSelected); - } + infrared_signal_set_signal(a->ir, sigs->items[i].signal); + strncpy(a->sig, furi_string_get_cstr(sigs->items[i].name), sizeof(a->sig) - 1); + a->sig[sizeof(a->sig) - 1] = '\0'; + view_dispatcher_send_custom_event(a->vd, EvSig); } -void timed_remote_scene_ir_select_on_enter(void *context) { - TimedRemoteApp *app = context; +void +scene_select_enter(void *context) +{ + TimedRemoteApp *a = context; + submenu_reset(a->submenu); + submenu_set_header(a->submenu, "Select Signal"); - submenu_reset(app->submenu); - submenu_set_header(app->submenu, "Select Signal"); + sigs = ir_list_alloc(); + if (!sigs) + { + submenu_add_item(a->submenu, "(Out of memory)", 0, NULL, NULL); + } else if (ir_load(a->file, sigs)) + { + if (sigs->count == 0) + { + submenu_add_item(a->submenu, "(No signals in file)", 0, NULL, NULL); + } else + { + for (size_t i = 0; i < sigs->count; i++) + submenu_add_item( + a->submenu, furi_string_get_cstr(sigs->items[i].name), i, pick_sig, a); + } + } else + { + submenu_add_item(a->submenu, "(Error reading file)", 0, NULL, NULL); + } - /* Load signals from selected file */ - signal_list = ir_signal_list_alloc(); - if (ir_helper_load_file(app->selected_file_path, signal_list)) { - if (signal_list->count == 0) { - submenu_add_item(app->submenu, "(No signals in file)", 0, NULL, NULL); - } else { - for (size_t i = 0; i < signal_list->count; i++) { - submenu_add_item(app->submenu, - furi_string_get_cstr(signal_list->items[i].name), i, - ir_select_callback, app); - } - } - } else { - submenu_add_item(app->submenu, "(Error reading file)", 0, NULL, NULL); - } - - view_dispatcher_switch_to_view(app->view_dispatcher, TimedRemoteViewSubmenu); + view_dispatcher_switch_to_view(a->vd, ViewMenu); } -bool timed_remote_scene_ir_select_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventSignalSelected) { - scene_manager_next_scene(app->scene_manager, - TimedRemoteSceneTimerConfig); - consumed = true; - } - } - - return consumed; +bool +scene_select_event(void *context, SceneManagerEvent event) +{ + TimedRemoteApp *a = context; + if (event.type != SceneManagerEventTypeCustom || event.event != EvSig) + return false; + scene_manager_next_scene(a->sm, ScCfg); + return true; } -void timed_remote_scene_ir_select_on_exit(void *context) { - TimedRemoteApp *app = context; - submenu_reset(app->submenu); - - /* Free signal list */ - if (signal_list) { - ir_signal_list_free(signal_list); - signal_list = NULL; - } +void +scene_select_exit(void *context) +{ + TimedRemoteApp *a = context; + submenu_reset(a->submenu); + if (!sigs) + return; + ir_list_free(sigs); + sigs = NULL; } diff --git a/scenes/scene_timer_config.c b/scenes/scene_timer_config.c @@ -1,202 +1,199 @@ #include "../timed_remote.h" #include "timed_remote_scene.h" -enum { - TimerConfigIndexMode, - TimerConfigIndexHours, - TimerConfigIndexMinutes, - TimerConfigIndexSeconds, - TimerConfigIndexRepeat, - TimerConfigIndexConfirm, +enum +{ + IdxMode, + IdxH, + IdxM, + IdxS, + IdxRepeat, + IdxStart, }; -static void timer_config_mode_change(VariableItem *item) { - TimedRemoteApp *app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - app->timer_mode = (index == 0) ? TimerModeCountdown : TimerModeScheduled; - variable_item_set_current_value_text( - item, app->timer_mode == TimerModeCountdown ? "Countdown" : "Scheduled"); - - /* Disable repeat in scheduled mode */ - if (app->timer_mode == TimerModeScheduled) { - app->repeat_count = 0; - } - - /* Trigger rebuild to show/hide repeat options */ - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventModeChanged); -} - -static void timer_config_hours_change(VariableItem *item) { - TimedRemoteApp *app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - app->hours = index; - char buf[4]; - snprintf(buf, sizeof(buf), "%02d", app->hours); - variable_item_set_current_value_text(item, buf); -} - -static void timer_config_minutes_change(VariableItem *item) { - TimedRemoteApp *app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - app->minutes = index; - char buf[4]; - snprintf(buf, sizeof(buf), "%02d", app->minutes); - variable_item_set_current_value_text(item, buf); -} - -static void timer_config_seconds_change(VariableItem *item) { - TimedRemoteApp *app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - app->seconds = index; - char buf[4]; - snprintf(buf, sizeof(buf), "%02d", app->seconds); - variable_item_set_current_value_text(item, buf); -} - -static void timer_config_repeat_change(VariableItem *item) { - TimedRemoteApp *app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - - char buf[16]; - if (index == 0) { - /* Off */ - app->repeat_count = 0; - snprintf(buf, sizeof(buf), "Off"); - } else if (index == 1) { - /* Unlimited */ - app->repeat_count = 255; - snprintf(buf, sizeof(buf), "Unlimited"); - } else { - /* 1, 2, 3, ... 99 */ - app->repeat_count = index - 1; - snprintf(buf, sizeof(buf), "%d", app->repeat_count); - } - variable_item_set_current_value_text(item, buf); -} - -static void timer_config_enter_callback(void *context, uint32_t index) { - TimedRemoteApp *app = context; - /* In countdown mode, confirm is at index 5, in scheduled mode it's at index 4 */ - uint32_t confirm_index = app->timer_mode == TimerModeCountdown - ? TimerConfigIndexConfirm - : (TimerConfigIndexSeconds + 1); - if (index == confirm_index) { - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventTimerConfigured); - } -} - -static void build_timer_config_list(TimedRemoteApp *app) { - VariableItem *item; - char buf[16]; - - variable_item_list_reset(app->variable_item_list); - - /* Mode: Countdown / Scheduled */ - item = variable_item_list_add(app->variable_item_list, "Mode", 2, - timer_config_mode_change, app); - variable_item_set_current_value_index( - item, app->timer_mode == TimerModeCountdown ? 0 : 1); - variable_item_set_current_value_text( - item, app->timer_mode == TimerModeCountdown ? "Countdown" : "Scheduled"); - - /* Hours: 0-23 */ - item = variable_item_list_add(app->variable_item_list, "Hours", 24, - timer_config_hours_change, app); - variable_item_set_current_value_index(item, app->hours); - snprintf(buf, sizeof(buf), "%02d", app->hours); - variable_item_set_current_value_text(item, buf); - - /* Minutes: 0-59 */ - item = variable_item_list_add(app->variable_item_list, "Minutes", 60, - timer_config_minutes_change, app); - variable_item_set_current_value_index(item, app->minutes); - snprintf(buf, sizeof(buf), "%02d", app->minutes); - variable_item_set_current_value_text(item, buf); - - /* Seconds: 0-59 */ - item = variable_item_list_add(app->variable_item_list, "Seconds", 60, - timer_config_seconds_change, app); - variable_item_set_current_value_index(item, app->seconds); - snprintf(buf, sizeof(buf), "%02d", app->seconds); - variable_item_set_current_value_text(item, buf); - - /* Repeat options - only for countdown mode */ - if (app->timer_mode == TimerModeCountdown) { - /* Repeat: Off, Unlimited, 1, 2, 3, ... 99 (total 101 values) */ - item = variable_item_list_add(app->variable_item_list, "Repeat", 101, - timer_config_repeat_change, app); - - /* Convert repeat_count to index */ - uint8_t repeat_index; - if (app->repeat_count == 0) { - repeat_index = 0; /* Off */ - } else if (app->repeat_count == 255) { - repeat_index = 1; /* Unlimited */ - } else { - repeat_index = app->repeat_count + 1; /* 1-99 */ - } - variable_item_set_current_value_index(item, repeat_index); - - /* Set display text */ - if (app->repeat_count == 0) { - variable_item_set_current_value_text(item, "Off"); - } else if (app->repeat_count == 255) { - variable_item_set_current_value_text(item, "Unlimited"); - } else { - snprintf(buf, sizeof(buf), "%d", app->repeat_count); - variable_item_set_current_value_text(item, buf); - } - } - - /* Confirm button */ - variable_item_list_add(app->variable_item_list, ">> Start Timer <<", 0, NULL, - NULL); - - variable_item_list_set_enter_callback(app->variable_item_list, - timer_config_enter_callback, app); -} - -void timed_remote_scene_timer_config_on_enter(void *context) { - TimedRemoteApp *app = context; - build_timer_config_list(app); - view_dispatcher_switch_to_view(app->view_dispatcher, - TimedRemoteViewVariableItemList); -} - -bool timed_remote_scene_timer_config_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventModeChanged) { - /* Rebuild the list to show/hide repeat options */ - build_timer_config_list(app); - consumed = true; - } else if (event.event == TimedRemoteEventTimerConfigured) { - /* Initialize repeats remaining based on repeat_count encoding */ - if (app->repeat_count == 0) { - /* Off - single execution */ - app->repeats_remaining = 1; - } else if (app->repeat_count == 255) { - /* Unlimited */ - app->repeats_remaining = 255; - } else { - /* Fixed count (1-99): initial + N repeats = N+1 total executions */ - app->repeats_remaining = app->repeat_count + 1; - } - scene_manager_next_scene(app->scene_manager, - TimedRemoteSceneTimerRunning); - consumed = true; - } - } - - return consumed; -} - -void timed_remote_scene_timer_config_on_exit(void *context) { - TimedRemoteApp *app = context; - variable_item_list_reset(app->variable_item_list); +#define REP_OFF 0U +#define REP_INF 255U + +static void +set2(VariableItem *it, uint8_t v) +{ + char s[4]; + snprintf(s, sizeof(s), "%02d", v); + variable_item_set_current_value_text(it, s); +} + +static uint8_t +rep_from_idx(uint8_t i) +{ + if (i == 0) + return REP_OFF; + if (i == 1) + return REP_INF; + return i - 1; +} + +static uint8_t +idx_from_rep(uint8_t r) +{ + if (r == REP_OFF) + return 0; + if (r == REP_INF) + return 1; + return r + 1; +} + +static void +set_rep(VariableItem *it, uint8_t r) +{ + if (r == REP_OFF) + { + variable_item_set_current_value_text(it, "Off"); + return; + } + if (r == REP_INF) + { + variable_item_set_current_value_text(it, "Unlimited"); + return; + } + char s[16]; + snprintf(s, sizeof(s), "%u", r); + variable_item_set_current_value_text(it, s); +} + +static uint32_t +start_idx(const TimedRemoteApp *a) +{ + if (a->mode == ModeDown) + return IdxStart; + return IdxRepeat; +} + +static void +mode_chg(VariableItem *it) +{ + TimedRemoteApp *a = variable_item_get_context(it); + a->mode = variable_item_get_current_value_index(it) == 0 + ? ModeDown + : ModeSched; + variable_item_set_current_value_text( + it, a->mode == ModeDown ? "Countdown" : "Scheduled"); + if (a->mode == ModeSched) + a->repeat = REP_OFF; + view_dispatcher_send_custom_event(a->vd, EvMode); +} + +static void +h_chg(VariableItem *it) +{ + TimedRemoteApp *a = variable_item_get_context(it); + a->h = variable_item_get_current_value_index(it); + set2(it, a->h); +} + +static void +m_chg(VariableItem *it) +{ + TimedRemoteApp *a = variable_item_get_context(it); + a->m = variable_item_get_current_value_index(it); + set2(it, a->m); +} + +static void +s_chg(VariableItem *it) +{ + TimedRemoteApp *a = variable_item_get_context(it); + a->s = variable_item_get_current_value_index(it); + set2(it, a->s); +} + +static void +rep_chg(VariableItem *it) +{ + TimedRemoteApp *a = variable_item_get_context(it); + a->repeat = rep_from_idx(variable_item_get_current_value_index(it)); + set_rep(it, a->repeat); +} + +static void +enter_cb(void *ctx, uint32_t i) +{ + TimedRemoteApp *a = ctx; + if (i != start_idx(a)) + return; + view_dispatcher_send_custom_event(a->vd, EvCfg); +} + +static void +add_tm(VariableItemList *l, const char *n, uint8_t nvals, uint8_t v, VariableItemChangeCallback cb, TimedRemoteApp *a) +{ + VariableItem *it = variable_item_list_add(l, n, nvals, cb, a); + variable_item_set_current_value_index(it, v); + set2(it, v); +} + +static void +build(TimedRemoteApp *a) +{ + variable_item_list_reset(a->vlist); + + VariableItem *it = variable_item_list_add(a->vlist, "Mode", 2, mode_chg, a); + variable_item_set_current_value_index(it, a->mode == ModeDown ? 0 : 1); + variable_item_set_current_value_text( + it, a->mode == ModeDown ? "Countdown" : "Scheduled"); + + add_tm(a->vlist, "Hours", 24, a->h, h_chg, a); + add_tm(a->vlist, "Minutes", 60, a->m, m_chg, a); + add_tm(a->vlist, "Seconds", 60, a->s, s_chg, a); + + if (a->mode == ModeDown) + { + it = variable_item_list_add(a->vlist, "Repeat", 101, rep_chg, a); + variable_item_set_current_value_index(it, idx_from_rep(a->repeat)); + set_rep(it, a->repeat); + } + + variable_item_list_add(a->vlist, ">> Start Timer <<", 0, NULL, NULL); + variable_item_list_set_enter_callback(a->vlist, enter_cb, a); +} + +void +scene_cfg_enter(void *context) +{ + TimedRemoteApp *a = context; + build(a); + view_dispatcher_switch_to_view(a->vd, ViewList); +} + +bool +scene_cfg_event(void *context, SceneManagerEvent event) +{ + TimedRemoteApp *a = context; + if (event.type != SceneManagerEventTypeCustom) + return false; + if (event.event == EvMode) + { + build(a); + return true; + } + if (event.event != EvCfg) + return false; + if (a->repeat == REP_OFF) + { + a->repeat_left = 1; + } else if (a->repeat == REP_INF) + { + a->repeat_left = REP_INF; + } else + { + a->repeat_left = a->repeat + 1; + } + scene_manager_next_scene(a->sm, ScRun); + return true; +} + +void +scene_cfg_exit(void *context) +{ + TimedRemoteApp *a = context; + variable_item_list_reset(a->vlist); } diff --git a/scenes/scene_timer_running.c b/scenes/scene_timer_running.c @@ -3,134 +3,130 @@ #include "../timed_remote.h" #include "timed_remote_scene.h" -static void timer_callback(void *context) { - TimedRemoteApp *app = context; - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventTimerTick); +static void +tick_cb(void *context) +{ + TimedRemoteApp *a = context; + view_dispatcher_send_custom_event(a->vd, EvTick); } -static void update_display(TimedRemoteApp *app) { - uint8_t h, m, s; - time_helper_seconds_to_hms(app->seconds_remaining, &h, &m, &s); - - char time_str[16]; - snprintf(time_str, sizeof(time_str), "%02d:%02d:%02d", h, m, s); - - widget_reset(app->widget); - widget_add_string_element(app->widget, 64, 5, AlignCenter, AlignTop, - FontSecondary, app->signal_name); - widget_add_string_element(app->widget, 64, 25, AlignCenter, AlignTop, - FontBigNumbers, time_str); - - if (app->repeat_count > 0 && app->repeat_count < 255) { - /* Fixed repeat count (1-99) */ - char repeat_str[24]; - snprintf(repeat_str, sizeof(repeat_str), "Repeat: %d/%d", - app->repeat_count - app->repeats_remaining + 1, app->repeat_count); - widget_add_string_element(app->widget, 64, 52, AlignCenter, AlignTop, - FontSecondary, repeat_str); - } else if (app->repeat_count == 255) { - /* Unlimited repeat */ - widget_add_string_element(app->widget, 64, 52, AlignCenter, AlignTop, - FontSecondary, "Repeat: Unlimited"); - } +static uint32_t +countdown(const TimedRemoteApp *a) +{ + return time_hms_sec(a->h, a->m, a->s); } -void timed_remote_scene_timer_running_on_enter(void *context) { - TimedRemoteApp *app = context; - - /* Calculate initial remaining time */ - if (app->timer_mode == TimerModeCountdown) { - app->seconds_remaining = - time_helper_hms_to_seconds(app->hours, app->minutes, app->seconds); - } else { - /* Scheduled mode - calculate time until target */ - app->seconds_remaining = - time_helper_seconds_until(app->hours, app->minutes, app->seconds); - if (app->seconds_remaining == 0) { - /* Target time already passed - fire immediately */ - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventTimerFired); - return; - } - } - - /* Initialize repeat tracking for non-repeat or scheduled */ - if (app->repeat_count == 0) { - app->repeats_remaining = 1; - } - - update_display(app); - view_dispatcher_switch_to_view(app->view_dispatcher, TimedRemoteViewWidget); - - /* Start 1-second timer */ - app->timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, app); - furi_timer_start(app->timer, furi_kernel_get_tick_frequency()); /* 1 second */ +static void +redraw(TimedRemoteApp *a) +{ + uint8_t h, m, s; + time_sec_hms(a->left, &h, &m, &s); + + char t[16]; + snprintf(t, sizeof(t), "%02d:%02d:%02d", h, m, s); + + widget_reset(a->widget); + widget_add_string_element(a->widget, 64, 5, AlignCenter, AlignTop, FontSecondary, a->sig); + widget_add_string_element(a->widget, 64, 25, AlignCenter, AlignTop, FontBigNumbers, t); + if (a->repeat > 0 && a->repeat < 255) + { + char r[24]; + snprintf(r, sizeof(r), "Repeat: %d/%d", a->repeat - a->repeat_left + 1, a->repeat); + widget_add_string_element(a->widget, 64, 52, AlignCenter, AlignTop, FontSecondary, r); + } else if (a->repeat == 255) + { + widget_add_string_element( + a->widget, 64, 52, AlignCenter, AlignTop, FontSecondary, "Repeat: Unlimited"); + } } -bool timed_remote_scene_timer_running_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventTimerTick) { - if (app->seconds_remaining > 0) { - app->seconds_remaining--; - update_display(app); - } - - if (app->seconds_remaining == 0) { - /* Timer fired! */ - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventTimerFired); - } - consumed = true; - } else if (event.event == TimedRemoteEventTimerFired) { - /* Transmit IR signal */ - if (app->ir_signal) { - ir_helper_transmit(app->ir_signal); - } - - if (app->repeat_count == 255) { - /* Unlimited repeat */ - app->seconds_remaining = - time_helper_hms_to_seconds(app->hours, app->minutes, app->seconds); - update_display(app); - } else { - app->repeats_remaining--; - - if (app->repeat_count != 0 && app->repeats_remaining > 0) { - /* Reset countdown for next repeat */ - app->seconds_remaining = time_helper_hms_to_seconds( - app->hours, app->minutes, app->seconds); - update_display(app); - } else { - /* Done - show confirmation */ - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneConfirm); - } - } - consumed = true; - } - } else if (event.type == SceneManagerEventTypeBack) { - /* User pressed Back - cancel timer */ - scene_manager_search_and_switch_to_previous_scene(app->scene_manager, - TimedRemoteSceneIrBrowse); - consumed = true; - } - - return consumed; +void +scene_run_enter(void *context) +{ + TimedRemoteApp *a = context; + + if (a->mode == ModeDown) + { + a->left = countdown(a); + } else + { + a->left = time_until(a->h, a->m, a->s); + if (a->left == 0) + { + view_dispatcher_send_custom_event(a->vd, EvFire); + return; + } + } + + if (a->repeat == 0) + a->repeat_left = 1; + + redraw(a); + view_dispatcher_switch_to_view(a->vd, ViewRun); + + a->timer = furi_timer_alloc(tick_cb, FuriTimerTypePeriodic, a); + if (!a->timer) + return; + furi_timer_start(a->timer, furi_kernel_get_tick_frequency()); } -void timed_remote_scene_timer_running_on_exit(void *context) { - TimedRemoteApp *app = context; - - /* Stop and free timer */ - if (app->timer) { - furi_timer_stop(app->timer); - furi_timer_free(app->timer); - app->timer = NULL; - } +bool +scene_run_event(void *context, SceneManagerEvent event) +{ + TimedRemoteApp *a = context; + if (event.type == SceneManagerEventTypeBack) + { + scene_manager_search_and_switch_to_previous_scene(a->sm, ScBrowse); + return true; + } + if (event.type != SceneManagerEventTypeCustom) + return false; + if (event.event == EvTick) + { + if (a->left > 0) + { + a->left--; + redraw(a); + } + if (a->left == 0) + view_dispatcher_send_custom_event(a->vd, EvFire); + return true; + } + if (event.event != EvFire) + return false; + + if (a->ir) + ir_tx(a->ir); + + if (a->repeat == 255) + { + a->left = countdown(a); + redraw(a); + return true; + } + + if (a->repeat_left > 0) + a->repeat_left--; + if (a->repeat != 0 && a->repeat_left > 0) + { + a->left = countdown(a); + redraw(a); + return true; + } + + scene_manager_next_scene(a->sm, ScDone); + return true; +} - widget_reset(app->widget); +void +scene_run_exit(void *context) +{ + TimedRemoteApp *a = context; + if (a->timer) + { + furi_timer_stop(a->timer); + furi_timer_free(a->timer); + a->timer = NULL; + } + widget_reset(a->widget); } diff --git a/scenes/timed_remote_scene.c b/scenes/timed_remote_scene.c @@ -1,35 +1,32 @@ #include "timed_remote_scene.h" -/* Scene handler tables */ - -void (*const timed_remote_scene_on_enter_handlers[])(void *) = { - timed_remote_scene_ir_browse_on_enter, - timed_remote_scene_ir_select_on_enter, - timed_remote_scene_timer_config_on_enter, - timed_remote_scene_timer_running_on_enter, - timed_remote_scene_confirm_on_enter, +static void (*const enter[])(void *) = { + [ScBrowse] = scene_browse_enter, + [ScSelect] = scene_select_enter, + [ScCfg] = scene_cfg_enter, + [ScRun] = scene_run_enter, + [ScDone] = scene_done_enter, }; -bool (*const timed_remote_scene_on_event_handlers[])(void *, - SceneManagerEvent) = { - timed_remote_scene_ir_browse_on_event, - timed_remote_scene_ir_select_on_event, - timed_remote_scene_timer_config_on_event, - timed_remote_scene_timer_running_on_event, - timed_remote_scene_confirm_on_event, +static bool (*const event[])(void *, SceneManagerEvent) = { + [ScBrowse] = scene_browse_event, + [ScSelect] = scene_select_event, + [ScCfg] = scene_cfg_event, + [ScRun] = scene_run_event, + [ScDone] = scene_done_event, }; -void (*const timed_remote_scene_on_exit_handlers[])(void *) = { - timed_remote_scene_ir_browse_on_exit, - timed_remote_scene_ir_select_on_exit, - timed_remote_scene_timer_config_on_exit, - timed_remote_scene_timer_running_on_exit, - timed_remote_scene_confirm_on_exit, +static void (*const on_exit[])(void *) = { + [ScBrowse] = scene_browse_exit, + [ScSelect] = scene_select_exit, + [ScCfg] = scene_cfg_exit, + [ScRun] = scene_run_exit, + [ScDone] = scene_done_exit, }; -const SceneManagerHandlers timed_remote_scene_handlers = { - .on_enter_handlers = timed_remote_scene_on_enter_handlers, - .on_event_handlers = timed_remote_scene_on_event_handlers, - .on_exit_handlers = timed_remote_scene_on_exit_handlers, - .scene_num = TimedRemoteSceneCount, +const SceneManagerHandlers scene_handlers = { + .on_enter_handlers = enter, + .on_event_handlers = event, + .on_exit_handlers = on_exit, + .scene_num = ScN, }; diff --git a/scenes/timed_remote_scene.h b/scenes/timed_remote_scene.h @@ -2,59 +2,39 @@ #include <gui/scene_manager.h> -/* Scene IDs */ -typedef enum { - TimedRemoteSceneIrBrowse, - TimedRemoteSceneIrSelect, - TimedRemoteSceneTimerConfig, - TimedRemoteSceneTimerRunning, - TimedRemoteSceneConfirm, - TimedRemoteSceneCount, -} TimedRemoteScene; - -/* Scene event IDs */ -typedef enum { - TimedRemoteSceneEventConsumed = true, - TimedRemoteSceneEventNotConsumed = false, -} TimedRemoteSceneEvent; - -/* Custom events */ -typedef enum { - /* File/signal selected */ - TimedRemoteEventFileSelected, - TimedRemoteEventSignalSelected, - /* Timer configuration */ - TimedRemoteEventModeChanged, - TimedRemoteEventTimerConfigured, - /* Timer events */ - TimedRemoteEventTimerTick, - TimedRemoteEventTimerFired, - /* Confirmation */ - TimedRemoteEventConfirmDone, -} TimedRemoteCustomEvent; - -/* Scene handlers - declared extern, defined in individual scene files */ -extern void timed_remote_scene_ir_browse_on_enter(void *context); -extern bool timed_remote_scene_ir_browse_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_ir_browse_on_exit(void *context); - -extern void timed_remote_scene_ir_select_on_enter(void *context); -extern bool timed_remote_scene_ir_select_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_ir_select_on_exit(void *context); - -extern void timed_remote_scene_timer_config_on_enter(void *context); -extern bool timed_remote_scene_timer_config_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_timer_config_on_exit(void *context); - -extern void timed_remote_scene_timer_running_on_enter(void *context); -extern bool timed_remote_scene_timer_running_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_timer_running_on_exit(void *context); - -extern void timed_remote_scene_confirm_on_enter(void *context); -extern bool timed_remote_scene_confirm_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_confirm_on_exit(void *context); +typedef enum +{ + ScBrowse, + ScSelect, + ScCfg, + ScRun, + ScDone, + ScN, +} SceneId; + +typedef enum +{ + EvFile, + EvSig, + EvMode, + EvCfg, + EvTick, + EvFire, + EvDone, +} EvId; + +void scene_browse_enter(void *context); +bool scene_browse_event(void *context, SceneManagerEvent event); +void scene_browse_exit(void *context); +void scene_select_enter(void *context); +bool scene_select_event(void *context, SceneManagerEvent event); +void scene_select_exit(void *context); +void scene_cfg_enter(void *context); +bool scene_cfg_event(void *context, SceneManagerEvent event); +void scene_cfg_exit(void *context); +void scene_run_enter(void *context); +bool scene_run_event(void *context, SceneManagerEvent event); +void scene_run_exit(void *context); +void scene_done_enter(void *context); +bool scene_done_event(void *context, SceneManagerEvent event); +void scene_done_exit(void *context); diff --git a/timed_remote.c b/timed_remote.c @@ -1,120 +1,107 @@ #include "timed_remote.h" #include "scenes/timed_remote_scene.h" -extern const SceneManagerHandlers timed_remote_scene_handlers; +extern const SceneManagerHandlers scene_handlers; -/* View dispatcher navigation callback */ -static bool timed_remote_navigation_callback(void *context) { - TimedRemoteApp *app = context; - return scene_manager_handle_back_event(app->scene_manager); +static bool +nav_cb(void *context) +{ + TimedRemoteApp *app = context; + return scene_manager_handle_back_event(app->sm); } -/* View dispatcher custom event callback */ -static bool timed_remote_custom_event_callback(void *context, - uint32_t custom_event) { - TimedRemoteApp *app = context; - return scene_manager_handle_custom_event(app->scene_manager, custom_event); +static bool +evt_cb(void *context, uint32_t evt) +{ + TimedRemoteApp *app = context; + return scene_manager_handle_custom_event(app->sm, evt); } -TimedRemoteApp *timed_remote_app_alloc(void) { - TimedRemoteApp *app = malloc(sizeof(TimedRemoteApp)); - memset(app, 0, sizeof(TimedRemoteApp)); - - /* Open GUI record */ - app->gui = furi_record_open(RECORD_GUI); - - /* Allocate view dispatcher */ - app->view_dispatcher = view_dispatcher_alloc(); - view_dispatcher_set_event_callback_context(app->view_dispatcher, app); - view_dispatcher_set_navigation_event_callback( - app->view_dispatcher, timed_remote_navigation_callback); - view_dispatcher_set_custom_event_callback(app->view_dispatcher, - timed_remote_custom_event_callback); - view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, - ViewDispatcherTypeFullscreen); - - /* Allocate scene manager */ - app->scene_manager = scene_manager_alloc(&timed_remote_scene_handlers, app); - - /* Allocate views */ - app->submenu = submenu_alloc(); - view_dispatcher_add_view(app->view_dispatcher, TimedRemoteViewSubmenu, - submenu_get_view(app->submenu)); - - app->variable_item_list = variable_item_list_alloc(); - view_dispatcher_add_view( - app->view_dispatcher, TimedRemoteViewVariableItemList, - variable_item_list_get_view(app->variable_item_list)); - - app->text_input = text_input_alloc(); - view_dispatcher_add_view(app->view_dispatcher, TimedRemoteViewTextInput, - text_input_get_view(app->text_input)); - - app->widget = widget_alloc(); - view_dispatcher_add_view(app->view_dispatcher, TimedRemoteViewWidget, - widget_get_view(app->widget)); - - app->popup = popup_alloc(); - view_dispatcher_add_view(app->view_dispatcher, TimedRemoteViewPopup, - popup_get_view(app->popup)); - - return app; -} +TimedRemoteApp * +timed_remote_app_alloc(void) +{ + TimedRemoteApp *app = malloc(sizeof(TimedRemoteApp)); + memset(app, 0, sizeof(TimedRemoteApp)); + + app->gui = furi_record_open(RECORD_GUI); + + app->vd = view_dispatcher_alloc(); + view_dispatcher_set_event_callback_context(app->vd, app); + view_dispatcher_set_navigation_event_callback(app->vd, nav_cb); + view_dispatcher_set_custom_event_callback(app->vd, evt_cb); + view_dispatcher_attach_to_gui(app->vd, app->gui, + ViewDispatcherTypeFullscreen); + + app->sm = scene_manager_alloc(&scene_handlers, app); + + app->submenu = submenu_alloc(); + view_dispatcher_add_view(app->vd, ViewMenu, + submenu_get_view(app->submenu)); -void timed_remote_app_free(TimedRemoteApp *app) { - /* Free timer if still running */ - if (app->timer) { - furi_timer_stop(app->timer); - furi_timer_free(app->timer); - } + app->vlist = variable_item_list_alloc(); + view_dispatcher_add_view( + app->vd, ViewList, + variable_item_list_get_view(app->vlist)); + + app->widget = widget_alloc(); + view_dispatcher_add_view(app->vd, ViewRun, + widget_get_view(app->widget)); + + app->popup = popup_alloc(); + view_dispatcher_add_view(app->vd, ViewPop, + popup_get_view(app->popup)); + + return app; +} - /* Free IR signal if allocated */ - if (app->ir_signal) { - infrared_signal_free(app->ir_signal); - } +void +timed_remote_app_free(TimedRemoteApp *app) +{ + if (app->timer) + { + furi_timer_stop(app->timer); + furi_timer_free(app->timer); + } - /* Remove and free views */ - view_dispatcher_remove_view(app->view_dispatcher, TimedRemoteViewSubmenu); - submenu_free(app->submenu); + if (app->ir) + { + infrared_signal_free(app->ir); + } - view_dispatcher_remove_view(app->view_dispatcher, - TimedRemoteViewVariableItemList); - variable_item_list_free(app->variable_item_list); + view_dispatcher_remove_view(app->vd, ViewMenu); + submenu_free(app->submenu); - view_dispatcher_remove_view(app->view_dispatcher, TimedRemoteViewTextInput); - text_input_free(app->text_input); + view_dispatcher_remove_view(app->vd, + ViewList); + variable_item_list_free(app->vlist); - view_dispatcher_remove_view(app->view_dispatcher, TimedRemoteViewWidget); - widget_free(app->widget); + view_dispatcher_remove_view(app->vd, ViewRun); + widget_free(app->widget); - view_dispatcher_remove_view(app->view_dispatcher, TimedRemoteViewPopup); - popup_free(app->popup); + view_dispatcher_remove_view(app->vd, ViewPop); + popup_free(app->popup); - /* Free scene manager */ - scene_manager_free(app->scene_manager); + scene_manager_free(app->sm); - /* Free view dispatcher */ - view_dispatcher_free(app->view_dispatcher); + view_dispatcher_free(app->vd); - /* Close GUI record */ - furi_record_close(RECORD_GUI); + furi_record_close(RECORD_GUI); - free(app); + free(app); } -int32_t timed_remote_app(void *p) { - UNUSED(p); +int32_t +timed_remote_app(void *p) +{ + UNUSED(p); - TimedRemoteApp *app = timed_remote_app_alloc(); + TimedRemoteApp *app = timed_remote_app_alloc(); - /* Start with file browser scene */ - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneIrBrowse); + scene_manager_next_scene(app->sm, ScBrowse); - /* Run event loop */ - view_dispatcher_run(app->view_dispatcher); + view_dispatcher_run(app->vd); - /* Cleanup */ - timed_remote_app_free(app); + timed_remote_app_free(app); - return 0; + return 0; } diff --git a/timed_remote.h b/timed_remote.h @@ -4,69 +4,55 @@ #include <gui/gui.h> #include <gui/modules/popup.h> #include <gui/modules/submenu.h> -#include <gui/modules/text_input.h> #include <gui/modules/variable_item_list.h> #include <gui/modules/widget.h> #include <gui/scene_manager.h> #include <gui/view_dispatcher.h> #include <lib/infrared/signal/infrared_signal.h> -/* Timer mode enumeration */ -typedef enum { - TimerModeCountdown, - TimerModeScheduled, -} TimerMode; +typedef enum +{ + ModeDown, + ModeSched, +} ModeId; -/* View IDs for ViewDispatcher */ -typedef enum { - TimedRemoteViewSubmenu, - TimedRemoteViewVariableItemList, - TimedRemoteViewTextInput, - TimedRemoteViewWidget, - TimedRemoteViewPopup, -} TimedRemoteView; +typedef enum +{ + ViewMenu, + ViewList, + ViewRun, + ViewPop, +} ViewId; -/* Maximum lengths */ #define SIGNAL_NAME_MAX_LEN 32 #define FILE_PATH_MAX_LEN 256 -/* Main application state */ -typedef struct { - /* Core Flipper components */ - Gui *gui; - ViewDispatcher *view_dispatcher; - SceneManager *scene_manager; +typedef struct +{ + Gui *gui; + ViewDispatcher *vd; + SceneManager *sm; - /* Views */ - Submenu *submenu; - VariableItemList *variable_item_list; - TextInput *text_input; - Widget *widget; - Popup *popup; + Submenu *submenu; + VariableItemList *vlist; + Widget *widget; + Popup *popup; - /* IR state */ - InfraredSignal *ir_signal; - char signal_name[SIGNAL_NAME_MAX_LEN]; - char selected_file_path[FILE_PATH_MAX_LEN]; + InfraredSignal *ir; + char sig[SIGNAL_NAME_MAX_LEN]; + char file[FILE_PATH_MAX_LEN]; - /* Timer configuration */ - TimerMode timer_mode; - uint8_t hours; - uint8_t minutes; - uint8_t seconds; + ModeId mode; + uint8_t h; + uint8_t m; + uint8_t s; - /* Repeat options (Countdown mode only) */ - uint8_t repeat_count; /* 0 = off, 255 = unlimited, 1-99 = count */ - uint8_t repeats_remaining; + uint8_t repeat; /* 0 = off, 255 = unlimited, 1-99 = count */ + uint8_t repeat_left; - /* Timer runtime state */ - FuriTimer *timer; - uint32_t seconds_remaining; - - /* Text input buffer */ - char text_input_buffer[SIGNAL_NAME_MAX_LEN]; + FuriTimer *timer; + uint32_t left; } TimedRemoteApp; -/* App lifecycle */ TimedRemoteApp *timed_remote_app_alloc(void); void timed_remote_app_free(TimedRemoteApp *app);