Просмотр исходного кода

Add ir_intervalometer from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: ir_intervalometer
git-subtree-mainline: e835dec7262f1da7fce24a110b2a61e629a86ce0
git-subtree-split: 4051071e5881f446a3b2db73648682115fd90874
Willy-JL 1 год назад
Родитель
Сommit
e9e7b49393

+ 34 - 0
ir_intervalometer/.github/workflows/build.yml

@@ -0,0 +1,34 @@
+name: "FAP Build"
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+  schedule:
+    # do a build every day
+    - cron: "1 1 * * *"
+jobs:
+  ufbt-build-action:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - name: dev channel
+            sdk-channel: dev
+          - name: release channel
+            sdk-channel: release
+    name: 'ufbt: Build for ${{ matrix.name }}'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1.2
+        id: build-app
+        with:
+          sdk-channel: ${{ matrix.sdk-channel }}
+          sdk-index-url: ${{ matrix.sdk-index-url }}
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }}
+          path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 1 - 0
ir_intervalometer/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev apps_source_code/ir_intervalometer

+ 40 - 0
ir_intervalometer/README.md

@@ -0,0 +1,40 @@
+# An Intervalometer App For Flipper Zero
+
+![](./doc/img/app-icon.png)
+
+![](./doc/img/running-intervalometer.png)
+
+This is a simple configurable valometer app for Sony cameras. It provides a
+configurable shutter timer and uses the IR blaster to trigger the camera
+shutter.
+
+## Configuration
+
+![](./doc/img/settings-initial.png)
+
+The default configuration functions just as a 2 second self timer.
+
+![](./doc/img/settings-edit.png)
+
+This can be easily configured to do something like a 12 shot timelapse over 1 hour.
+
+![](./doc/img/settings-burst.png)
+
+Scrolling down the settings page, there is also the option to shoot a "burst"
+of images at each interval.
+
+# Building
+
+1. Download the Flipper firmware repo at: https://github.com/flipperdevices/flipperzero-firmware
+2. Inside the Flipper fw repo, copy this repo into `./applications_user/intervalometer/`
+3. Run `./fbt fap_intervalometer`
+4. Read the output of `fbt` to find the `.fap` in `./build/`
+
+# TODO
+
+1. Add support for additional trigger types, such as bluetooth and other cameras.
+2. Add support for using more advanced camera controls like exposure compensation
+   to allow for shooting an exposure bracket.
+3. Add github actions for CI.
+3. Refactor the codebase to be cute.
+

+ 16 - 0
ir_intervalometer/application.fam

@@ -0,0 +1,16 @@
+App(
+    appid="ir_intervalometer",
+    name="[IR] Intervalometer",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="flipvalo_app",
+    requires=["gui"],
+    stack_size=2 * 1024,
+    order=20,
+    fap_icon="intervalometer_10x.png",
+    fap_icon_assets="icons",
+    fap_category="Infrared",
+    fap_author="@Nitepone",
+    fap_weburl="https://github.com/Nitepone/flipper-intervalometer",
+    fap_version="1.1",
+    fap_description="This is a simple configurable valometer app for DSLR cameras. Works via Infrared port.",
+)

BIN
ir_intervalometer/doc/img/app-icon.png


BIN
ir_intervalometer/doc/img/running-intervalometer.png


BIN
ir_intervalometer/doc/img/settings-burst.png


BIN
ir_intervalometer/doc/img/settings-edit.png


BIN
ir_intervalometer/doc/img/settings-initial.png


BIN
ir_intervalometer/icons/ArrowDown_3x5.png


BIN
ir_intervalometer/icons/ArrowUp_3x5.png


BIN
ir_intervalometer/img/1.png


BIN
ir_intervalometer/img/2.png


+ 736 - 0
ir_intervalometer/intervalometer.c

@@ -0,0 +1,736 @@
+// An intervalometer application written for the Flipper Zero
+//
+// author: nitepone <sierra>
+
+#include "intervalometer.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <core/string.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <gui/icon.h>
+#include <infrared_transmit.h>
+
+#include <input/input.h>
+
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+
+#include <ir_intervalometer_icons.h>
+
+// 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,
+    FvTrigMax = 2,
+};
+
+// 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;
+}
+
+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"};
+
+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;
+    }
+    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, FuriMessageQueue* event_queue) {
+    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);
+
+    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);
+    }
+    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;
+}

+ 4 - 0
ir_intervalometer/intervalometer.h

@@ -0,0 +1,4 @@
+#ifndef INTERVALOMETER_H
+#define INTERVALOMETER_H
+
+#endif /* !INTERVALOMETER_H */

BIN
ir_intervalometer/intervalometer_10x.png