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

Initial fork and first working implementation. Much more to do yet.

Cody Tolene 2 лет назад
Родитель
Сommit
728163b5ea
35 измененных файлов с 2266 добавлено и 2 удалено
  1. BIN
      .github/images/flipper-dev.png
  2. 36 0
      .github/images/flipper-zero-buy-now.svg
  3. 41 0
      .github/images/flipper-zero-docs.svg
  4. 77 2
      README.md
  5. 16 0
      src-fap/application.fam
  6. 129 0
      src-fap/camera-suite.c
  7. 63 0
      src-fap/camera-suite.h
  8. 74 0
      src-fap/helpers/camera_suite_custom_event.h
  9. 35 0
      src-fap/helpers/camera_suite_haptic.c
  10. 7 0
      src-fap/helpers/camera_suite_haptic.h
  11. 38 0
      src-fap/helpers/camera_suite_led.c
  12. 3 0
      src-fap/helpers/camera_suite_led.h
  13. 26 0
      src-fap/helpers/camera_suite_speaker.c
  14. 5 0
      src-fap/helpers/camera_suite_speaker.h
  15. 109 0
      src-fap/helpers/camera_suite_storage.c
  16. 19 0
      src-fap/helpers/camera_suite_storage.h
  17. BIN
      src-fap/icons/camera-suite.png
  18. 30 0
      src-fap/scenes/camera_suite_scene.c
  19. 29 0
      src-fap/scenes/camera_suite_scene.h
  20. 6 0
      src-fap/scenes/camera_suite_scene_config.h
  21. 51 0
      src-fap/scenes/camera_suite_scene_guide.c
  22. 86 0
      src-fap/scenes/camera_suite_scene_menu.c
  23. 103 0
      src-fap/scenes/camera_suite_scene_settings.c
  24. 55 0
      src-fap/scenes/camera_suite_scene_start.c
  25. 52 0
      src-fap/scenes/camera_suite_scene_style_1.c
  26. 54 0
      src-fap/scenes/camera_suite_scene_style_2.c
  27. 119 0
      src-fap/views/camera_suite_view_guide.c
  28. 19 0
      src-fap/views/camera_suite_view_guide.h
  29. 126 0
      src-fap/views/camera_suite_view_start.c
  30. 19 0
      src-fap/views/camera_suite_view_start.h
  31. 302 0
      src-fap/views/camera_suite_view_style_1.c
  32. 86 0
      src-fap/views/camera_suite_view_style_1.h
  33. 249 0
      src-fap/views/camera_suite_view_style_2.c
  34. 19 0
      src-fap/views/camera_suite_view_style_2.h
  35. 183 0
      src-firmware/esp32_cam_uart_stream.ino

BIN
.github/images/flipper-dev.png


+ 36 - 0
.github/images/flipper-zero-buy-now.svg

@@ -0,0 +1,36 @@
+<svg 
+  aria-label="BUY NOW: FLIPPER ZERO"
+  height="28" 
+  role="img" 
+  width="201.75" 
+  xmlns:xlink="http://www.w3.org/1999/xlink" 
+  xmlns="http://www.w3.org/2000/svg" 
+>
+  <title>BUY NOW: FLIPPER ZERO</title>
+  <g shape-rendering="crispEdges">
+    <rect width="81.75" height="28" fill="#555"/>
+    <rect x="81.75" width="120" height="28" fill="#ee6e00"/>
+  </g>
+  <g 
+    fill="#fff" 
+    font-family="Verdana,Geneva,DejaVu Sans,sans-serif" 
+    font-size="100"
+    text-anchor="middle" 
+    text-rendering="geometricPrecision" 
+  >
+    <text 
+      fill="#fff"
+      textLength="577.5" 
+      transform="scale(.1)" 
+      x="408.75" 
+      y="175" 
+    >BUY NOW</text>
+    <text 
+      fill="#fff" 
+      font-weight="bold"
+      textLength="960" 
+      transform="scale(.1)" 
+      x="1417.5" y="175" 
+    >FLIPPER ZERO</text>
+  </g>
+</svg>

+ 41 - 0
.github/images/flipper-zero-docs.svg

@@ -0,0 +1,41 @@
+<svg 
+  aria-label="DOCS: FLIPPER ZERO"
+  height="28" 
+  role="img" 
+  width="178" 
+  xmlns:xlink="http://www.w3.org/1999/xlink" 
+  xmlns="http://www.w3.org/2000/svg" 
+>
+  <title>DOCS: FLIPPER ZERO</title>
+  <g shape-rendering="crispEdges">
+    <rect width="58" height="28" fill="#555"/>
+    <rect x="58" width="120" height="28" fill="#ee6e00"/>
+  </g>
+  <g 
+    fill="#fff" 
+    font-family="Verdana,Geneva,DejaVu Sans,sans-serif" 
+    font-size="100"
+    text-anchor="middle" 
+    text-rendering="geometricPrecision" 
+  >
+    <text 
+      fill="#fff"
+      textLength="340" 
+      transform="scale(.1)" 
+      x="290" 
+      y="175" 
+    >
+      DOCS
+    </text>
+    <text 
+      fill="#fff" 
+      font-weight="bold"
+      textLength="960" 
+      transform="scale(.1)" 
+      x="1180" 
+      y="175" 
+    >
+      FLIPPER ZERO
+    </text>
+  </g>
+</svg>

+ 77 - 2
README.md

@@ -1,2 +1,77 @@
-# Flipper-Zero-Camera-Suite
-Firmware and software to run an ESP32-CAM module on your Flipper Zero device.
+<div align="center">
+  <img align="center" src=".github/images/flipper-dev.png" />
+  <h2 align="center">Flipper Zero - Camera Suite</h2>
+  <p align="center">
+    Firmware and software to run an ESP32-CAM module on your Flipper Zero device.
+  </p>
+  <a href="https://shop.flipperzero.one/">
+    <img src=".github/images/flipper-zero-buy-now.svg" />
+  </a>
+  <a href="https://docs.flipperzero.one/">
+    <img src=".github/images/flipper-zero-docs.svg" />
+  </a>
+</div>
+
+---
+
+## Table of Contents <a name="index"></a>
+
+- [Hardware Requirements](#hardware-requirements)
+- [Hardware Installation](#hardware-installation)
+- [Firmware Installation](#firmware-installation)
+- [Software Installation](#software-installation)
+- [Software Guide](#software-guide)
+- [Attributions](#attributions)
+
+## Hardware Requirements <a name="hardware-requirements"></a>
+
+Requires an ESP32-CAM module. Here's a few on Amazon that I've personally used:
+
+- https://amzn.to/3NCoQUq
+
+- https://amzn.to/46IuAF9
+
+<p align="right">[ <a href="#index">Back to top</a> ]</p>
+
+## Hardware Installation <a name="hardware-installation"></a>
+
+This section is coming soon...
+
+<p align="right">[ <a href="#index">Back to top</a> ]</p>
+
+## Firmware Installation <a name="firmware-installation"></a>
+
+This section is coming soon...
+
+<p align="right">[ <a href="#index">Back to top</a> ]</p>
+
+## Software Installation <a name="software-installation"></a>
+
+This section is coming soon...
+
+<p align="right">[ <a href="#index">Back to top</a> ]</p>
+
+## Software Guide <a name="software-guide"></a>
+
+This section is coming soon...
+
+<p align="right">[ <a href="#index">Back to top</a> ]</p>
+
+## Attributions <a name="attributions"></a>
+
+This project is based on/forked from the [Fliper Zero Camera Application][flipperzero-camera]
+by [Z4urce][z4urce] combined with the [Flipper Zero Boilerplate Application][boilerplate-app]
+by [Dave Lee][leedave].
+
+<p align="right">[ <a href="#index">Back to top</a> ]</p>
+
+Fin. Thanks for looking and happy programming friend!
+
+Cody
+
+<!-- LINKS -->
+
+[flipper-zero-fap-boilerplate]: https://github.com/leedave/flipper-zero-fap-boilerplate
+[flipperzero-camera]: https://github.com/Z4urce/flipperzero-camera
+[github-profile-z4urce]: https://github.com/Z4urce
+[github-profile-leedave]: https://github.com/leedave

+ 16 - 0
src-fap/application.fam

@@ -0,0 +1,16 @@
+App(
+    appid="camerasuite",
+    apptype=FlipperAppType.EXTERNAL,
+    cdefines=["APP_CAMERA_SUITE"],
+    entry_point="camera_suite_app",
+	fap_author="Cody Tolene",
+    fap_category="GPIO",
+	fap_description="A camera suite application for the Flipper Zero ESP32-CAM module.",
+	fap_icon="icons/camera-suite.png",
+    fap_libs=["assets"],
+	fap_weburl="https://github.com/CodyTolene/Flipper-Zero-Cam",
+    name="[ESP32] Camera Suite",
+    order=1,
+    requires=["gui", "storage"],
+    stack_size=8 * 1024
+)

+ 129 - 0
src-fap/camera-suite.c

@@ -0,0 +1,129 @@
+#include "camera-suite.h"
+
+bool camera_suite_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+void camera_suite_tick_event_callback(void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+//leave app if back button pressed
+bool camera_suite_navigation_event_callback(void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+CameraSuite* camera_suite_app_alloc() {
+    CameraSuite* app = malloc(sizeof(CameraSuite));
+    app->gui = furi_record_open(RECORD_GUI);
+    app->notification = furi_record_open(RECORD_NOTIFICATION);
+
+    //Turn backlight on, believe me this makes testing your app easier
+    notification_message(app->notification, &sequence_display_backlight_on);
+
+    //Scene additions
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    app->scene_manager = scene_manager_alloc(&camera_suite_scene_handlers, app);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, camera_suite_navigation_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, camera_suite_tick_event_callback, 100);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, camera_suite_custom_event_callback);
+    app->submenu = submenu_alloc();
+
+    // Set defaults, in case no config loaded
+    app->haptic = 1;
+    app->speaker = 1;
+    app->led = 1;
+
+    // Load configs
+    camera_suite_read_settings(app);
+
+    view_dispatcher_add_view(
+        app->view_dispatcher, CameraSuiteViewIdMenu, submenu_get_view(app->submenu));
+
+    app->camera_suite_view_start = camera_suite_view_start_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CameraSuiteViewIdStartscreen,
+        camera_suite_view_start_get_view(app->camera_suite_view_start));
+
+    app->camera_suite_view_style_1 = camera_suite_view_style_1_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CameraSuiteViewIdScene1,
+        camera_suite_view_style_1_get_view(app->camera_suite_view_style_1));
+
+    app->camera_suite_view_style_2 = camera_suite_view_style_2_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CameraSuiteViewIdScene2,
+        camera_suite_view_style_2_get_view(app->camera_suite_view_style_2));
+
+    app->camera_suite_view_guide = camera_suite_view_guide_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CameraSuiteViewIdGuide,
+        camera_suite_view_guide_get_view(app->camera_suite_view_guide));
+
+    app->button_menu = button_menu_alloc();
+
+    app->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CameraSuiteViewIdSettings,
+        variable_item_list_get_view(app->variable_item_list));
+
+    //End Scene Additions
+
+    return app;
+}
+
+void camera_suite_app_free(CameraSuite* app) {
+    furi_assert(app);
+
+    // Scene manager
+    scene_manager_free(app->scene_manager);
+
+    // View Dispatcher
+    view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdScene1);
+    view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdScene2);
+    view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdGuide);
+    view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdSettings);
+    submenu_free(app->submenu);
+
+    view_dispatcher_free(app->view_dispatcher);
+    furi_record_close(RECORD_GUI);
+
+    app->gui = NULL;
+    app->notification = NULL;
+
+    //Remove whatever is left
+    free(app);
+}
+
+/** Main entry point for initialization. */
+int32_t camera_suite_app(void* p) {
+    UNUSED(p);
+    CameraSuite* app = camera_suite_app_alloc();
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+    // Init with start scene.
+    scene_manager_next_scene(app->scene_manager, CameraSuiteSceneStart);
+    furi_hal_power_suppress_charge_enter();
+    view_dispatcher_run(app->view_dispatcher);
+    camera_suite_save_settings(app);
+    furi_hal_power_suppress_charge_exit();
+    camera_suite_app_free(app);
+    return 0;
+}

+ 63 - 0
src-fap/camera-suite.h

@@ -0,0 +1,63 @@
+#pragma once
+
+#include "helpers/camera_suite_storage.h"
+#include "scenes/camera_suite_scene.h"
+#include "views/camera_suite_view_guide.h"
+#include "views/camera_suite_view_start.h"
+#include "views/camera_suite_view_style_1.h"
+#include "views/camera_suite_view_style_2.h"
+#include <assets_icons.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/modules/button_menu.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/scene_manager.h>
+#include <gui/view_dispatcher.h>
+#include <input/input.h>
+#include <notification/notification_messages.h>
+#include <stdlib.h>
+
+#define TAG "Camera Suite"
+
+typedef struct {
+    Gui* gui;
+    NotificationApp* notification;
+    ViewDispatcher* view_dispatcher;
+    Submenu* submenu;
+    SceneManager* scene_manager;
+    VariableItemList* variable_item_list;
+    CameraSuiteViewStart* camera_suite_view_start;
+    CameraSuiteViewStyle1* camera_suite_view_style_1;
+    CameraSuiteViewStyle2* camera_suite_view_style_2;
+    CameraSuiteViewGuide* camera_suite_view_guide;
+    uint32_t haptic;
+    uint32_t speaker;
+    uint32_t led;
+    ButtonMenu* button_menu;
+} CameraSuite;
+
+typedef enum {
+    CameraSuiteViewIdStartscreen,
+    CameraSuiteViewIdMenu,
+    CameraSuiteViewIdScene1,
+    CameraSuiteViewIdScene2,
+    CameraSuiteViewIdGuide,
+    CameraSuiteViewIdSettings,
+} CameraSuiteViewId;
+
+typedef enum {
+    CameraSuiteHapticOff,
+    CameraSuiteHapticOn,
+} CameraSuiteHapticState;
+
+typedef enum {
+    CameraSuiteSpeakerOff,
+    CameraSuiteSpeakerOn,
+} CameraSuiteSpeakerState;
+
+typedef enum {
+    CameraSuiteLedOff,
+    CameraSuiteLedOn,
+} CameraSuiteLedState;

+ 74 - 0
src-fap/helpers/camera_suite_custom_event.h

@@ -0,0 +1,74 @@
+#pragma once
+
+typedef enum {
+    // Scene events: Start menu
+    CameraSuiteCustomEventStartUp,
+    CameraSuiteCustomEventStartDown,
+    CameraSuiteCustomEventStartLeft,
+    CameraSuiteCustomEventStartRight,
+    CameraSuiteCustomEventStartOk,
+    CameraSuiteCustomEventStartBack,
+    // Scene events: Camera style 1
+    CameraSuiteCustomEventSceneStyle1Up,
+    CameraSuiteCustomEventSceneStyle1Down,
+    CameraSuiteCustomEventSceneStyle1Left,
+    CameraSuiteCustomEventSceneStyle1Right,
+    CameraSuiteCustomEventSceneStyle1Ok,
+    CameraSuiteCustomEventSceneStyle1Back,
+    // Scene events: Camera style 2
+    CameraSuiteCustomEventSceneStyle2Up,
+    CameraSuiteCustomEventSceneStyle2Down,
+    CameraSuiteCustomEventSceneStyle2Left,
+    CameraSuiteCustomEventSceneStyle2Right,
+    CameraSuiteCustomEventSceneStyle2Ok,
+    CameraSuiteCustomEventSceneStyle2Back,
+    // Scene events: Guide
+    CameraSuiteCustomEventSceneGuideUp,
+    CameraSuiteCustomEventSceneGuideDown,
+    CameraSuiteCustomEventSceneGuideLeft,
+    CameraSuiteCustomEventSceneGuideRight,
+    CameraSuiteCustomEventSceneGuideOk,
+    CameraSuiteCustomEventSceneGuideBack,
+} CameraSuiteCustomEvent;
+
+enum CameraSuiteCustomEventType {
+    // Reserve first 100 events for button types and indexes, starting from 0.
+    CameraSuiteCustomEventMenuVoid,
+    CameraSuiteCustomEventMenuSelected,
+};
+
+#pragma pack(push, 1)
+
+typedef union {
+    uint32_t packed_value;
+    struct {
+        uint16_t type;
+        int16_t value;
+    } content;
+} CameraSuiteCustomEventMenu;
+
+#pragma pack(pop)
+
+static inline uint32_t camera_suite_custom_menu_event_pack(uint16_t type, int16_t value) {
+    CameraSuiteCustomEventMenu event = {.content = {.type = type, .value = value}};
+    return event.packed_value;
+}
+
+static inline void
+    camera_suite_custom_menu_event_unpack(uint32_t packed_value, uint16_t* type, int16_t* value) {
+    CameraSuiteCustomEventMenu event = {.packed_value = packed_value};
+    if(type) *type = event.content.type;
+    if(value) *value = event.content.value;
+}
+
+static inline uint16_t camera_suite_custom_menu_event_get_type(uint32_t packed_value) {
+    uint16_t type;
+    camera_suite_custom_menu_event_unpack(packed_value, &type, NULL);
+    return type;
+}
+
+static inline int16_t camera_suite_custom_menu_event_get_value(uint32_t packed_value) {
+    int16_t value;
+    camera_suite_custom_menu_event_unpack(packed_value, NULL, &value);
+    return value;
+}

+ 35 - 0
src-fap/helpers/camera_suite_haptic.c

@@ -0,0 +1,35 @@
+#include "camera_suite_haptic.h"
+#include "../camera-suite.h"
+
+void camera_suite_play_happy_bump(void* context) {
+    CameraSuite* app = context;
+    if(app->haptic != 1) {
+        return;
+    }
+    notification_message(app->notification, &sequence_set_vibro_on);
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 20);
+    notification_message(app->notification, &sequence_reset_vibro);
+}
+
+void camera_suite_play_bad_bump(void* context) {
+    CameraSuite* app = context;
+    if(app->haptic != 1) {
+        return;
+    }
+    notification_message(app->notification, &sequence_set_vibro_on);
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 100);
+    notification_message(app->notification, &sequence_reset_vibro);
+}
+
+void camera_suite_play_long_bump(void* context) {
+    CameraSuite* app = context;
+    if(app->haptic != 1) {
+        return;
+    }
+    for(int i = 0; i < 4; i++) {
+        notification_message(app->notification, &sequence_set_vibro_on);
+        furi_thread_flags_wait(0, FuriFlagWaitAny, 50);
+        notification_message(app->notification, &sequence_reset_vibro);
+        furi_thread_flags_wait(0, FuriFlagWaitAny, 100);
+    }
+}

+ 7 - 0
src-fap/helpers/camera_suite_haptic.h

@@ -0,0 +1,7 @@
+#include <notification/notification_messages.h>
+
+void camera_suite_play_happy_bump(void* context);
+
+void camera_suite_play_bad_bump(void* context);
+
+void camera_suite_play_long_bump(void* context);

+ 38 - 0
src-fap/helpers/camera_suite_led.c

@@ -0,0 +1,38 @@
+#include "camera_suite_led.h"
+#include "../camera-suite.h"
+
+void camera_suite_led_set_rgb(void* context, int red, int green, int blue) {
+    CameraSuite* app = context;
+    if(app->led != 1) {
+        return;
+    }
+    NotificationMessage notification_led_message_1;
+    notification_led_message_1.type = NotificationMessageTypeLedRed;
+    NotificationMessage notification_led_message_2;
+    notification_led_message_2.type = NotificationMessageTypeLedGreen;
+    NotificationMessage notification_led_message_3;
+    notification_led_message_3.type = NotificationMessageTypeLedBlue;
+
+    notification_led_message_1.data.led.value = red;
+    notification_led_message_2.data.led.value = green;
+    notification_led_message_3.data.led.value = blue;
+    const NotificationSequence notification_sequence = {
+        &notification_led_message_1,
+        &notification_led_message_2,
+        &notification_led_message_3,
+        &message_do_not_reset,
+        NULL,
+    };
+    notification_message(app->notification, &notification_sequence);
+    //Delay, prevent removal from RAM before LED value set.
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 10);
+}
+
+void camera_suite_led_reset(void* context) {
+    CameraSuite* app = context;
+    notification_message(app->notification, &sequence_reset_red);
+    notification_message(app->notification, &sequence_reset_green);
+    notification_message(app->notification, &sequence_reset_blue);
+    //Delay, prevent removal from RAM before LED value set.
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 300); 
+}

+ 3 - 0
src-fap/helpers/camera_suite_led.h

@@ -0,0 +1,3 @@
+void camera_suite_led_set_rgb(void* context, int red, int green, int blue);
+
+void camera_suite_led_reset(void* context);

+ 26 - 0
src-fap/helpers/camera_suite_speaker.c

@@ -0,0 +1,26 @@
+#include "camera_suite_speaker.h"
+#include "../camera-suite.h"
+
+#define NOTE_INPUT 587.33f
+
+void camera_suite_play_input_sound(void* context) {
+    CameraSuite* app = context;
+    if(app->speaker != 1) {
+        return;
+    }
+    float volume = 1.0f;
+    if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
+        furi_hal_speaker_start(NOTE_INPUT, volume);
+    }
+}
+
+void camera_suite_stop_all_sound(void* context) {
+    CameraSuite* app = context;
+    if(app->speaker != 1) {
+        return;
+    }
+    if(furi_hal_speaker_is_mine()) {
+        furi_hal_speaker_stop();
+        furi_hal_speaker_release();
+    }
+}

+ 5 - 0
src-fap/helpers/camera_suite_speaker.h

@@ -0,0 +1,5 @@
+#define NOTE_INPUT 587.33f
+
+void camera_suite_play_input_sound(void* context);
+
+void camera_suite_stop_all_sound(void* context);

+ 109 - 0
src-fap/helpers/camera_suite_storage.c

@@ -0,0 +1,109 @@
+#include "camera_suite_storage.h"
+
+static Storage* camera_suite_open_storage() {
+    return furi_record_open(RECORD_STORAGE);
+}
+
+static void camera_suite_close_storage() {
+    furi_record_close(RECORD_STORAGE);
+}
+
+static void camera_suite_close_config_file(FlipperFormat* file) {
+    if(file == NULL) return;
+    flipper_format_file_close(file);
+    flipper_format_free(file);
+}
+
+void camera_suite_save_settings(void* context) {
+    CameraSuite* app = context;
+
+    FURI_LOG_D(TAG, "Saving Settings");
+    Storage* storage = camera_suite_open_storage();
+    FlipperFormat* fff_file = flipper_format_file_alloc(storage);
+
+    // Overwrite wont work, so delete first
+    if(storage_file_exists(storage, BOILERPLATE_SETTINGS_SAVE_PATH)) {
+        storage_simply_remove(storage, BOILERPLATE_SETTINGS_SAVE_PATH);
+    }
+
+    // Open File, create if not exists
+    if(!storage_common_stat(storage, BOILERPLATE_SETTINGS_SAVE_PATH, NULL) == FSE_OK) {
+        FURI_LOG_D(
+            TAG, "Config file %s is not found. Will create new.", BOILERPLATE_SETTINGS_SAVE_PATH);
+        if(storage_common_stat(storage, CONFIG_FILE_DIRECTORY_PATH, NULL) == FSE_NOT_EXIST) {
+            FURI_LOG_D(
+                TAG, "Directory %s doesn't exist. Will create new.", CONFIG_FILE_DIRECTORY_PATH);
+            if(!storage_simply_mkdir(storage, CONFIG_FILE_DIRECTORY_PATH)) {
+                FURI_LOG_E(TAG, "Error creating directory %s", CONFIG_FILE_DIRECTORY_PATH);
+            }
+        }
+    }
+
+    if(!flipper_format_file_open_new(fff_file, BOILERPLATE_SETTINGS_SAVE_PATH)) {
+        //totp_close_config_file(fff_file);
+        FURI_LOG_E(TAG, "Error creating new file %s", BOILERPLATE_SETTINGS_SAVE_PATH);
+        camera_suite_close_storage();
+        return;
+    }
+
+    // Store Settings
+    flipper_format_write_header_cstr(
+        fff_file, BOILERPLATE_SETTINGS_HEADER, BOILERPLATE_SETTINGS_FILE_VERSION);
+    flipper_format_write_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_HAPTIC, &app->haptic, 1);
+    flipper_format_write_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_SPEAKER, &app->speaker, 1);
+    flipper_format_write_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_LED, &app->led, 1);
+
+    if(!flipper_format_rewind(fff_file)) {
+        camera_suite_close_config_file(fff_file);
+        FURI_LOG_E(TAG, "Rewind error");
+        camera_suite_close_storage();
+        return;
+    }
+
+    camera_suite_close_config_file(fff_file);
+    camera_suite_close_storage();
+}
+
+void camera_suite_read_settings(void* context) {
+    CameraSuite* app = context;
+    Storage* storage = camera_suite_open_storage();
+    FlipperFormat* fff_file = flipper_format_file_alloc(storage);
+
+    if(storage_common_stat(storage, BOILERPLATE_SETTINGS_SAVE_PATH, NULL) != FSE_OK) {
+        camera_suite_close_config_file(fff_file);
+        camera_suite_close_storage();
+        return;
+    }
+    uint32_t file_version;
+    FuriString* temp_str = furi_string_alloc();
+
+    if(!flipper_format_file_open_existing(fff_file, BOILERPLATE_SETTINGS_SAVE_PATH)) {
+        FURI_LOG_E(TAG, "Cannot open file %s", BOILERPLATE_SETTINGS_SAVE_PATH);
+        camera_suite_close_config_file(fff_file);
+        camera_suite_close_storage();
+        return;
+    }
+
+    if(!flipper_format_read_header(fff_file, temp_str, &file_version)) {
+        FURI_LOG_E(TAG, "Missing Header Data");
+        camera_suite_close_config_file(fff_file);
+        camera_suite_close_storage();
+        return;
+    }
+
+    if(file_version < BOILERPLATE_SETTINGS_FILE_VERSION) {
+        FURI_LOG_I(TAG, "old config version, will be removed.");
+        camera_suite_close_config_file(fff_file);
+        camera_suite_close_storage();
+        return;
+    }
+
+    flipper_format_read_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_HAPTIC, &app->haptic, 1);
+    flipper_format_read_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_SPEAKER, &app->speaker, 1);
+    flipper_format_read_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_LED, &app->led, 1);
+
+    flipper_format_rewind(fff_file);
+
+    camera_suite_close_config_file(fff_file);
+    camera_suite_close_storage();
+}

+ 19 - 0
src-fap/helpers/camera_suite_storage.h

@@ -0,0 +1,19 @@
+#include <stdlib.h>
+#include <string.h>
+#include <storage/storage.h>
+#include <flipper_format/flipper_format_i.h>
+#include "../camera-suite.h"
+
+#define BOILERPLATE_SETTINGS_FILE_VERSION 1
+#define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/camera-suite")
+#define BOILERPLATE_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/camera-suite.conf"
+#define BOILERPLATE_SETTINGS_SAVE_PATH_TMP BOILERPLATE_SETTINGS_SAVE_PATH ".tmp"
+#define BOILERPLATE_SETTINGS_HEADER "Camera Suite Config File"
+#define BOILERPLATE_SETTINGS_KEY_HAPTIC "Haptic"
+#define BOILERPLATE_SETTINGS_KEY_LED "Led"
+#define BOILERPLATE_SETTINGS_KEY_SPEAKER "Speaker"
+#define BOILERPLATE_SETTINGS_KEY_SAVE_SETTINGS "SaveSettings"
+
+void camera_suite_save_settings(void* context);
+
+void camera_suite_read_settings(void* context);

BIN
src-fap/icons/camera-suite.png


+ 30 - 0
src-fap/scenes/camera_suite_scene.c

@@ -0,0 +1,30 @@
+#include "camera_suite_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const camera_suite_on_enter_handlers[])(void*) = {
+#include "camera_suite_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const camera_suite_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "camera_suite_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const camera_suite_on_exit_handlers[])(void* context) = {
+#include "camera_suite_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers camera_suite_scene_handlers = {
+    .on_enter_handlers = camera_suite_on_enter_handlers,
+    .on_event_handlers = camera_suite_on_event_handlers,
+    .on_exit_handlers = camera_suite_on_exit_handlers,
+    .scene_num = CameraSuiteSceneNum,
+};

+ 29 - 0
src-fap/scenes/camera_suite_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) CameraSuiteScene##id,
+typedef enum {
+#include "camera_suite_scene_config.h"
+    CameraSuiteSceneNum,
+} CameraSuiteScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers camera_suite_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "camera_suite_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "camera_suite_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "camera_suite_scene_config.h"
+#undef ADD_SCENE

+ 6 - 0
src-fap/scenes/camera_suite_scene_config.h

@@ -0,0 +1,6 @@
+ADD_SCENE(camera_suite, start, Start)
+ADD_SCENE(camera_suite, menu, Menu)
+ADD_SCENE(camera_suite, style_1, Style_1)
+ADD_SCENE(camera_suite, style_2, Style_2)
+ADD_SCENE(camera_suite, guide, Guide)
+ADD_SCENE(camera_suite, settings, Settings)

+ 51 - 0
src-fap/scenes/camera_suite_scene_guide.c

@@ -0,0 +1,51 @@
+#include "../camera-suite.h"
+#include "../helpers/camera_suite_custom_event.h"
+#include "../views/camera_suite_view_guide.h"
+
+void camera_suite_view_guide_callback(CameraSuiteCustomEvent event, void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void camera_suite_scene_guide_on_enter(void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    camera_suite_view_guide_set_callback(
+        app->camera_suite_view_guide, camera_suite_view_guide_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdGuide);
+}
+
+bool camera_suite_scene_guide_on_event(void* context, SceneManagerEvent event) {
+    CameraSuite* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case CameraSuiteCustomEventSceneGuideLeft:
+        case CameraSuiteCustomEventSceneGuideRight:
+        case CameraSuiteCustomEventSceneGuideUp:
+        case CameraSuiteCustomEventSceneGuideDown:
+            // Do nothing.
+            break;
+        case CameraSuiteCustomEventSceneGuideBack:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, CameraSuiteSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void camera_suite_scene_guide_on_exit(void* context) {
+    CameraSuite* app = context;
+    UNUSED(app);
+}

+ 86 - 0
src-fap/scenes/camera_suite_scene_menu.c

@@ -0,0 +1,86 @@
+#include "../camera-suite.h"
+
+enum SubmenuIndex {
+    /** Atkinson Dithering Algorithm. */
+    SubmenuIndexSceneStyle1 = 10,
+    /** Floyd-Steinberg Dithering Algorithm. */
+    SubmenuIndexSceneStyle2,
+    /** Guide/how-to. */
+    SubmenuIndexGuide,
+    /** Settings menu. */
+    SubmenuIndexSettings,
+};
+
+void camera_suite_scene_menu_submenu_callback(void* context, uint32_t index) {
+    CameraSuite* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void camera_suite_scene_menu_on_enter(void* context) {
+    CameraSuite* app = context;
+
+    submenu_add_item(
+        app->submenu,
+        "Style 1: Atkinson",
+        SubmenuIndexSceneStyle1,
+        camera_suite_scene_menu_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Style 2: Floyd-Steinberg",
+        SubmenuIndexSceneStyle2,
+        camera_suite_scene_menu_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu, "Guide", SubmenuIndexGuide, camera_suite_scene_menu_submenu_callback, app);
+    submenu_add_item(
+        app->submenu,
+        "Settings",
+        SubmenuIndexSettings,
+        camera_suite_scene_menu_submenu_callback,
+        app);
+
+    submenu_set_selected_item(
+        app->submenu, scene_manager_get_scene_state(app->scene_manager, CameraSuiteSceneMenu));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdMenu);
+}
+
+bool camera_suite_scene_menu_on_event(void* context, SceneManagerEvent event) {
+    CameraSuite* app = context;
+    UNUSED(app);
+    if(event.type == SceneManagerEventTypeBack) {
+        // Exit application.
+        scene_manager_stop(app->scene_manager);
+        view_dispatcher_stop(app->view_dispatcher);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexSceneStyle1) {
+            scene_manager_set_scene_state(
+                app->scene_manager, CameraSuiteSceneMenu, SubmenuIndexSceneStyle1);
+            scene_manager_next_scene(app->scene_manager, CameraSuiteSceneStyle_1);
+            return true;
+        } else if(event.event == SubmenuIndexSceneStyle2) {
+            scene_manager_set_scene_state(
+                app->scene_manager, CameraSuiteSceneMenu, SubmenuIndexSceneStyle2);
+            scene_manager_next_scene(app->scene_manager, CameraSuiteSceneStyle_2);
+            return true;
+        } else if(event.event == SubmenuIndexGuide) {
+            scene_manager_set_scene_state(
+                app->scene_manager, CameraSuiteSceneMenu, SubmenuIndexGuide);
+            scene_manager_next_scene(app->scene_manager, CameraSuiteSceneGuide);
+            return true;
+        } else if(event.event == SubmenuIndexSettings) {
+            scene_manager_set_scene_state(
+                app->scene_manager, CameraSuiteSceneMenu, SubmenuIndexSettings);
+            scene_manager_next_scene(app->scene_manager, CameraSuiteSceneSettings);
+            return true;
+        }
+    }
+    return false;
+}
+
+void camera_suite_scene_menu_on_exit(void* context) {
+    CameraSuite* app = context;
+    submenu_reset(app->submenu);
+}

+ 103 - 0
src-fap/scenes/camera_suite_scene_settings.c

@@ -0,0 +1,103 @@
+#include "../camera-suite.h"
+#include <lib/toolbox/value_index.h>
+
+const char* const haptic_text[2] = {
+    "OFF",
+    "ON",
+};
+
+const uint32_t haptic_value[2] = {
+    CameraSuiteHapticOff,
+    CameraSuiteHapticOn,
+};
+
+const char* const speaker_text[2] = {
+    "OFF",
+    "ON",
+};
+
+const uint32_t speaker_value[2] = {
+    CameraSuiteSpeakerOff,
+    CameraSuiteSpeakerOn,
+};
+
+const char* const led_text[2] = {
+    "OFF",
+    "ON",
+};
+
+const uint32_t led_value[2] = {
+    CameraSuiteLedOff,
+    CameraSuiteLedOn,
+};
+
+static void camera_suite_scene_settings_set_haptic(VariableItem* item) {
+    CameraSuite* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, haptic_text[index]);
+    app->haptic = haptic_value[index];
+}
+
+static void camera_suite_scene_settings_set_speaker(VariableItem* item) {
+    CameraSuite* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, speaker_text[index]);
+    app->speaker = speaker_value[index];
+}
+
+static void camera_suite_scene_settings_set_led(VariableItem* item) {
+    CameraSuite* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, led_text[index]);
+    app->led = led_value[index];
+}
+
+void camera_suite_scene_settings_submenu_callback(void* context, uint32_t index) {
+    CameraSuite* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void camera_suite_scene_settings_on_enter(void* context) {
+    CameraSuite* app = context;
+    VariableItem* item;
+    uint8_t value_index;
+
+    // Haptic FX ON/OFF
+    item = variable_item_list_add(
+        app->variable_item_list, "Haptic FX:", 2, camera_suite_scene_settings_set_haptic, app);
+    value_index = value_index_uint32(app->haptic, haptic_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, haptic_text[value_index]);
+
+    // Sound FX ON/OFF
+    item = variable_item_list_add(
+        app->variable_item_list, "Sound FX:", 2, camera_suite_scene_settings_set_speaker, app);
+    value_index = value_index_uint32(app->speaker, speaker_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, speaker_text[value_index]);
+
+    // LED FX ON/OFF
+    item = variable_item_list_add(
+        app->variable_item_list, "LED FX:", 2, camera_suite_scene_settings_set_led, app);
+    value_index = value_index_uint32(app->led, led_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, led_text[value_index]);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdSettings);
+}
+
+bool camera_suite_scene_settings_on_event(void* context, SceneManagerEvent event) {
+    CameraSuite* app = context;
+    UNUSED(app);
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+    }
+    return consumed;
+}
+
+void camera_suite_scene_settings_on_exit(void* context) {
+    CameraSuite* app = context;
+    variable_item_list_set_selected_item(app->variable_item_list, 0);
+    variable_item_list_reset(app->variable_item_list);
+}

+ 55 - 0
src-fap/scenes/camera_suite_scene_start.c

@@ -0,0 +1,55 @@
+#include "../camera-suite.h"
+#include "../helpers/camera_suite_custom_event.h"
+#include "../views/camera_suite_view_start.h"
+
+void camera_suite_scene_start_callback(CameraSuiteCustomEvent event, void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void camera_suite_scene_start_on_enter(void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    camera_suite_view_start_set_callback(
+        app->camera_suite_view_start, camera_suite_scene_start_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdStartscreen);
+}
+
+bool camera_suite_scene_start_on_event(void* context, SceneManagerEvent event) {
+    CameraSuite* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case CameraSuiteCustomEventStartLeft:
+        case CameraSuiteCustomEventStartRight:
+            break;
+        case CameraSuiteCustomEventStartUp:
+        case CameraSuiteCustomEventStartDown:
+            break;
+        case CameraSuiteCustomEventStartOk:
+            scene_manager_next_scene(app->scene_manager, CameraSuiteSceneMenu);
+            consumed = true;
+            break;
+        case CameraSuiteCustomEventStartBack:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, CameraSuiteSceneStart)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void camera_suite_scene_start_on_exit(void* context) {
+    CameraSuite* app = context;
+    UNUSED(app);
+}

+ 52 - 0
src-fap/scenes/camera_suite_scene_style_1.c

@@ -0,0 +1,52 @@
+#include "../camera-suite.h"
+#include "../helpers/camera_suite_custom_event.h"
+#include "../views/camera_suite_view_style_1.h"
+
+static void camera_suite_view_style_1_callback(CameraSuiteCustomEvent event, void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void camera_suite_scene_style_1_on_enter(void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    camera_suite_view_style_1_set_callback(
+        app->camera_suite_view_style_1, camera_suite_view_style_1_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdScene1);
+}
+
+bool camera_suite_scene_style_1_on_event(void* context, SceneManagerEvent event) {
+    CameraSuite* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case CameraSuiteCustomEventSceneStyle1Left:
+        case CameraSuiteCustomEventSceneStyle1Right:
+        case CameraSuiteCustomEventSceneStyle1Up:
+        case CameraSuiteCustomEventSceneStyle1Down:
+        case CameraSuiteCustomEventSceneStyle1Ok:
+            // Do nothing.
+            break;
+        case CameraSuiteCustomEventSceneStyle1Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, CameraSuiteSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void camera_suite_scene_style_1_on_exit(void* context) {
+    CameraSuite* app = context;
+    UNUSED(app);
+}

+ 54 - 0
src-fap/scenes/camera_suite_scene_style_2.c

@@ -0,0 +1,54 @@
+#include "../camera-suite.h"
+#include "../helpers/camera_suite_custom_event.h"
+#include "../helpers/camera_suite_haptic.h"
+#include "../helpers/camera_suite_led.h"
+#include "../views/camera_suite_view_style_2.h"
+
+void camera_suite_view_style_2_callback(CameraSuiteCustomEvent event, void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void camera_suite_scene_style_2_on_enter(void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    camera_suite_view_style_2_set_callback(
+        app->camera_suite_view_style_2, camera_suite_view_style_2_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdScene2);
+}
+
+bool camera_suite_scene_style_2_on_event(void* context, SceneManagerEvent event) {
+    CameraSuite* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case CameraSuiteCustomEventSceneStyle2Left:
+        case CameraSuiteCustomEventSceneStyle2Right:
+        case CameraSuiteCustomEventSceneStyle2Up:
+        case CameraSuiteCustomEventSceneStyle2Down:
+        case CameraSuiteCustomEventSceneStyle2Ok:
+            // Do nothing.
+            break;
+        case CameraSuiteCustomEventSceneStyle2Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, CameraSuiteSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void camera_suite_scene_style_2_on_exit(void* context) {
+    CameraSuite* app = context;
+    UNUSED(app);
+}

+ 119 - 0
src-fap/views/camera_suite_view_guide.c

@@ -0,0 +1,119 @@
+#include "../camera-suite.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+
+struct CameraSuiteViewGuide {
+    View* view;
+    CameraSuiteViewGuideCallback callback;
+    void* context;
+};
+
+typedef struct {
+    int some_value;
+} CameraSuiteViewGuideModel;
+
+void camera_suite_view_guide_set_callback(
+    CameraSuiteViewGuide* instance,
+    CameraSuiteViewGuideCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void camera_suite_view_guide_draw(Canvas* canvas, CameraSuiteViewGuideModel* model) {
+    UNUSED(model);
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 0, 0, AlignLeft, AlignTop, "Guide");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 0, 12, AlignLeft, AlignTop, "Left = Contrast Down");
+    canvas_draw_str_aligned(canvas, 0, 22, AlignLeft, AlignTop, "Right = Contrast Up");
+    canvas_draw_str_aligned(canvas, 0, 32, AlignLeft, AlignTop, "Up = Brightness Up");
+    canvas_draw_str_aligned(canvas, 0, 42, AlignLeft, AlignTop, "Down = Brightness Down");
+    canvas_draw_str_aligned(canvas, 0, 52, AlignLeft, AlignTop, "Center = Take Picture");
+}
+
+static void camera_suite_view_guide_model_init(CameraSuiteViewGuideModel* const model) {
+    model->some_value = 1;
+}
+
+bool camera_suite_view_guide_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    CameraSuiteViewGuide* instance = context;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewGuideModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventSceneGuideBack, instance->context);
+                },
+                true);
+            break;
+        case InputKeyLeft:
+        case InputKeyRight:
+        case InputKeyUp:
+        case InputKeyDown:
+        case InputKeyOk:
+        case InputKeyMAX:
+            // Do nothing.
+            break;
+        }
+    }
+    return true;
+}
+
+void camera_suite_view_guide_exit(void* context) {
+    furi_assert(context);
+}
+
+void camera_suite_view_guide_enter(void* context) {
+    furi_assert(context);
+    CameraSuiteViewGuide* instance = (CameraSuiteViewGuide*)context;
+    with_view_model(
+        instance->view,
+        CameraSuiteViewGuideModel * model,
+        { camera_suite_view_guide_model_init(model); },
+        true);
+}
+
+CameraSuiteViewGuide* camera_suite_view_guide_alloc() {
+    CameraSuiteViewGuide* instance = malloc(sizeof(CameraSuiteViewGuide));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(CameraSuiteViewGuideModel));
+    view_set_context(instance->view, instance); // furi_assert crashes in events without this
+    view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_guide_draw);
+    view_set_input_callback(instance->view, camera_suite_view_guide_input);
+    view_set_enter_callback(instance->view, camera_suite_view_guide_enter);
+    view_set_exit_callback(instance->view, camera_suite_view_guide_exit);
+
+    with_view_model(
+        instance->view,
+        CameraSuiteViewGuideModel * model,
+        { camera_suite_view_guide_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void camera_suite_view_guide_free(CameraSuiteViewGuide* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, CameraSuiteViewGuideModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* camera_suite_view_guide_get_view(CameraSuiteViewGuide* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 19 - 0
src-fap/views/camera_suite_view_guide.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/camera_suite_custom_event.h"
+
+typedef struct CameraSuiteViewGuide CameraSuiteViewGuide;
+
+typedef void (*CameraSuiteViewGuideCallback)(CameraSuiteCustomEvent event, void* context);
+
+void camera_suite_view_guide_set_callback(
+    CameraSuiteViewGuide* camera_suite_view_guide,
+    CameraSuiteViewGuideCallback callback,
+    void* context);
+
+View* camera_suite_view_guide_get_view(CameraSuiteViewGuide* camera_suite_static);
+
+CameraSuiteViewGuide* camera_suite_view_guide_alloc();
+
+void camera_suite_view_guide_free(CameraSuiteViewGuide* camera_suite_static);

+ 126 - 0
src-fap/views/camera_suite_view_start.c

@@ -0,0 +1,126 @@
+#include "../camera-suite.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+
+struct CameraSuiteViewStart {
+    View* view;
+    CameraSuiteViewStartCallback callback;
+    void* context;
+};
+
+typedef struct {
+    int some_value;
+} CameraSuiteViewStartModel;
+
+void camera_suite_view_start_set_callback(
+    CameraSuiteViewStart* instance,
+    CameraSuiteViewStartCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void camera_suite_view_start_draw(Canvas* canvas, CameraSuiteViewStartModel* model) {
+    UNUSED(model);
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, "Camera Suite");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignTop, "Flipper Zero");
+    canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignTop, "ESP32 CAM");
+    elements_button_center(canvas, "Start");
+}
+
+static void camera_suite_view_start_model_init(CameraSuiteViewStartModel* const model) {
+    model->some_value = 1;
+}
+
+bool camera_suite_view_start_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    CameraSuiteViewStart* instance = context;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            // Exit application.
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStartModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventStartBack, instance->context);
+                },
+                true);
+            break;
+        case InputKeyOk:
+            // Start the application.
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStartModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventStartOk, instance->context);
+                },
+                true);
+            break;
+        case InputKeyMAX:
+        case InputKeyLeft:
+        case InputKeyRight:
+        case InputKeyUp:
+        case InputKeyDown:
+            // Do nothing.
+            break;
+        }
+    }
+    return true;
+}
+
+void camera_suite_view_start_exit(void* context) {
+    furi_assert(context);
+}
+
+void camera_suite_view_start_enter(void* context) {
+    furi_assert(context);
+    CameraSuiteViewStart* instance = (CameraSuiteViewStart*)context;
+    with_view_model(
+        instance->view,
+        CameraSuiteViewStartModel * model,
+        { camera_suite_view_start_model_init(model); },
+        true);
+}
+
+CameraSuiteViewStart* camera_suite_view_start_alloc() {
+    CameraSuiteViewStart* instance = malloc(sizeof(CameraSuiteViewStart));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(CameraSuiteViewStartModel));
+    // furi_assert crashes in events without this
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_start_draw);
+    view_set_input_callback(instance->view, camera_suite_view_start_input);
+
+    with_view_model(
+        instance->view,
+        CameraSuiteViewStartModel * model,
+        { camera_suite_view_start_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void camera_suite_view_start_free(CameraSuiteViewStart* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, CameraSuiteViewStartModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* camera_suite_view_start_get_view(CameraSuiteViewStart* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 19 - 0
src-fap/views/camera_suite_view_start.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/camera_suite_custom_event.h"
+
+typedef struct CameraSuiteViewStart CameraSuiteViewStart;
+
+typedef void (*CameraSuiteViewStartCallback)(CameraSuiteCustomEvent event, void* context);
+
+void camera_suite_view_start_set_callback(
+    CameraSuiteViewStart* camera_suite_view_start,
+    CameraSuiteViewStartCallback callback,
+    void* context);
+
+View* camera_suite_view_start_get_view(CameraSuiteViewStart* camera_suite_static);
+
+CameraSuiteViewStart* camera_suite_view_start_alloc();
+
+void camera_suite_view_start_free(CameraSuiteViewStart* camera_suite_static);

+ 302 - 0
src-fap/views/camera_suite_view_style_1.c

@@ -0,0 +1,302 @@
+#include "../camera-suite.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+
+struct CameraSuiteViewStyle1 {
+    View* view;
+    CameraSuiteViewStyle1Callback callback;
+    FuriThread* worker_thread;
+    FuriStreamBuffer* rx_stream;
+    void* context;
+};
+
+void camera_suite_view_style_1_set_callback(
+    CameraSuiteViewStyle1* instance,
+    CameraSuiteViewStyle1Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+static void camera_suite_view_style_1_draw(Canvas* canvas, UartDumpModel* model) {
+    // Clear the screen.
+    canvas_set_color(canvas, ColorBlack);
+
+    // Draw the frame.
+    canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGHT);
+
+    // Draw the pixels.
+    for(size_t p = 0; p < FRAME_BUFFER_LENGTH; ++p) {
+        uint8_t x = p % ROW_BUFFER_LENGTH; // 0 .. 15
+        uint8_t y = p / ROW_BUFFER_LENGTH; // 0 .. 63
+
+        for(uint8_t i = 0; i < 8; ++i) {
+            if((model->pixels[p] & (1 << (7 - i))) != 0) {
+                canvas_draw_dot(canvas, (x * 8) + i, y);
+            }
+        }
+    }
+
+    // Draw the guide if the camera is not initialized.
+    if(!model->initialized) {
+        canvas_draw_icon(canvas, 74, 16, &I_DolphinCommon_56x48);
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 8, 12, "Connect the ESP32-CAM");
+        canvas_draw_str(canvas, 20, 24, "VCC - 3V3");
+        canvas_draw_str(canvas, 20, 34, "GND - GND");
+        canvas_draw_str(canvas, 20, 44, "U0R - TX");
+        canvas_draw_str(canvas, 20, 54, "U0T - RX");
+    }
+}
+
+static void camera_suite_view_style_1_model_init(UartDumpModel* const model) {
+    for(size_t i = 0; i < FRAME_BUFFER_LENGTH; i++) {
+        model->pixels[i] = 0;
+    }
+}
+
+static bool camera_suite_view_style_1_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    CameraSuiteViewStyle1* instance = context;
+    if(event->type == InputTypeRelease) {
+        uint8_t data[1];
+        switch(event->key) {
+        case InputKeyBack:
+            // Stop the camera stream.
+            data[0] = 'S';
+            // Go back to the main menu.
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventSceneStyle1Back, instance->context);
+                },
+                true);
+            break;
+        case InputKeyLeft:
+            // Camera: Invert.
+            data[0] = '<';
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventSceneStyle1Left, instance->context);
+                },
+                true);
+            break;
+        case InputKeyRight:
+            // Camera: Enable/disable dithering.
+            data[0] = '>';
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventSceneStyle1Right, instance->context);
+                },
+                true);
+            break;
+        case InputKeyUp:
+            // Camera: Increase contrast.
+            data[0] = 'C';
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventSceneStyle1Up, instance->context);
+                },
+                true);
+            break;
+        case InputKeyDown:
+            // Camera: Reduce contrast.
+            data[0] = 'c';
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventSceneStyle1Down, instance->context);
+                },
+                true);
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventSceneStyle1Ok, instance->context);
+                },
+                true);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+        // Send `data` to the ESP32-CAM
+        furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+    }
+    return true;
+}
+
+static void camera_suite_view_style_1_exit(void* context) {
+    furi_assert(context);
+}
+
+static void camera_suite_view_style_1_enter(void* context) {
+    // Check `context` for null. If it is null, abort program, else continue.
+    furi_assert(context);
+
+    // Cast `context` to `CameraSuiteViewStyle1*` and store it in `instance`.
+    CameraSuiteViewStyle1* instance = (CameraSuiteViewStyle1*)context;
+
+    with_view_model(
+        instance->view,
+        UartDumpModel * model,
+        { camera_suite_view_style_1_model_init(model); },
+        true);
+}
+
+static void camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* context) {
+    // Check `context` for null. If it is null, abort program, else continue.
+    furi_assert(context);
+
+    // Cast `context` to `CameraSuiteViewStyle1*` and store it in `instance`.
+    CameraSuiteViewStyle1* instance = context;
+
+    // If `uartIrqEvent` is `UartIrqEventRXNE`, send the data to the
+    // `rx_stream` and set the `WorkerEventRx` flag.
+    if(uartIrqEvent == UartIrqEventRXNE) {
+        furi_stream_buffer_send(instance->rx_stream, &data, 1, 0);
+        furi_thread_flags_set(furi_thread_get_id(instance->worker_thread), WorkerEventRx);
+    }
+}
+
+static void process_ringbuffer(UartDumpModel* model, uint8_t byte) {
+    // First char has to be 'Y' in the buffer.
+    if(model->ringbuffer_index == 0 && byte != 'Y') {
+        return;
+    }
+
+    // Second char has to be ':' in the buffer or reset.
+    if(model->ringbuffer_index == 1 && byte != ':') {
+        model->ringbuffer_index = 0;
+        process_ringbuffer(model, byte);
+        return;
+    }
+
+    // Assign current byte to the ringbuffer.
+    model->row_ringbuffer[model->ringbuffer_index] = byte;
+    // Increment the ringbuffer index.
+    ++model->ringbuffer_index;
+
+    // Let's wait 'till the buffer fills.
+    if(model->ringbuffer_index < RING_BUFFER_LENGTH) {
+        return;
+    }
+
+    // Flush the ringbuffer to the framebuffer.
+    model->ringbuffer_index = 0; // Reset the ringbuffer
+    model->initialized = true; // Established the connection successfully.
+    size_t row_start_index =
+        model->row_ringbuffer[2] * ROW_BUFFER_LENGTH; // Third char will determine the row number
+
+    if(row_start_index > LAST_ROW_INDEX) { // Failsafe
+        row_start_index = 0;
+    }
+
+    for(size_t i = 0; i < ROW_BUFFER_LENGTH; ++i) {
+        model->pixels[row_start_index + i] =
+            model->row_ringbuffer[i + 3]; // Writing the remaining 16 bytes into the frame buffer
+    }
+}
+
+static int32_t camera_worker(void* context) {
+    furi_assert(context);
+    CameraSuiteViewStyle1* instance = context;
+
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever);
+        furi_check((events & FuriFlagError) == 0);
+
+        if(events & WorkerEventStop) {
+            break;
+        } else if(events & WorkerEventRx) {
+            size_t length = 0;
+            do {
+                size_t intended_data_size = 64;
+                uint8_t data[intended_data_size];
+                length =
+                    furi_stream_buffer_receive(instance->rx_stream, data, intended_data_size, 0);
+
+                if(length > 0) {
+                    with_view_model(
+                        instance->view,
+                        UartDumpModel * model,
+                        {
+                            for(size_t i = 0; i < length; i++) {
+                                process_ringbuffer(model, data[i]);
+                            }
+                        },
+                        false);
+                }
+            } while(length > 0);
+        }
+    }
+
+    return 0;
+}
+
+CameraSuiteViewStyle1* camera_suite_view_style_1_alloc() {
+    CameraSuiteViewStyle1* instance = malloc(sizeof(CameraSuiteViewStyle1));
+
+    instance->view = view_alloc();
+
+    instance->rx_stream = furi_stream_buffer_alloc(2048, 1);
+
+    // Set up views
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UartDumpModel));
+    view_set_context(instance->view, instance); // furi_assert crashes in events without this
+    view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_style_1_draw);
+    view_set_input_callback(instance->view, camera_suite_view_style_1_input);
+    view_set_enter_callback(instance->view, camera_suite_view_style_1_enter);
+    view_set_exit_callback(instance->view, camera_suite_view_style_1_exit);
+
+    with_view_model(
+        instance->view,
+        UartDumpModel * model,
+        { camera_suite_view_style_1_model_init(model); },
+        true);
+
+    instance->worker_thread = furi_thread_alloc_ex("UsbUartWorker", 2048, camera_worker, instance);
+    furi_thread_start(instance->worker_thread);
+
+    // Enable uart listener
+    furi_hal_console_disable();
+    furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400);
+    furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, camera_on_irq_cb, instance);
+
+    return instance;
+}
+
+void camera_suite_view_style_1_free(CameraSuiteViewStyle1* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, UartDumpModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* camera_suite_view_style_1_get_view(CameraSuiteViewStyle1* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 86 - 0
src-fap/views/camera_suite_view_style_1.h

@@ -0,0 +1,86 @@
+#include "../helpers/camera_suite_custom_event.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_console.h>
+#include <furi_hal_uart.h>
+#include <gui/elements.h>
+#include <gui/gui.h>
+#include <gui/icon_i.h>
+#include <gui/modules/dialog_ex.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+#include <storage/filesystem_api_defines.h>
+#include <storage/storage.h>
+
+#pragma once
+
+#define FRAME_WIDTH 128
+#define FRAME_HEIGHT 64
+#define FRAME_BIT_DEPTH 1
+#define FRAME_BUFFER_LENGTH \
+    (FRAME_WIDTH * FRAME_HEIGHT * FRAME_BIT_DEPTH / 8) // 128*64*1 / 8 = 1024
+#define ROW_BUFFER_LENGTH (FRAME_WIDTH / 8) // 128/8 = 16
+#define RING_BUFFER_LENGTH (ROW_BUFFER_LENGTH + 3) // ROW_BUFFER_LENGTH + Header => 16 + 3 = 19
+#define LAST_ROW_INDEX (FRAME_BUFFER_LENGTH - ROW_BUFFER_LENGTH) // 1024 - 16 = 1008
+
+// const uint8_t _I_DolphinCommon_56x48_0[] = {
+//     0x01, 0x00, 0xdf, 0x00, 0x00, 0x1f, 0xfe, 0x0e, 0x05, 0x3f, 0x04, 0x06, 0x78, 0x06, 0x30, 0x20,
+//     0xf8, 0x00, 0xc6, 0x12, 0x1c, 0x04, 0x0c, 0x0a, 0x38, 0x08, 0x08, 0x0c, 0x60, 0xc0, 0x21, 0xe0,
+//     0x04, 0x0a, 0x18, 0x02, 0x1b, 0x00, 0x18, 0xa3, 0x00, 0x21, 0x90, 0x01, 0x8a, 0x20, 0x02, 0x19,
+//     0x80, 0x18, 0x80, 0x64, 0x09, 0x20, 0x89, 0x81, 0x8c, 0x3e, 0x41, 0xe2, 0x80, 0x50, 0x00, 0x43,
+//     0x08, 0x01, 0x0c, 0xfc, 0x68, 0x40, 0x61, 0xc0, 0x50, 0x30, 0x00, 0x63, 0xa0, 0x7f, 0x80, 0xc4,
+//     0x41, 0x19, 0x07, 0xff, 0x02, 0x06, 0x18, 0x24, 0x03, 0x41, 0xf3, 0x2b, 0x10, 0x19, 0x38, 0x10,
+//     0x30, 0x31, 0x7f, 0xe0, 0x34, 0x08, 0x30, 0x19, 0x60, 0x80, 0x65, 0x86, 0x0a, 0x4c, 0x0c, 0x30,
+//     0x81, 0xb9, 0x41, 0xa0, 0x54, 0x08, 0xc7, 0xe2, 0x06, 0x8a, 0x18, 0x25, 0x02, 0x21, 0x0f, 0x19,
+//     0x88, 0xd8, 0x6e, 0x1b, 0x01, 0xd1, 0x1b, 0x86, 0x39, 0x66, 0x3a, 0xa4, 0x1a, 0x50, 0x06, 0x48,
+//     0x18, 0x18, 0xd0, 0x03, 0x01, 0x41, 0x98, 0xcc, 0x60, 0x39, 0x01, 0x49, 0x2d, 0x06, 0x03, 0x50,
+//     0xf8, 0x40, 0x3e, 0x02, 0xc1, 0x82, 0x86, 0xc7, 0xfe, 0x0f, 0x28, 0x2c, 0x91, 0xd2, 0x90, 0x9a,
+//     0x18, 0x19, 0x3e, 0x6d, 0x73, 0x12, 0x16, 0x00, 0x32, 0x49, 0x72, 0xc0, 0x7e, 0x5d, 0x44, 0xba,
+//     0x2c, 0x08, 0xa4, 0xc8, 0x82, 0x06, 0x17, 0xe0, 0x81, 0x90, 0x2a, 0x40, 0x61, 0xe1, 0xa2, 0x44,
+//     0x0c, 0x76, 0x2b, 0xe8, 0x89, 0x26, 0x43, 0x83, 0x31, 0x8c, 0x78, 0x0c, 0xb0, 0x48, 0x10, 0x1a,
+//     0xe0, 0x00, 0x63,
+// };
+// const uint8_t* const _I_DolphinCommon_56x48[] = {_I_DolphinCommon_56x48_0};
+// const Icon I_DolphinCommon_56x48 = {
+//     .width = 56,
+//     .height = 48,
+//     .frame_count = 1,
+//     .frame_rate = 0,
+//     .frames = _I_DolphinCommon_56x48};
+
+typedef struct UartDumpModel UartDumpModel;
+
+struct UartDumpModel {
+    uint8_t pixels[FRAME_BUFFER_LENGTH];
+
+    bool initialized;
+
+    uint8_t row_ringbuffer[RING_BUFFER_LENGTH];
+    uint8_t ringbuffer_index;
+};
+
+typedef struct CameraSuiteViewStyle1 CameraSuiteViewStyle1;
+
+typedef void (*CameraSuiteViewStyle1Callback)(CameraSuiteCustomEvent event, void* context);
+
+void camera_suite_view_style_1_set_callback(
+    CameraSuiteViewStyle1* camera_suite_view_style_1,
+    CameraSuiteViewStyle1Callback callback,
+    void* context);
+
+CameraSuiteViewStyle1* camera_suite_view_style_1_alloc();
+
+void camera_suite_view_style_1_free(CameraSuiteViewStyle1* camera_suite_static);
+
+View* camera_suite_view_style_1_get_view(CameraSuiteViewStyle1* camera_suite_static);
+
+typedef enum {
+    // Reserved for StreamBuffer internal event
+    WorkerEventReserved = (1 << 0),
+    WorkerEventStop = (1 << 1),
+    WorkerEventRx = (1 << 2),
+} WorkerEventFlags;
+
+#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx)

+ 249 - 0
src-fap/views/camera_suite_view_style_2.c

@@ -0,0 +1,249 @@
+#include "../camera-suite.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+#include "../helpers/camera_suite_haptic.h"
+#include "../helpers/camera_suite_speaker.h"
+#include "../helpers/camera_suite_led.h"
+
+struct CameraSuiteViewStyle2 {
+    View* view;
+    CameraSuiteViewStyle2Callback callback;
+    void* context;
+};
+
+typedef struct {
+    int screen_text;
+} CameraSuiteViewStyle2Model;
+
+char buttonText[11][14] = {
+    "",
+    "Press Up",
+    "Press Down",
+    "Press Left",
+    "Press Right",
+    "Press Ok",
+    "Release Up",
+    "Release Down",
+    "Release Left",
+    "Release Right",
+    "Release Ok",
+};
+
+void camera_suite_view_style_2_set_callback(
+    CameraSuiteViewStyle2* instance,
+    CameraSuiteViewStyle2Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void camera_suite_view_style_2_draw(Canvas* canvas, CameraSuiteViewStyle2Model* model) {
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, "Scene 2: Input Examples");
+    canvas_set_font(canvas, FontSecondary);
+    char* strInput = malloc(15);
+    strcpy(strInput, buttonText[model->screen_text]);
+    canvas_draw_str_aligned(canvas, 0, 22, AlignLeft, AlignTop, strInput);
+    free(strInput);
+}
+
+static void camera_suite_view_style_2_model_init(CameraSuiteViewStyle2Model* const model) {
+    model->screen_text = 0;
+}
+
+bool camera_suite_view_style_2_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    CameraSuiteViewStyle2* instance = context;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    UNUSED(model);
+                    camera_suite_stop_all_sound(instance->context);
+                    instance->callback(CameraSuiteCustomEventSceneStyle2Back, instance->context);
+                    camera_suite_play_long_bump(instance->context);
+                },
+                true);
+            break;
+        case InputKeyUp:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 6;
+                    camera_suite_play_bad_bump(instance->context);
+                    camera_suite_stop_all_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 255, 0, 255);
+                },
+                true);
+            break;
+        case InputKeyDown:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 7;
+                    camera_suite_play_bad_bump(instance->context);
+                    camera_suite_stop_all_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 255, 255, 0);
+                },
+                true);
+            break;
+        case InputKeyLeft:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 8;
+                    camera_suite_play_bad_bump(instance->context);
+                    camera_suite_stop_all_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 0, 255, 255);
+                },
+                true);
+            break;
+        case InputKeyRight:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 9;
+                    camera_suite_play_bad_bump(instance->context);
+                    camera_suite_stop_all_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 255, 0, 0);
+                },
+                true);
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 10;
+                    camera_suite_play_bad_bump(instance->context);
+                    camera_suite_stop_all_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 255, 255, 255);
+                },
+                true);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    } else if(event->type == InputTypePress) {
+        switch(event->key) {
+        case InputKeyUp:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 1;
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                },
+                true);
+            break;
+        case InputKeyDown:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 2;
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                },
+                true);
+            break;
+        case InputKeyLeft:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 3;
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                },
+                true);
+            break;
+        case InputKeyRight:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 4;
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                },
+                true);
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                CameraSuiteViewStyle2Model * model,
+                {
+                    model->screen_text = 5;
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                },
+                true);
+            break;
+        case InputKeyBack:
+        case InputKeyMAX:
+            break;
+        }
+    }
+
+    return true;
+}
+
+void camera_suite_view_style_2_exit(void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    camera_suite_stop_all_sound(app);
+    //camera_suite_led_reset(app);
+}
+
+void camera_suite_view_style_2_enter(void* context) {
+    furi_assert(context);
+    dolphin_deed(DolphinDeedPluginStart);
+}
+
+CameraSuiteViewStyle2* camera_suite_view_style_2_alloc() {
+    CameraSuiteViewStyle2* instance = malloc(sizeof(CameraSuiteViewStyle2));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(CameraSuiteViewStyle2Model));
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_style_2_draw);
+    view_set_input_callback(instance->view, camera_suite_view_style_2_input);
+    //view_set_enter_callback(instance->view, camera_suite_view_style_2_enter);
+    view_set_exit_callback(instance->view, camera_suite_view_style_2_exit);
+
+    with_view_model(
+        instance->view,
+        CameraSuiteViewStyle2Model * model,
+        { camera_suite_view_style_2_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void camera_suite_view_style_2_free(CameraSuiteViewStyle2* instance) {
+    furi_assert(instance);
+
+    view_free(instance->view);
+    free(instance);
+}
+
+View* camera_suite_view_style_2_get_view(CameraSuiteViewStyle2* instance) {
+    furi_assert(instance);
+
+    return instance->view;
+}

+ 19 - 0
src-fap/views/camera_suite_view_style_2.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/camera_suite_custom_event.h"
+
+typedef struct CameraSuiteViewStyle2 CameraSuiteViewStyle2;
+
+typedef void (*CameraSuiteViewStyle2Callback)(CameraSuiteCustomEvent event, void* context);
+
+void camera_suite_view_style_2_set_callback(
+    CameraSuiteViewStyle2* instance,
+    CameraSuiteViewStyle2Callback callback,
+    void* context);
+
+CameraSuiteViewStyle2* camera_suite_view_style_2_alloc();
+
+void camera_suite_view_style_2_free(CameraSuiteViewStyle2* camera_suite_static);
+
+View* camera_suite_view_style_2_get_view(CameraSuiteViewStyle2* boilerpate_static);

+ 183 - 0
src-firmware/esp32_cam_uart_stream.ino

@@ -0,0 +1,183 @@
+#include "esp_camera.h"
+
+#define PWDN_GPIO_NUM     32
+#define RESET_GPIO_NUM    -1
+#define XCLK_GPIO_NUM      0
+#define SIOD_GPIO_NUM     26
+#define SIOC_GPIO_NUM     27
+
+#define Y9_GPIO_NUM       35
+#define Y8_GPIO_NUM       34
+#define Y7_GPIO_NUM       39
+#define Y6_GPIO_NUM       36
+#define Y5_GPIO_NUM       21
+#define Y4_GPIO_NUM       19
+#define Y3_GPIO_NUM       18
+#define Y2_GPIO_NUM        5
+#define VSYNC_GPIO_NUM    25
+#define HREF_GPIO_NUM     23
+#define PCLK_GPIO_NUM     22
+
+
+void setup() {
+  Serial.begin(230400);
+
+  camera_config_t config;
+  config.ledc_channel = LEDC_CHANNEL_0;
+  config.ledc_timer = LEDC_TIMER_0;
+  config.pin_d0 = Y2_GPIO_NUM;
+  config.pin_d1 = Y3_GPIO_NUM;
+  config.pin_d2 = Y4_GPIO_NUM;
+  config.pin_d3 = Y5_GPIO_NUM;
+  config.pin_d4 = Y6_GPIO_NUM;
+  config.pin_d5 = Y7_GPIO_NUM;
+  config.pin_d6 = Y8_GPIO_NUM;
+  config.pin_d7 = Y9_GPIO_NUM;
+  config.pin_xclk = XCLK_GPIO_NUM;
+  config.pin_pclk = PCLK_GPIO_NUM;
+  config.pin_vsync = VSYNC_GPIO_NUM;
+  config.pin_href = HREF_GPIO_NUM;
+  config.pin_sscb_sda = SIOD_GPIO_NUM;
+  config.pin_sscb_scl = SIOC_GPIO_NUM;
+  config.pin_pwdn = PWDN_GPIO_NUM;
+  config.pin_reset = RESET_GPIO_NUM;
+  config.xclk_freq_hz = 20000000;
+  config.pixel_format = PIXFORMAT_GRAYSCALE;
+
+  // We don't need a big frame
+  config.frame_size = FRAMESIZE_QQVGA;
+  config.fb_count = 1;
+
+  // camera init
+  esp_err_t err = esp_camera_init(&config);
+  if (err != ESP_OK) {
+    Serial.printf("Camera init failed with error 0x%x", err);
+    return;
+  }
+
+  // Setting high contrast to make easier to dither
+  sensor_t * s = esp_camera_sensor_get();
+  s->set_contrast(s, 2);
+}
+
+bool stop_stream = false;
+bool disable_dithering = false;
+bool invert = false;
+
+void loop() {
+
+  // Reading serial
+  if (Serial.available() > 0) {
+    char r = Serial.read();
+    sensor_t * s = esp_camera_sensor_get();
+    
+    switch(r) {
+      case 'S':
+        stop_stream = false;
+        break;
+      case 's':
+        stop_stream = true;
+        break;
+      case 'D':
+        disable_dithering = false;
+        break;
+      case 'd':
+        disable_dithering = true;
+        break;
+      case 'C':
+        s->set_contrast(s, s->status.contrast + 1);
+        break;
+      case 'c':
+        s->set_contrast(s, s->status.contrast - 1);
+        break;
+      case 'B':
+        s->set_contrast(s, s->status.brightness + 1);
+        break;
+      case 'b':
+        s->set_contrast(s, s->status.brightness - 1);
+        break;
+
+      // Toggle cases
+      case 'M': // Toggle Mirror
+        s->set_hmirror(s, !s->status.hmirror);
+        break;
+      case '>':
+        disable_dithering = !disable_dithering;
+        break;
+      case '<':
+        invert = !invert;
+      default:
+        break;
+    }
+  }
+
+  if (stop_stream){
+    return;
+  }
+
+  camera_fb_t* fb = esp_camera_fb_get();
+
+  if (!fb) {
+    return;
+  }
+  
+  //Length: 19200
+  //Width: 160
+  //Height: 120
+  //Format: 2
+  //Target: 128x64
+
+  if (!disable_dithering) {
+    DitherImage(fb);
+  }
+
+  uint8_t flipper_y = 0;
+  for(uint8_t y = 28; y < 92; ++y) {
+    Serial.print("Y:");
+    Serial.print((char)flipper_y);
+
+    size_t true_y = y * fb->width;
+    for (uint8_t x = 16; x < 144; x+=8){
+      char c = 0;
+      for(uint8_t j = 0; j < 8; ++j){
+        if (IsDarkBit(fb->buf[true_y + x + (7-j)])){
+          c |= 1 << j;
+        }
+      }
+      Serial.print(c);
+    }
+
+    ++flipper_y;
+    Serial.flush();
+  }
+
+  esp_camera_fb_return(fb);
+  fb = NULL;
+  delay(50);
+}
+
+bool IsDarkBit(uint8_t bit){
+  bool result = bit < 128;
+
+  if (invert){
+    result = !result;
+  }
+
+  return result;
+}
+
+void DitherImage(camera_fb_t* fb) {
+  for(uint8_t y = 0; y < fb->height; ++y){
+      for (uint8_t x = 0; x < fb->width; ++x){
+        size_t current = (y*fb->width) + x;
+        uint8_t oldpixel = fb->buf[current];
+        uint8_t newpixel = oldpixel >= 128 ? 255 : 0;
+        fb->buf[current] = newpixel;
+        uint8_t quant_error = oldpixel - newpixel;
+        fb->buf[(y*fb->width) + x + 1] = fb->buf[(y*fb->width) + x + 1] + quant_error * 7 / 16;
+        fb->buf[(y+1*fb->width) + x-1] = fb->buf[(y+1*fb->width) + x-1] + quant_error * 3 / 16;
+        fb->buf[(y + 1*fb->width) + x] = fb->buf[(y + 1*fb->width) + x] + quant_error * 5 / 16;
+        fb->buf[(y+1*fb->width) + x+1] = fb->buf[(y+1*fb->width) + x+1] + quant_error * 1 / 16;
+      }
+    }  
+}