// An intervalometer application written for the Flipper Zero // // author: nitepone #include "intervalometer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // app ui scenes enum flipvalo_ui_scene { FVSceneMain, FVSceneConfig, }; // defines a flipvalo camera trigger struct flipvalo_trigger { const char* display_name; int (*send)(void* output_config); }; enum flipvalo_trigger_variants { FvTrigMin = 0, FvTrigSony = 0, FvTrigCanon = 1, FvTrigNikon = 2, FvTrigPentax = 3, FvTrigMax = 3, }; // run config for intervalometer struct flipvalo_config { int init_delay_msec; // initial delay to start capture int interval_delay_msec; // time between shots int shot_count; // total shots in run int burst_count; // number of triggers in a shot int burst_delay_msec; // time between triggers in a shot int tickrate; // tick rate in "ticks per second" enum flipvalo_trigger_variants trigger; // current trigger void* output_config; }; // run time states for intervalometer enum flipvalo_state { FVDone = 0, // done, 0 so it is default if state struct is zeroed FVWaitInitShot, // waiting for first shot FVWaitContShot, // waiting between "bursts" or "shots" FVWaitBurst, // waiting between shots in a "burst" }; // run time data for intervalometer // (this can be safely cleared between runs of the intervalometer) struct flipvalo_run_state { enum flipvalo_state state; // current state of the run int tick_cur; // current tick count int tick_next; // tick when next action will occur int shot_cur; // current shot int burst_cur; // current trigger in a burst }; enum flipvalo_config_edit_lines { FvConfigEditInitDelay, FvConfigEditMIN = FvConfigEditInitDelay, FvConfigEditShotCount, FvConfigEditShotDelay, FvConfigEditBurstCount, FvConfigEditBurstDelay, FvConfigEditTrigger, FvConfigEditMAX = FvConfigEditTrigger, }; struct flipvalo_config_edit_view { // the `config` that is under edit struct flipvalo_config* config; // the `cur_index` of the selection // (e.g. editing the 3rd value of a number) int cur_index; // the `cur_line` of the selection enum flipvalo_config_edit_lines cur_line; // the current line that is at the top of the scrolled view enum flipvalo_config_edit_lines scroll_pos; // are we editing the selection? // (this is really only needed for number fields) bool edit_mode; }; // private data of app struct flipvalo_priv { struct flipvalo_config config; struct flipvalo_config_edit_view config_edit_view; struct flipvalo_run_state run_state; enum flipvalo_ui_scene ui_scene; int gui_shutter_blink; FuriTimer* timer; NotificationApp* notifications; FuriMutex* mutex; }; enum event_type { EventTypeTick, EventTypeKey, }; struct plugin_event { enum event_type type; InputEvent input; }; enum flipvalo_config_edit_line_type { FvConfigEditTypeTimer, FvConfigEditTypeCount, FvConfigEditTypeEnum, }; static void flipvalo_config_edit_view_init(struct flipvalo_config_edit_view* view) { view->config = NULL; view->cur_index = 0; view->cur_line = 0; view->scroll_pos = 0; view->edit_mode = false; } static int sony_ir_trigger_send(void* ctx) { UNUSED(ctx); InfraredMessage message = { .address = 0x1E3A, .command = 0x2D, .protocol = InfraredProtocolSIRC20, }; infrared_send(&message, 1); return 0; } uint32_t canon_ir_timings[] = {594, 7182, 593}; static int canon_ir_trigger_send(void* ctx) { UNUSED(ctx); infrared_send_raw_ext(canon_ir_timings, 3, true, 38000, 0.33); return 0; } uint32_t nikon_ir_timings[] = {1945, 28253, 404, 1513, 410, 3611, 460, 70144, 1974, 28213, 455, 1493, 461, 3591, 409}; static int nikon_ir_trigger_send(void* ctx) { UNUSED(ctx); infrared_send_raw_ext(nikon_ir_timings, 15, true, 38000, 0.33); return 0; } uint32_t pentax_ir_timings[] = { 13044, 3057, 965, 1023, 967, 1022, 968, 1023, 990, 1053, 966, 1023, 967, 1024, 989 }; static int pentax_ir_trigger_send(void* ctx) { UNUSED(ctx); infrared_send_raw_ext(pentax_ir_timings, 15, true, 38000, 0.33); return 0; } struct flipvalo_trigger sony_ir_trigger = { .send = sony_ir_trigger_send, .display_name = "Sony IR" }; struct flipvalo_trigger canon_ir_trigger = { .send = canon_ir_trigger_send, .display_name = "Canon IR"}; struct flipvalo_trigger nikon_ir_trigger = { .send = nikon_ir_trigger_send, .display_name = "Nikon IR"}; struct flipvalo_trigger pentax_ir_trigger = { .send = pentax_ir_trigger_send, .display_name = "Pentax IR" }; static struct flipvalo_trigger* flipvalo_get_trigger( enum flipvalo_trigger_variants variant ) { switch (variant) { case FvTrigSony: return &sony_ir_trigger; case FvTrigCanon: return &canon_ir_trigger; case FvTrigNikon: return &nikon_ir_trigger; case FvTrigPentax: return &pentax_ir_trigger; } return NULL; } #define ITEM_H 64 / 3 #define ITEM_W 128 #define VALUE_X 100 #define VALUE_W 45 static void flipvalo_config_edit_draw(Canvas* canvas, struct flipvalo_config_edit_view* view) { int* line_value; char* line_label = NULL; const char* line_disp_str = ""; FuriString* temp_str = furi_string_alloc(); enum flipvalo_config_edit_line_type line_type; enum flipvalo_config_edit_lines selected_line; for(size_t line = 0; line < 3; line++) { selected_line = view->scroll_pos + line; switch(selected_line) { case FvConfigEditInitDelay: line_value = &view->config->init_delay_msec; line_type = FvConfigEditTypeTimer; line_label = "Init Time"; break; case FvConfigEditShotDelay: line_value = &view->config->interval_delay_msec; line_type = FvConfigEditTypeTimer; line_label = "Seq Time"; break; case FvConfigEditShotCount: line_value = &view->config->shot_count; line_type = FvConfigEditTypeCount; line_label = "Seq Count"; break; case FvConfigEditBurstDelay: line_value = &view->config->burst_delay_msec; line_type = FvConfigEditTypeTimer; line_label = "Brst Time"; break; case FvConfigEditBurstCount: line_value = &view->config->burst_count; line_type = FvConfigEditTypeCount; line_label = "Brst Count"; break; case FvConfigEditTrigger: line_value = NULL; line_type = FvConfigEditTypeEnum; line_label = "Trig Type"; line_disp_str = flipvalo_get_trigger(view->config->trigger)->display_name; break; default: continue; }; canvas_set_color(canvas, ColorBlack); if((selected_line) == view->cur_line) { elements_slightly_rounded_box(canvas, 0, ITEM_H * line + 1, ITEM_W, ITEM_H - 1); canvas_set_color(canvas, ColorWhite); } uint8_t text_y = ITEM_H * line + ITEM_H / 2 + 2; canvas_draw_str_aligned(canvas, 6, text_y, AlignLeft, AlignCenter, line_label); switch(line_type) { case FvConfigEditTypeTimer: furi_string_printf( temp_str, "%02d:%02d:%02d:%03d", *line_value / 3600000, (*line_value / 60000) % 60, (*line_value / 1000) % 60, *line_value % 1000); canvas_set_font(canvas, FontKeyboard); canvas_draw_str_aligned( canvas, 124, text_y, AlignRight, AlignCenter, furi_string_get_cstr(temp_str)); canvas_set_font(canvas, FontSecondary); if(view->edit_mode && view->cur_line == selected_line) { switch(view->cur_index) { case 0: canvas_draw_icon(canvas, 117, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 117, text_y + 5, &I_ArrowDown_3x5); canvas_draw_icon(canvas, 112, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 112, text_y + 5, &I_ArrowDown_3x5); canvas_draw_icon(canvas, 107, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 107, text_y + 5, &I_ArrowDown_3x5); break; case 1: canvas_draw_icon(canvas, 93, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 93, text_y + 5, &I_ArrowDown_3x5); canvas_draw_icon(canvas, 89, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 89, text_y + 5, &I_ArrowDown_3x5); break; case 2: canvas_draw_icon(canvas, 75, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 75, text_y + 5, &I_ArrowDown_3x5); canvas_draw_icon(canvas, 71, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 71, text_y + 5, &I_ArrowDown_3x5); break; case 3: canvas_draw_icon(canvas, 57, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 57, text_y + 5, &I_ArrowDown_3x5); canvas_draw_icon(canvas, 53, text_y - 9, &I_ArrowUp_3x5); canvas_draw_icon(canvas, 53, text_y + 5, &I_ArrowDown_3x5); break; } } break; case FvConfigEditTypeCount: furi_string_printf(temp_str, "%d", *line_value); canvas_draw_str_aligned( canvas, VALUE_X, text_y, AlignCenter, AlignCenter, furi_string_get_cstr(temp_str)); // TODO(luna) 0 values are actually more special for shot count and burst count. // former being infinite, latter being uh.. nothing? not allowed?.. review this logic later. if(*line_value > 0) { canvas_draw_str_aligned( canvas, VALUE_X - VALUE_W / 2, text_y, AlignCenter, AlignCenter, "<"); } canvas_draw_str_aligned( canvas, VALUE_X + VALUE_W / 2, text_y, AlignCenter, AlignCenter, ">"); break; case FvConfigEditTypeEnum: furi_string_printf(temp_str, "%s", line_disp_str); canvas_draw_str_aligned( canvas, VALUE_X, text_y, AlignCenter, AlignCenter, furi_string_get_cstr(temp_str)); canvas_draw_str_aligned( canvas, VALUE_X - VALUE_W / 2, text_y, AlignCenter, AlignCenter, "<"); canvas_draw_str_aligned( canvas, VALUE_X + VALUE_W / 2, text_y, AlignCenter, AlignCenter, ">"); break; } } furi_string_free(temp_str); } static void flipvalo_config_edit_input_move_cursor(struct flipvalo_config_edit_view* view, int dx, int dy) { enum flipvalo_config_edit_lines new_line = 0; int* line_value = NULL; enum flipvalo_config_edit_line_type line_type; // only used for enum type int max_value; int min_value; switch(view->cur_line) { case FvConfigEditInitDelay: line_value = &view->config->init_delay_msec; line_type = FvConfigEditTypeTimer; break; case FvConfigEditShotDelay: line_value = &view->config->interval_delay_msec; line_type = FvConfigEditTypeTimer; break; case FvConfigEditShotCount: line_value = &view->config->shot_count; line_type = FvConfigEditTypeCount; break; case FvConfigEditBurstDelay: line_value = &view->config->burst_delay_msec; line_type = FvConfigEditTypeTimer; break; case FvConfigEditBurstCount: line_value = &view->config->burst_count; line_type = FvConfigEditTypeCount; break; case FvConfigEditTrigger: line_value = (int*)(&view->config->trigger); line_type = FvConfigEditTypeEnum; min_value = FvTrigMin; max_value = FvTrigMax; break; default: return; }; if(!view->edit_mode) { // Do `dy` behaviors new_line = view->cur_line + dy; if(new_line > FvConfigEditMAX) { // Out of bound cursor. No-op. return; } view->cur_line = new_line; // Handle moving scroll position. if(new_line < view->scroll_pos) { view->scroll_pos = new_line; } else if(new_line >= (view->scroll_pos + 3)) { view->scroll_pos += dy; } // Do `dx` behavior switch(line_type) { case FvConfigEditTypeTimer: // no-op unless edit mode break; case FvConfigEditTypeCount: min_value = 0; max_value = INT_MAX; // fall through. case FvConfigEditTypeEnum: if((*line_value + dx) >= min_value && (*line_value + dx) <= max_value) { *line_value += dx; } break; } } else /* edit mode */ { switch(line_type) { case FvConfigEditTypeCount: case FvConfigEditTypeEnum: // If current line does not edit mode.. why are we in edit mode? // Reaching this would be a bug, so lets go back to normal mode. view->edit_mode = false; return; case FvConfigEditTypeTimer: switch(view->cur_index) { case 0: if(*line_value + (dy * -10) >= 0) { *line_value += (dy * -10); } break; case 1: if(*line_value + (dy * -1000) >= 0) { *line_value += (dy * -1000); } break; case 2: if(*line_value + (dy * -60000) >= 0) { *line_value += (dy * -60000); } break; case 3: if(*line_value + (dy * -3600000) >= 0) { *line_value += (dy * -3600000); } break; } view->cur_index -= dx; if(view->cur_index < 0) { view->cur_index = 0; } if(view->cur_index > 3) { view->cur_index = 3; } break; } } } static int flipvalo_config_edit_input(InputEvent* event, struct flipvalo_config_edit_view* view) { // ignore all but short and repeats if(!(event->type == InputTypeShort || event->type == InputTypeRepeat)) { return 0; } switch(event->key) { case InputKeyRight: flipvalo_config_edit_input_move_cursor(view, 1, 0); break; case InputKeyLeft: flipvalo_config_edit_input_move_cursor(view, -1, 0); break; case InputKeyUp: flipvalo_config_edit_input_move_cursor(view, 0, -1); break; case InputKeyDown: flipvalo_config_edit_input_move_cursor(view, 0, 1); break; case InputKeyOk: //TODO(luna) Check if line supports edit mode before doing this. view->edit_mode = !view->edit_mode; break; case InputKeyBack: if(view->edit_mode) { view->edit_mode = false; } else { // exit config edit view return 1; } default: break; } return 0; } // XXX(luna) back to app static void flipvalo_run_state_init(struct flipvalo_run_state* fv_run_state) { fv_run_state->burst_cur = 1; fv_run_state->shot_cur = 1; fv_run_state->tick_next = 0; fv_run_state->state = FVDone; fv_run_state->tick_next = 0; fv_run_state->tick_cur = 0; } static void input_callback(InputEvent* input_event, void* event_queue_void) { FuriMessageQueue* event_queue = (FuriMessageQueue*)event_queue_void; furi_assert(event_queue); struct plugin_event event = {.type = EventTypeKey, .input = *input_event}; furi_message_queue_put(event_queue, &event, FuriWaitForever); } static inline bool flipvalo_intv_running(struct flipvalo_priv* fv_priv) { return fv_priv->run_state.state != FVDone; } static void flipvalo_intv_tick(struct flipvalo_priv* fv_priv) { struct flipvalo_config* conf = &fv_priv->config; struct flipvalo_run_state* run = &fv_priv->run_state; // check if action required if(run->tick_cur++ >= run->tick_next) { // call trigger function flipvalo_get_trigger(conf->trigger)->send(conf->output_config); fv_priv->gui_shutter_blink = 3; // end of burst, prepare next shot if(run->burst_cur >= conf->burst_count) { run->burst_cur = 1; run->shot_cur++; run->state = FVWaitContShot; run->tick_next = run->tick_cur + ((conf->interval_delay_msec * conf->tickrate) / 1000); } else /*continue burst */ { run->burst_cur++; run->state = FVWaitBurst; run->tick_next = run->tick_cur + ((conf->burst_delay_msec * conf->tickrate) / 1000); } } if(run->shot_cur > conf->shot_count) { run->state = FVDone; } } static void flipvalo_intv_stop(struct flipvalo_priv* fv_priv) { fv_priv->run_state.state = FVDone; } static void flipvalo_intv_start(struct flipvalo_priv* fv_priv) { // clear struct furi_assert(fv_priv); flipvalo_run_state_init(&fv_priv->run_state); fv_priv->run_state.state = FVWaitInitShot; fv_priv->run_state.tick_next = ((fv_priv->config.init_delay_msec * fv_priv->config.tickrate) / 1000); } static void timer_callback(void* ctx) { furi_assert(ctx); struct flipvalo_priv* fv_priv = ctx; furi_mutex_acquire(fv_priv->mutex, FuriWaitForever); if(flipvalo_intv_running(fv_priv)) { flipvalo_intv_tick(fv_priv); } furi_mutex_release(fv_priv->mutex); } static void render_callback(Canvas* const canvas, void* ctx) { furi_assert(ctx); struct flipvalo_priv* fv_priv = ctx; FuriString* temp_str = furi_string_alloc(); furi_mutex_acquire(fv_priv->mutex, FuriWaitForever); // invert screen if blinking if(fv_priv->gui_shutter_blink > 0) { fv_priv->gui_shutter_blink--; canvas_draw_box(canvas, 0, 0, 127, 63); canvas_set_color(canvas, ColorWhite); } if(fv_priv->ui_scene == FVSceneMain) { int countdown_msec = (1000 * (fv_priv->run_state.tick_next - fv_priv->run_state.tick_cur)) / fv_priv->config.tickrate; int elapsed_msec = (1000 * fv_priv->run_state.tick_cur) / fv_priv->config.tickrate; canvas_draw_frame(canvas, 0, 0, 128, 64); // draw countdown canvas_set_font(canvas, FontPrimary); furi_string_printf( temp_str, "%02d:%02d:%02d:%03d", countdown_msec / 3600000, (countdown_msec / 60000) % 60, (countdown_msec / 1000) % 60, countdown_msec % 1000); canvas_draw_str_aligned( canvas, 64, 24, AlignCenter, AlignCenter, furi_string_get_cstr(temp_str)); // draw top and bottom status bars canvas_set_font(canvas, FontSecondary); furi_string_printf( temp_str, "%02d:%02d:%02d", elapsed_msec / 3600000, (elapsed_msec / 60000) % 60, (elapsed_msec / 1000) % 60); canvas_draw_str_aligned( canvas, 4, 8, AlignLeft, AlignCenter, furi_string_get_cstr(temp_str)); furi_string_printf(temp_str, "Shot: %d", fv_priv->run_state.shot_cur); canvas_draw_str_aligned( canvas, 124, 8, AlignRight, AlignCenter, furi_string_get_cstr(temp_str)); elements_button_left(canvas, "Cfg"); elements_button_right(canvas, "Snap"); if(fv_priv->run_state.state == FVDone) { elements_button_center(canvas, "Start"); } else { elements_button_center(canvas, "Stop "); } } else if(fv_priv->ui_scene == FVSceneConfig) { flipvalo_config_edit_draw(canvas, &fv_priv->config_edit_view); } furi_string_free(temp_str); furi_mutex_release(fv_priv->mutex); } static void flipvalo_config_init(struct flipvalo_config* fv_conf) { fv_conf->init_delay_msec = 2000; fv_conf->interval_delay_msec = 0; fv_conf->shot_count = 1; fv_conf->burst_count = 1; fv_conf->burst_delay_msec = 0; fv_conf->tickrate = 125; fv_conf->trigger = FvTrigSony; fv_conf->output_config = NULL; } static void flipvalo_priv_init(struct flipvalo_priv* fv_priv) { flipvalo_config_init(&fv_priv->config); flipvalo_config_edit_view_init(&fv_priv->config_edit_view); flipvalo_run_state_init(&fv_priv->run_state); fv_priv->gui_shutter_blink = 0; fv_priv->timer = NULL; fv_priv->notifications = NULL; fv_priv->mutex = furi_mutex_alloc(FuriMutexTypeNormal); fv_priv->gui_shutter_blink = 0; } int32_t flipvalo_app() { int ret = 0; ViewPort* view_port = NULL; Gui* gui = NULL; FuriStatus event_status = {0}; struct plugin_event event = {0}; FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(struct plugin_event)); struct flipvalo_priv* fv_priv = malloc(sizeof(*fv_priv)); flipvalo_priv_init(fv_priv); bool otg_was_enabled = furi_hal_power_is_otg_enabled(); InfraredSettings settings = {0}; infrared_settings_load(&settings); if(settings.tx_pin < FuriHalInfraredTxPinMax) { furi_hal_infrared_set_tx_output(settings.tx_pin); if(settings.otg_enabled != otg_was_enabled) { if(settings.otg_enabled) { furi_hal_power_enable_otg(); } else { furi_hal_power_disable_otg(); } } } else { FuriHalInfraredTxPin tx_pin_detected = furi_hal_infrared_detect_tx_output(); furi_hal_infrared_set_tx_output(tx_pin_detected); if(tx_pin_detected != FuriHalInfraredTxPinInternal) { furi_hal_power_enable_otg(); } } if(!fv_priv->mutex) { FURI_LOG_E("Flipvalo", "Cannot create mutex\r\n"); ret = 1; goto cleanup; } view_port = view_port_alloc(); view_port_draw_callback_set(view_port, render_callback, fv_priv); view_port_input_callback_set(view_port, input_callback, event_queue); fv_priv->timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, fv_priv); furi_timer_start( fv_priv->timer, (uint32_t)furi_kernel_get_tick_frequency() / fv_priv->config.tickrate); gui = furi_record_open("gui"); gui_add_view_port(gui, view_port, GuiLayerFullscreen); while(true) { event_status = furi_message_queue_get(event_queue, &event, 100); furi_mutex_acquire(fv_priv->mutex, FuriWaitForever); // catch event_status that is not Ok if(event_status == FuriStatusErrorTimeout) { // timeout, ignore goto next_event; } else if(event_status != FuriStatusOk) { FURI_LOG_E("Flipvalo", "Event Queue Error: %d\r\n", event_status); goto next_event; // TODO(luna) evaluate if we should exit here. //goto cleanup; } // handle input if(/* long press back */ event.type == EventTypeKey && event.input.type == InputTypeLong && event.input.key == InputKeyBack) { goto cleanup; } switch(fv_priv->ui_scene) { case FVSceneMain: // TODO(luna) Maybe give this a function.. look howl clean FVSceneConfig is... if(event.type == EventTypeKey) { if(event.input.type == InputTypeShort || event.input.type == InputTypeLong) { switch(event.input.key) { case InputKeyUp: break; case InputKeyDown: break; case InputKeyLeft: flipvalo_intv_stop(fv_priv); fv_priv->config_edit_view.config = &fv_priv->config; fv_priv->ui_scene = FVSceneConfig; break; case InputKeyRight: fv_priv->gui_shutter_blink = 3; flipvalo_get_trigger(fv_priv->config.trigger) ->send(fv_priv->config.output_config); break; case InputKeyOk: if(flipvalo_intv_running(fv_priv)) { flipvalo_intv_stop(fv_priv); } else { flipvalo_intv_start(fv_priv); } break; case InputKeyMAX: break; case InputKeyBack: break; } } } break; case FVSceneConfig: ret = flipvalo_config_edit_input(&event.input, &fv_priv->config_edit_view); if(ret) { fv_priv->ui_scene = FVSceneMain; } break; } next_event: furi_mutex_release(fv_priv->mutex); view_port_update(view_port); } cleanup: if(view_port) { view_port_enabled_set(view_port, false); if(gui) { gui_remove_view_port(gui, view_port); furi_record_close("gui"); } view_port_free(view_port); } furi_hal_infrared_set_tx_output(FuriHalInfraredTxPinInternal); if(furi_hal_power_is_otg_enabled() != otg_was_enabled) { if(otg_was_enabled) { furi_hal_power_enable_otg(); } else { furi_hal_power_disable_otg(); } } if(event_queue) { furi_message_queue_free(event_queue); } if(fv_priv) { furi_mutex_free(fv_priv->mutex); furi_timer_free(fv_priv->timer); } free(fv_priv); return ret; }