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 3fc23431d5701c11f9714b6154894808739cf1fc
parent 1c9a4403c1711ab87c34efd9ddcfb65155f7c621
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date:   Fri, 30 Jan 2026 22:56:08 +0100

refactor: simplify app by removing IR recording and consolidating timer config

- Remove IR recording scenes and functionality (7 scenes deleted)
- Create unified timer config scene combining mode, time input, and repeat options
- Start app directly at IR file browser
- Fix scheduled mode button index calculation
- Simplify scene flow from 10 scenes to 5

Diffstat:
Mhelpers/ir_helper.c | 98-------------------------------------------------------------------------------
Mhelpers/ir_helper.h | 20--------------------
Mscenes/scene_confirm.c | 4++--
Dscenes/scene_ir_record.c | 57---------------------------------------------------------
Mscenes/scene_ir_select.c | 3++-
Dscenes/scene_ir_source.c | 54------------------------------------------------------
Dscenes/scene_main_menu.c | 45---------------------------------------------
Dscenes/scene_repeat_options.c | 100-------------------------------------------------------------------------------
Dscenes/scene_signal_name.c | 61-------------------------------------------------------------
Dscenes/scene_time_input.c | 116-------------------------------------------------------------------------------
Ascenes/scene_timer_config.c | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dscenes/scene_timer_mode.c | 56--------------------------------------------------------
Mscenes/scene_timer_running.c | 2+-
Mscenes/timed_remote_scene.c | 24+++---------------------
Mscenes/timed_remote_scene.h | 63++++++++-------------------------------------------------------
Mtimed_remote.c | 4++--
16 files changed, 209 insertions(+), 689 deletions(-)

diff --git a/helpers/ir_helper.c b/helpers/ir_helper.c @@ -50,106 +50,8 @@ static void ir_signal_list_add(IrSignalList *list, InfraredSignal *signal, list->count++; } -/* ========== Recording ========== */ - -typedef struct { - InfraredSignal *signal; - bool received; - FuriSemaphore *semaphore; -} RecordContext; - -static void ir_record_callback(void *context, - InfraredWorkerSignal *received_signal) { - RecordContext *ctx = context; - - const InfraredMessage *message = - infrared_worker_get_decoded_signal(received_signal); - if (message) { - ctx->signal = infrared_signal_alloc(); - infrared_signal_set_message(ctx->signal, message); - ctx->received = true; - } else { - /* Raw signal */ - const uint32_t *timings; - size_t timings_cnt; - infrared_worker_get_raw_signal(received_signal, &timings, &timings_cnt); - if (timings && timings_cnt > 0) { - ctx->signal = infrared_signal_alloc(); - infrared_signal_set_raw_signal(ctx->signal, timings, timings_cnt, - INFRARED_COMMON_CARRIER_FREQUENCY, - INFRARED_COMMON_DUTY_CYCLE); - ctx->received = true; - } - } - - furi_semaphore_release(ctx->semaphore); -} - -bool ir_helper_record(InfraredSignal **signal, uint32_t timeout_ms) { - InfraredWorker *worker = infrared_worker_alloc(); - - RecordContext ctx = { - .signal = NULL, - .received = false, - .semaphore = furi_semaphore_alloc(0, 1), - }; - - infrared_worker_rx_set_received_signal_callback(worker, ir_record_callback, - &ctx); - infrared_worker_rx_start(worker); - - FuriStatus status = furi_semaphore_acquire(ctx.semaphore, timeout_ms); - - infrared_worker_rx_stop(worker); - infrared_worker_free(worker); - furi_semaphore_free(ctx.semaphore); - - if (status == FuriStatusOk && ctx.received) { - *signal = ctx.signal; - return true; - } - - if (ctx.signal) { - infrared_signal_free(ctx.signal); - } - *signal = NULL; - return false; -} - /* ========== File I/O ========== */ -bool ir_helper_save(InfraredSignal *signal, const char *name, - const char *path) { - Storage *storage = furi_record_open(RECORD_STORAGE); - FlipperFormat *ff = flipper_format_file_alloc(storage); - bool success = false; - - do { - /* Check if file exists */ - if (storage_file_exists(storage, path)) { - /* Append mode */ - if (!flipper_format_file_open_append(ff, path)) - break; - } else { - /* Create new file */ - if (!flipper_format_file_open_always(ff, path)) - break; - if (!flipper_format_write_header_cstr(ff, IR_FILE_HEADER, - IR_FILE_VERSION)) - break; - } - - if (!infrared_signal_save(signal, ff, name)) - break; - - success = true; - } while (false); - - flipper_format_free(ff); - furi_record_close(RECORD_STORAGE); - return success; -} - bool ir_helper_load_file(const char *path, IrSignalList *list) { Storage *storage = furi_record_open(RECORD_STORAGE); FlipperFormat *ff = flipper_format_file_alloc(storage); diff --git a/helpers/ir_helper.h b/helpers/ir_helper.h @@ -32,26 +32,6 @@ IrSignalList *ir_signal_list_alloc(void); void ir_signal_list_free(IrSignalList *list); /** - * Record an IR signal from the receiver. - * Blocks until signal is received or timeout. - * - * @param signal Output: allocated InfraredSignal (caller must free) - * @param timeout_ms Timeout in milliseconds - * @return true if signal recorded, false on timeout/error - */ -bool ir_helper_record(InfraredSignal **signal, uint32_t timeout_ms); - -/** - * Save a signal to an .ir file (appends if file exists). - * - * @param signal The signal to save - * @param name Signal name (e.g., "Power") - * @param path File path (e.g., "/ext/infrared/timed_remote.ir") - * @return true on success - */ -bool ir_helper_save(InfraredSignal *signal, const char *name, const char *path); - -/** * Load all signals from an .ir file. * * @param path File path to load diff --git a/scenes/scene_confirm.c b/scenes/scene_confirm.c @@ -30,9 +30,9 @@ bool timed_remote_scene_confirm_on_event(void *context, if (event.type == SceneManagerEventTypeCustom) { if (event.event == TimedRemoteEventConfirmDone) { - /* Return to main menu */ + /* Return to file browser */ scene_manager_search_and_switch_to_previous_scene( - app->scene_manager, TimedRemoteSceneMainMenu); + app->scene_manager, TimedRemoteSceneIrBrowse); consumed = true; } } diff --git a/scenes/scene_ir_record.c b/scenes/scene_ir_record.c @@ -1,57 +0,0 @@ -#include "../helpers/ir_helper.h" -#include "../timed_remote.h" -#include "timed_remote_scene.h" - -#define RECORD_TIMEOUT_MS 30000 - -void timed_remote_scene_ir_record_on_enter(void *context) { - TimedRemoteApp *app = context; - - widget_reset(app->widget); - widget_add_string_element(app->widget, 64, 10, AlignCenter, AlignTop, - FontPrimary, "Recording IR..."); - widget_add_string_element(app->widget, 64, 26, AlignCenter, AlignTop, - FontSecondary, "Point remote at Flipper"); - widget_add_string_element(app->widget, 64, 40, AlignCenter, AlignTop, - FontSecondary, "and press a button"); - - view_dispatcher_switch_to_view(app->view_dispatcher, TimedRemoteViewWidget); - - /* Start recording in a thread or use async approach */ - /* For now, blocking call - consider making async for better UX */ - InfraredSignal *signal = NULL; - bool success = ir_helper_record(&signal, RECORD_TIMEOUT_MS); - - if (success && signal) { - /* Free any previous signal */ - if (app->ir_signal) { - infrared_signal_free(app->ir_signal); - } - app->ir_signal = signal; - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventRecordComplete); - } else { - /* Timeout or error - go back */ - scene_manager_previous_scene(app->scene_manager); - } -} - -bool timed_remote_scene_ir_record_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventRecordComplete) { - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneSignalName); - consumed = true; - } - } - - return consumed; -} - -void timed_remote_scene_ir_record_on_exit(void *context) { - TimedRemoteApp *app = context; - widget_reset(app->widget); -} diff --git a/scenes/scene_ir_select.c b/scenes/scene_ir_select.c @@ -59,7 +59,8 @@ bool timed_remote_scene_ir_select_on_event(void *context, if (event.type == SceneManagerEventTypeCustom) { if (event.event == TimedRemoteEventSignalSelected) { - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneTimerMode); + scene_manager_next_scene(app->scene_manager, + TimedRemoteSceneTimerConfig); consumed = true; } } diff --git a/scenes/scene_ir_source.c b/scenes/scene_ir_source.c @@ -1,54 +0,0 @@ -#include "../timed_remote.h" -#include "timed_remote_scene.h" - -enum { - IrSourceIndexRecord, - IrSourceIndexBrowse, -}; - -static void ir_source_callback(void *context, uint32_t index) { - TimedRemoteApp *app = context; - if (index == IrSourceIndexRecord) { - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventRecordSignal); - } else if (index == IrSourceIndexBrowse) { - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventBrowseFiles); - } -} - -void timed_remote_scene_ir_source_on_enter(void *context) { - TimedRemoteApp *app = context; - - submenu_reset(app->submenu); - submenu_set_header(app->submenu, "Select IR Source"); - submenu_add_item(app->submenu, "Record New Signal", IrSourceIndexRecord, - ir_source_callback, app); - submenu_add_item(app->submenu, "Browse IR Files", IrSourceIndexBrowse, - ir_source_callback, app); - - view_dispatcher_switch_to_view(app->view_dispatcher, TimedRemoteViewSubmenu); -} - -bool timed_remote_scene_ir_source_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventRecordSignal) { - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneIrRecord); - consumed = true; - } else if (event.event == TimedRemoteEventBrowseFiles) { - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneIrBrowse); - consumed = true; - } - } - - return consumed; -} - -void timed_remote_scene_ir_source_on_exit(void *context) { - TimedRemoteApp *app = context; - submenu_reset(app->submenu); -} diff --git a/scenes/scene_main_menu.c b/scenes/scene_main_menu.c @@ -1,45 +0,0 @@ -#include "../timed_remote.h" -#include "timed_remote_scene.h" - -enum { - MainMenuIndexStartTimer, -}; - -static void main_menu_callback(void *context, uint32_t index) { - TimedRemoteApp *app = context; - if (index == MainMenuIndexStartTimer) { - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventStartTimer); - } -} - -void timed_remote_scene_main_menu_on_enter(void *context) { - TimedRemoteApp *app = context; - - submenu_reset(app->submenu); - submenu_set_header(app->submenu, "Timed Remote"); - submenu_add_item(app->submenu, "Start New Timer", MainMenuIndexStartTimer, - main_menu_callback, app); - - view_dispatcher_switch_to_view(app->view_dispatcher, TimedRemoteViewSubmenu); -} - -bool timed_remote_scene_main_menu_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventStartTimer) { - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneIrSource); - consumed = true; - } - } - - return consumed; -} - -void timed_remote_scene_main_menu_on_exit(void *context) { - TimedRemoteApp *app = context; - submenu_reset(app->submenu); -} diff --git a/scenes/scene_repeat_options.c b/scenes/scene_repeat_options.c @@ -1,100 +0,0 @@ -#include "../timed_remote.h" -#include "timed_remote_scene.h" - -enum { - RepeatOptionsIndexToggle, - RepeatOptionsIndexCount, - RepeatOptionsIndexConfirm, -}; - -static void repeat_toggle_change(VariableItem *item) { - TimedRemoteApp *app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - app->repeat_enabled = (index == 1); - variable_item_set_current_value_text(item, - app->repeat_enabled ? "On" : "Off"); -} - -static void repeat_count_change(VariableItem *item) { - TimedRemoteApp *app = variable_item_get_context(item); - uint8_t index = variable_item_get_current_value_index(item); - app->repeat_count = index; /* 0 = unlimited, 1-99 = fixed */ - - char buf[16]; - if (app->repeat_count == 0) { - snprintf(buf, sizeof(buf), "Unlimited"); - } else { - snprintf(buf, sizeof(buf), "%d", app->repeat_count); - } - variable_item_set_current_value_text(item, buf); -} - -static void repeat_options_enter_callback(void *context, uint32_t index) { - TimedRemoteApp *app = context; - if (index == RepeatOptionsIndexConfirm) { - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventRepeatConfigured); - } -} - -void timed_remote_scene_repeat_options_on_enter(void *context) { - TimedRemoteApp *app = context; - VariableItem *item; - - variable_item_list_reset(app->variable_item_list); - - /* Repeat toggle: Off/On */ - item = variable_item_list_add(app->variable_item_list, "Repeat", 2, - repeat_toggle_change, app); - variable_item_set_current_value_index(item, app->repeat_enabled ? 1 : 0); - variable_item_set_current_value_text(item, - app->repeat_enabled ? "On" : "Off"); - - /* Repeat count: 0 = unlimited, 1-99 = fixed */ - item = variable_item_list_add(app->variable_item_list, "Count", 100, - repeat_count_change, app); - variable_item_set_current_value_index(item, app->repeat_count); - if (app->repeat_count == 0) { - variable_item_set_current_value_text(item, "Unlimited"); - } else { - char buf[8]; - 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, - repeat_options_enter_callback, app); - - view_dispatcher_switch_to_view(app->view_dispatcher, - TimedRemoteViewVariableItemList); -} - -bool timed_remote_scene_repeat_options_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventRepeatConfigured) { - /* Initialize repeats remaining */ - app->repeats_remaining = - app->repeat_enabled - ? (app->repeat_count == 0 ? 255 : app->repeat_count) - : 1; - scene_manager_next_scene(app->scene_manager, - TimedRemoteSceneTimerRunning); - consumed = true; - } - } - - return consumed; -} - -void timed_remote_scene_repeat_options_on_exit(void *context) { - TimedRemoteApp *app = context; - variable_item_list_reset(app->variable_item_list); -} diff --git a/scenes/scene_signal_name.c b/scenes/scene_signal_name.c @@ -1,61 +0,0 @@ -#include "../helpers/ir_helper.h" -#include "../helpers/time_helper.h" -#include "../timed_remote.h" -#include "timed_remote_scene.h" - -#define TIMED_REMOTE_IR_PATH "/ext/infrared/timed_remote.ir" - -static void signal_name_text_input_callback(void *context) { - TimedRemoteApp *app = context; - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventRecordComplete); -} - -void timed_remote_scene_signal_name_on_enter(void *context) { - TimedRemoteApp *app = context; - - /* Generate default name based on timestamp */ - time_helper_generate_signal_name(app->text_input_buffer, - sizeof(app->text_input_buffer)); - - text_input_reset(app->text_input); - text_input_set_header_text(app->text_input, "Name this signal:"); - text_input_set_result_callback( - app->text_input, signal_name_text_input_callback, app, - app->text_input_buffer, sizeof(app->text_input_buffer), - true /* clear default text on focus */); - - view_dispatcher_switch_to_view(app->view_dispatcher, - TimedRemoteViewTextInput); -} - -bool timed_remote_scene_signal_name_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventRecordComplete) { - /* Save the signal name */ - strncpy(app->signal_name, app->text_input_buffer, - sizeof(app->signal_name) - 1); - app->signal_name[sizeof(app->signal_name) - 1] = '\0'; - - /* Save signal to file */ - if (app->ir_signal) { - ir_helper_save(app->ir_signal, app->signal_name, TIMED_REMOTE_IR_PATH); - } - - /* Proceed to timer mode selection */ - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneTimerMode); - consumed = true; - } - } - - return consumed; -} - -void timed_remote_scene_signal_name_on_exit(void *context) { - TimedRemoteApp *app = context; - text_input_reset(app->text_input); -} diff --git a/scenes/scene_time_input.c b/scenes/scene_time_input.c @@ -1,116 +0,0 @@ -#include "../timed_remote.h" -#include "timed_remote_scene.h" - -/* State indices for VariableItemList */ -enum { - TimeInputIndexHours, - TimeInputIndexMinutes, - TimeInputIndexSeconds, - TimeInputIndexConfirm, -}; - -static void time_input_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 time_input_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 time_input_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 time_input_enter_callback(void *context, uint32_t index) { - TimedRemoteApp *app = context; - if (index == TimeInputIndexConfirm) { - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventTimeSet); - } -} - -void timed_remote_scene_time_input_on_enter(void *context) { - TimedRemoteApp *app = context; - VariableItem *item; - char buf[4]; - - variable_item_list_reset(app->variable_item_list); - - /* Note: VariableItemList doesn't support headers, the mode context is - * implicit */ - - /* Hours: 0-23 */ - item = variable_item_list_add(app->variable_item_list, "Hours", 24, - time_input_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, - time_input_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, - time_input_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); - - /* 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, - time_input_enter_callback, app); - - view_dispatcher_switch_to_view(app->view_dispatcher, - TimedRemoteViewVariableItemList); -} - -bool timed_remote_scene_time_input_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventTimeSet) { - if (app->timer_mode == TimerModeCountdown) { - /* Countdown mode - go to repeat options */ - scene_manager_next_scene(app->scene_manager, - TimedRemoteSceneRepeatOptions); - } else { - /* Scheduled mode - go directly to timer */ - app->repeat_enabled = false; - scene_manager_next_scene(app->scene_manager, - TimedRemoteSceneTimerRunning); - } - consumed = true; - } - } - - return consumed; -} - -void timed_remote_scene_time_input_on_exit(void *context) { - TimedRemoteApp *app = context; - variable_item_list_reset(app->variable_item_list); -} diff --git a/scenes/scene_timer_config.c b/scenes/scene_timer_config.c @@ -0,0 +1,191 @@ +#include "../timed_remote.h" +#include "timed_remote_scene.h" + +enum { + TimerConfigIndexMode, + TimerConfigIndexHours, + TimerConfigIndexMinutes, + TimerConfigIndexSeconds, + TimerConfigIndexRepeat, + TimerConfigIndexCount, + TimerConfigIndexConfirm, +}; + +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_enabled = false; + } + + /* 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); + app->repeat_enabled = (index == 1); + variable_item_set_current_value_text(item, + app->repeat_enabled ? "On" : "Off"); +} + +static void timer_config_count_change(VariableItem *item) { + TimedRemoteApp *app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + app->repeat_count = index; /* 0 = unlimited, 1-99 = fixed */ + + char buf[16]; + if (app->repeat_count == 0) { + snprintf(buf, sizeof(buf), "Unlimited"); + } else { + 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 6, 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 toggle: Off/On */ + item = variable_item_list_add(app->variable_item_list, "Repeat", 2, + timer_config_repeat_change, app); + variable_item_set_current_value_index(item, app->repeat_enabled ? 1 : 0); + variable_item_set_current_value_text(item, + app->repeat_enabled ? "On" : "Off"); + + /* Repeat count: 0 = unlimited, 1-99 = fixed */ + item = variable_item_list_add(app->variable_item_list, "Count", 100, + timer_config_count_change, app); + variable_item_set_current_value_index(item, app->repeat_count); + if (app->repeat_count == 0) { + 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 */ + app->repeats_remaining = + app->repeat_enabled + ? (app->repeat_count == 0 ? 255 : 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); +} diff --git a/scenes/scene_timer_mode.c b/scenes/scene_timer_mode.c @@ -1,56 +0,0 @@ -#include "../timed_remote.h" -#include "timed_remote_scene.h" - -enum { - TimerModeIndexCountdown, - TimerModeIndexScheduled, -}; - -static void timer_mode_callback(void *context, uint32_t index) { - TimedRemoteApp *app = context; - if (index == TimerModeIndexCountdown) { - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventModeCountdown); - } else if (index == TimerModeIndexScheduled) { - view_dispatcher_send_custom_event(app->view_dispatcher, - TimedRemoteEventModeScheduled); - } -} - -void timed_remote_scene_timer_mode_on_enter(void *context) { - TimedRemoteApp *app = context; - - submenu_reset(app->submenu); - submenu_set_header(app->submenu, "Timer Mode"); - submenu_add_item(app->submenu, "Countdown (in X time)", - TimerModeIndexCountdown, timer_mode_callback, app); - submenu_add_item(app->submenu, "Scheduled (at X time)", - TimerModeIndexScheduled, timer_mode_callback, app); - - view_dispatcher_switch_to_view(app->view_dispatcher, TimedRemoteViewSubmenu); -} - -bool timed_remote_scene_timer_mode_on_event(void *context, - SceneManagerEvent event) { - TimedRemoteApp *app = context; - bool consumed = false; - - if (event.type == SceneManagerEventTypeCustom) { - if (event.event == TimedRemoteEventModeCountdown) { - app->timer_mode = TimerModeCountdown; - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneTimeInput); - consumed = true; - } else if (event.event == TimedRemoteEventModeScheduled) { - app->timer_mode = TimerModeScheduled; - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneTimeInput); - consumed = true; - } - } - - return consumed; -} - -void timed_remote_scene_timer_mode_on_exit(void *context) { - TimedRemoteApp *app = context; - submenu_reset(app->submenu); -} diff --git a/scenes/scene_timer_running.c b/scenes/scene_timer_running.c @@ -106,7 +106,7 @@ bool timed_remote_scene_timer_running_on_event(void *context, } else if (event.type == SceneManagerEventTypeBack) { /* User pressed Back - cancel timer */ scene_manager_search_and_switch_to_previous_scene(app->scene_manager, - TimedRemoteSceneMainMenu); + TimedRemoteSceneIrBrowse); consumed = true; } diff --git a/scenes/timed_remote_scene.c b/scenes/timed_remote_scene.c @@ -3,44 +3,26 @@ /* Scene handler tables */ void (*const timed_remote_scene_on_enter_handlers[])(void *) = { - timed_remote_scene_main_menu_on_enter, - timed_remote_scene_ir_source_on_enter, - timed_remote_scene_ir_record_on_enter, - timed_remote_scene_signal_name_on_enter, timed_remote_scene_ir_browse_on_enter, timed_remote_scene_ir_select_on_enter, - timed_remote_scene_timer_mode_on_enter, - timed_remote_scene_time_input_on_enter, - timed_remote_scene_repeat_options_on_enter, + timed_remote_scene_timer_config_on_enter, timed_remote_scene_timer_running_on_enter, timed_remote_scene_confirm_on_enter, }; bool (*const timed_remote_scene_on_event_handlers[])(void *, SceneManagerEvent) = { - timed_remote_scene_main_menu_on_event, - timed_remote_scene_ir_source_on_event, - timed_remote_scene_ir_record_on_event, - timed_remote_scene_signal_name_on_event, timed_remote_scene_ir_browse_on_event, timed_remote_scene_ir_select_on_event, - timed_remote_scene_timer_mode_on_event, - timed_remote_scene_time_input_on_event, - timed_remote_scene_repeat_options_on_event, + timed_remote_scene_timer_config_on_event, timed_remote_scene_timer_running_on_event, timed_remote_scene_confirm_on_event, }; void (*const timed_remote_scene_on_exit_handlers[])(void *) = { - timed_remote_scene_main_menu_on_exit, - timed_remote_scene_ir_source_on_exit, - timed_remote_scene_ir_record_on_exit, - timed_remote_scene_signal_name_on_exit, timed_remote_scene_ir_browse_on_exit, timed_remote_scene_ir_select_on_exit, - timed_remote_scene_timer_mode_on_exit, - timed_remote_scene_time_input_on_exit, - timed_remote_scene_repeat_options_on_exit, + timed_remote_scene_timer_config_on_exit, timed_remote_scene_timer_running_on_exit, timed_remote_scene_confirm_on_exit, }; diff --git a/scenes/timed_remote_scene.h b/scenes/timed_remote_scene.h @@ -4,15 +4,9 @@ /* Scene IDs */ typedef enum { - TimedRemoteSceneMainMenu, - TimedRemoteSceneIrSource, - TimedRemoteSceneIrRecord, - TimedRemoteSceneSignalName, TimedRemoteSceneIrBrowse, TimedRemoteSceneIrSelect, - TimedRemoteSceneTimerMode, - TimedRemoteSceneTimeInput, - TimedRemoteSceneRepeatOptions, + TimedRemoteSceneTimerConfig, TimedRemoteSceneTimerRunning, TimedRemoteSceneConfirm, TimedRemoteSceneCount, @@ -26,23 +20,12 @@ typedef enum { /* Custom events */ typedef enum { - /* Main menu */ - TimedRemoteEventStartTimer, - /* IR source */ - TimedRemoteEventRecordSignal, - TimedRemoteEventBrowseFiles, - /* Recording complete */ - TimedRemoteEventRecordComplete, /* File/signal selected */ TimedRemoteEventFileSelected, TimedRemoteEventSignalSelected, - /* Timer mode */ - TimedRemoteEventModeCountdown, - TimedRemoteEventModeScheduled, - /* Time input complete */ - TimedRemoteEventTimeSet, - /* Repeat options */ - TimedRemoteEventRepeatConfigured, + /* Timer configuration */ + TimedRemoteEventModeChanged, + TimedRemoteEventTimerConfigured, /* Timer events */ TimedRemoteEventTimerTick, TimedRemoteEventTimerFired, @@ -51,26 +34,6 @@ typedef enum { } TimedRemoteCustomEvent; /* Scene handlers - declared extern, defined in individual scene files */ -extern void timed_remote_scene_main_menu_on_enter(void *context); -extern bool timed_remote_scene_main_menu_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_main_menu_on_exit(void *context); - -extern void timed_remote_scene_ir_source_on_enter(void *context); -extern bool timed_remote_scene_ir_source_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_ir_source_on_exit(void *context); - -extern void timed_remote_scene_ir_record_on_enter(void *context); -extern bool timed_remote_scene_ir_record_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_ir_record_on_exit(void *context); - -extern void timed_remote_scene_signal_name_on_enter(void *context); -extern bool timed_remote_scene_signal_name_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_signal_name_on_exit(void *context); - extern void timed_remote_scene_ir_browse_on_enter(void *context); extern bool timed_remote_scene_ir_browse_on_event(void *context, SceneManagerEvent event); @@ -81,20 +44,10 @@ 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_mode_on_enter(void *context); -extern bool timed_remote_scene_timer_mode_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_timer_mode_on_exit(void *context); - -extern void timed_remote_scene_time_input_on_enter(void *context); -extern bool timed_remote_scene_time_input_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_time_input_on_exit(void *context); - -extern void timed_remote_scene_repeat_options_on_enter(void *context); -extern bool timed_remote_scene_repeat_options_on_event(void *context, - SceneManagerEvent event); -extern void timed_remote_scene_repeat_options_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, diff --git a/timed_remote.c b/timed_remote.c @@ -107,8 +107,8 @@ int32_t timed_remote_app(void *p) { TimedRemoteApp *app = timed_remote_app_alloc(); - /* Start with main menu scene */ - scene_manager_next_scene(app->scene_manager, TimedRemoteSceneMainMenu); + /* Start with file browser scene */ + scene_manager_next_scene(app->scene_manager, TimedRemoteSceneIrBrowse); /* Run event loop */ view_dispatcher_run(app->view_dispatcher);