Quellcode durchsuchen

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

git-subtree-dir: camera_suite
git-subtree-mainline: 734ecdc750871f0696b542ae9065ea1221f99868
git-subtree-split: 038cde19208f4515a8bb0bd55daf3fb38dd6b4f3
Willy-JL vor 2 Jahren
Ursprung
Commit
d96d41b886
36 geänderte Dateien mit 2130 neuen und 0 gelöschten Zeilen
  1. 1 0
      camera_suite/.gitsubtree
  2. 28 0
      camera_suite/LICENSE
  3. 17 0
      camera_suite/application.fam
  4. 135 0
      camera_suite/camera_suite.c
  5. 86 0
      camera_suite/camera_suite.h
  6. 48 0
      camera_suite/docs/CHANGELOG.md
  7. 37 0
      camera_suite/docs/README.md
  8. 67 0
      camera_suite/helpers/camera_suite_custom_event.h
  9. 35 0
      camera_suite/helpers/camera_suite_haptic.c
  10. 7 0
      camera_suite/helpers/camera_suite_haptic.h
  11. 38 0
      camera_suite/helpers/camera_suite_led.c
  12. 3 0
      camera_suite/helpers/camera_suite_led.h
  13. 26 0
      camera_suite/helpers/camera_suite_speaker.c
  14. 5 0
      camera_suite/helpers/camera_suite_speaker.h
  15. 120 0
      camera_suite/helpers/camera_suite_storage.c
  16. 29 0
      camera_suite/helpers/camera_suite_storage.h
  17. BIN
      camera_suite/icons/camera_suite.png
  18. 30 0
      camera_suite/scenes/camera_suite_scene.c
  19. 29 0
      camera_suite/scenes/camera_suite_scene.h
  20. 52 0
      camera_suite/scenes/camera_suite_scene_camera.c
  21. 5 0
      camera_suite/scenes/camera_suite_scene_config.h
  22. 51 0
      camera_suite/scenes/camera_suite_scene_guide.c
  23. 73 0
      camera_suite/scenes/camera_suite_scene_menu.c
  24. 226 0
      camera_suite/scenes/camera_suite_scene_settings.c
  25. 55 0
      camera_suite/scenes/camera_suite_scene_start.c
  26. BIN
      camera_suite/screenshots/camera_preview.png
  27. BIN
      camera_suite/screenshots/guide.png
  28. BIN
      camera_suite/screenshots/main_menu.png
  29. BIN
      camera_suite/screenshots/settings.png
  30. BIN
      camera_suite/screenshots/start_screen.png
  31. 569 0
      camera_suite/views/camera_suite_view_camera.c
  32. 75 0
      camera_suite/views/camera_suite_view_camera.h
  33. 119 0
      camera_suite/views/camera_suite_view_guide.c
  34. 19 0
      camera_suite/views/camera_suite_view_guide.h
  35. 126 0
      camera_suite/views/camera_suite_view_start.c
  36. 19 0
      camera_suite/views/camera_suite_view_start.h

+ 1 - 0
camera_suite/.gitsubtree

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

+ 28 - 0
camera_suite/LICENSE

@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2023, Zalán Kórósi
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 17 - 0
camera_suite/application.fam

@@ -0,0 +1,17 @@
+App(
+    appid="camera_suite",
+    apptype=FlipperAppType.EXTERNAL,
+    cdefines=["APP_CAMERA_SUITE"],
+    entry_point="camera_suite_app",
+    fap_author="@CodyTolene @Z4urce @leedave",
+    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_version="1.3",
+    fap_weburl="https://github.com/CodyTolene/Flipper-Zero-Cam",
+    name="[ESP32] Camera Suite",
+    order=1,
+    requires=["gui", "storage"],
+    stack_size=8 * 1024,
+)

+ 135 - 0
camera_suite/camera_suite.c

@@ -0,0 +1,135 @@
+#include "camera_suite.h"
+#include <stdlib.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.
+    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->orientation = 0; // Orientation is "portrait", zero degrees by default.
+    app->dither = 0; // Dither algorithm is "Floyd Steinberg" by default.
+    app->flash = 1; // Flash is enabled by default.
+    app->haptic = 1; // Haptic is enabled by default
+    app->jpeg = 0; // Save JPEG to ESP32-CAM sd-card is disabled by default.
+    app->speaker = 1; // Speaker is enabled by default
+    app->led = 1; // LED is enabled by default
+
+    // 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_camera = camera_suite_view_camera_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CameraSuiteViewIdCamera,
+        camera_suite_view_camera_get_view(app->camera_suite_view_camera));
+
+    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, CameraSuiteViewIdStartscreen);
+    view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, CameraSuiteViewIdCamera);
+    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);
+
+    // Free remaining resources
+    camera_suite_view_start_free(app->camera_suite_view_start);
+    camera_suite_view_camera_free(app->camera_suite_view_camera);
+    camera_suite_view_guide_free(app->camera_suite_view_guide);
+    button_menu_free(app->button_menu);
+    variable_item_list_free(app->variable_item_list);
+
+    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;
+}

+ 86 - 0
camera_suite/camera_suite.h

@@ -0,0 +1,86 @@
+#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_camera.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;
+    CameraSuiteViewCamera* camera_suite_view_camera;
+    CameraSuiteViewGuide* camera_suite_view_guide;
+    uint32_t orientation;
+    uint32_t dither;
+    uint32_t flash;
+    uint32_t haptic;
+    uint32_t jpeg;
+    uint32_t speaker;
+    uint32_t led;
+    ButtonMenu* button_menu;
+} CameraSuite;
+
+typedef enum {
+    CameraSuiteViewIdStartscreen,
+    CameraSuiteViewIdMenu,
+    CameraSuiteViewIdCamera,
+    CameraSuiteViewIdGuide,
+    CameraSuiteViewIdSettings,
+} CameraSuiteViewId;
+
+typedef enum {
+    CameraSuiteOrientation0,
+    CameraSuiteOrientation90,
+    CameraSuiteOrientation180,
+    CameraSuiteOrientation270,
+} CameraSuiteOrientationState;
+
+typedef enum {
+    CameraSuiteDitherFloydSteinberg,
+    CameraSuiteDitherStucki,
+    CameraSuiteDitherJarvisJudiceNinke,
+} CameraSuiteDitherState;
+
+typedef enum {
+    CameraSuiteFlashOff,
+    CameraSuiteFlashOn,
+} CameraSuiteFlashState;
+
+typedef enum {
+    CameraSuiteJpegOff,
+    CameraSuiteJpegOn,
+} CameraSuiteJpegState;
+
+typedef enum {
+    CameraSuiteHapticOff,
+    CameraSuiteHapticOn,
+} CameraSuiteHapticState;
+
+typedef enum {
+    CameraSuiteSpeakerOff,
+    CameraSuiteSpeakerOn,
+} CameraSuiteSpeakerState;
+
+typedef enum {
+    CameraSuiteLedOff,
+    CameraSuiteLedOn,
+} CameraSuiteLedState;

+ 48 - 0
camera_suite/docs/CHANGELOG.md

@@ -0,0 +1,48 @@
+## Roadmap
+
+- Store images to onboard ESP32-CAM SD card (currently in progress, #24).
+- Camera preview GUI overlay (#21).
+- Full screen 90 degree and 270 degree fill (#6).
+
+## v1.3 (current)
+
+- Important: Firmware Update Required! Ensure you update your firmware to fully utilize the new features. Backwards compatibility should be ok.
+- New Feature: Introducing the Firmware Flash utility, simplifying the firmware flashing process. Refer to the project readme for detailed instructions. (Closes #26)
+- Enhancement: Flash functionality now remains active during camera preview, making it easier to take pictures in areas of low light.
+- Bug Fix: Addressed picture inversion issue reported by user leedave. Thanks for your contribution! (Closes #23)
+- Code Refinement: Enhanced firmware code for readability and maintainability. Separated concerns into individual files for a more organized structure.
+- Technical Improvements: Implemented general code enhancements and introduced syntactic sugar for cleaner, more efficient code.
+- Work in Progress: Added a new test function for saving pictures to the onboard ESP32-CAM SD card. This feature is under development and will allow users to save pictures directly to the SD card in the future. Tracked under feature request #24.
+
+## v1.2
+
+- Save image support. When the center button is pressed take a picture and save it to the "DCIM" folder at the root of your SD card. The image will be saved as a bitmap file with a timestamp as the filename ("YYYYMMDD-HHMMSS.bmp").
+- Camera flash support. Flashes the ESP32-CAM onboard LED when a picture is taken if enabled in the settings.
+- Move the camera dithering type to the settings scene as a new configurable option.
+- Add "Flash" option to the settings scene as a new configurable option.
+- Update documentation to reflect changes.
+- Update firmware with new dithering options set.
+- Update firmware with new flash support.
+- Update repo to reflect https://github.com/CodyTolene/Flipper-Zero-Development-Toolkit for easier tooling.
+
+## v1.1
+
+- Support and picture stabilization for all camera orientations (0 degree, 90 degree, 180 degree, and 270 degree).
+- Rename "Scene 1" to "Camera". No UX changes there.
+- Clean up unused "Scene 2". This was inaccessible to users previously and unused.
+- Add new dithering variations (requires the latest firmware installation, see here for the installation guide https://github.com/CodyTolene/Flipper-Zero-Camera-Suite#firmware-installation):
+  - "Jarvis Judice Ninke" dithering option
+  - "Stucki" dithering option.
+  - "Floyd-Steinberg" dithering option.
+  - Cycle through the dithering options with the center button on the Flipper Zero.
+- Resolves issue https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues/7
+- Resolves issue https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/pull/17
+
+## v1.0
+
+- Builds upon Z4urce's software found here (updated 6 months ago): https://github.com/Z4urce/flipperzero-camera
+- Utilizes the superb C boilerplate examples laid out by leedave (updated last month): https://github.com/leedave/flipper-zero-fap-boilerplate
+- Builds upon the "Camera" software into the new "Camera Suite" application with new usage:
+  - Add a scene for a guide.
+  - Add a scene for settings.
+  - Add ability to rotate the camera orientation.

+ 37 - 0
camera_suite/docs/README.md

@@ -0,0 +1,37 @@
+## Flipper Zero - Camera Suite
+
+Software to run an ESP32-CAM module on your Flipper Zero device.
+
+Full setup, wiring guide, etc. in the main project README here: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite
+
+Firmware is needed for the ESP32-CAM module, see here for more information: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite#firmware-installation
+
+## Software Guide
+
+Button mappings:
+
+**Up** = Contrast Up
+
+**Down** = Contrast Down
+
+**Left** = Toggle invert.
+
+**Right** = Toggle dithering on/off.
+
+**Center** = Take a picture and save to the "DCIM" folder at the root of your SD card. Image will be saved as a bitmap file with a timestamp as the filename ("YYYYMMDD-HHMMSS.bmp"). If flash is on in the settings (enabled by default) the ESP32-CAM onboard LED will light up when the picture is taken.
+
+**Back** = Go back.
+
+Settings:
+
+**Orientation** = Rotate the camera image 90 degrees counter-clockwise starting at zero by default (0, 90, 180, 270). This is useful if you have your camera module mounted in a different orientation than the default.
+
+**Flash** Toggle the ESP32-CAM onboard LED on/off when taking a picture.
+
+**Dithering Type** Change between the Cycle Floyd–Steinberg, Jarvis-Judice-Ninke, and Stucki dithering types.
+
+**Haptic FX** = Toggle haptic feedback on/off.
+
+**Sound FX** = Toggle sound effects on/off.
+
+**LED FX** = Toggle LED effects on/off.

+ 67 - 0
camera_suite/helpers/camera_suite_custom_event.h

@@ -0,0 +1,67 @@
+#pragma once
+
+typedef enum {
+    // Scene events: Start menu
+    CameraSuiteCustomEventStartUp,
+    CameraSuiteCustomEventStartDown,
+    CameraSuiteCustomEventStartLeft,
+    CameraSuiteCustomEventStartRight,
+    CameraSuiteCustomEventStartOk,
+    CameraSuiteCustomEventStartBack,
+    // Scene events: Camera
+    CameraSuiteCustomEventSceneCameraUp,
+    CameraSuiteCustomEventSceneCameraDown,
+    CameraSuiteCustomEventSceneCameraLeft,
+    CameraSuiteCustomEventSceneCameraRight,
+    CameraSuiteCustomEventSceneCameraOk,
+    CameraSuiteCustomEventSceneCameraBack,
+    // 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
camera_suite/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
camera_suite/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
camera_suite/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
camera_suite/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
camera_suite/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
camera_suite/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);

+ 120 - 0
camera_suite/helpers/camera_suite_storage.c

@@ -0,0 +1,120 @@
+#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_ORIENTATION, &app->orientation, 1);
+    flipper_format_write_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_DITHER, &app->dither, 1);
+    flipper_format_write_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_FLASH, &app->flash, 1);
+    flipper_format_write_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_JPEG, &app->jpeg, 1);
+    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;
+    }
+
+    // Read settings
+    flipper_format_read_uint32(
+        fff_file, BOILERPLATE_SETTINGS_KEY_ORIENTATION, &app->orientation, 1);
+    flipper_format_read_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_DITHER, &app->dither, 1);
+    flipper_format_read_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_FLASH, &app->flash, 1);
+    flipper_format_read_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_JPEG, &app->jpeg, 1);
+    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();
+}

+ 29 - 0
camera_suite/helpers/camera_suite_storage.h

@@ -0,0 +1,29 @@
+#include <stdlib.h>
+#include <string.h>
+#include <storage/storage.h>
+#include <flipper_format/flipper_format_i.h>
+
+#include "../camera_suite.h"
+
+#ifndef CAMERA_SUITE_STORAGE_H
+#define CAMERA_SUITE_STORAGE_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_ORIENTATION "Orientation"
+#define BOILERPLATE_SETTINGS_KEY_DITHER "Dither"
+#define BOILERPLATE_SETTINGS_KEY_FLASH "Flash"
+#define BOILERPLATE_SETTINGS_KEY_JPEG "SaveJPEG"
+#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);
+
+#endif

BIN
camera_suite/icons/camera_suite.png


+ 30 - 0
camera_suite/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
camera_suite/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

+ 52 - 0
camera_suite/scenes/camera_suite_scene_camera.c

@@ -0,0 +1,52 @@
+#include "../camera_suite.h"
+#include "../helpers/camera_suite_custom_event.h"
+#include "../views/camera_suite_view_camera.h"
+
+void camera_suite_view_camera_callback(CameraSuiteCustomEvent event, void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void camera_suite_scene_camera_on_enter(void* context) {
+    furi_assert(context);
+    CameraSuite* app = context;
+    camera_suite_view_camera_set_callback(
+        app->camera_suite_view_camera, camera_suite_view_camera_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, CameraSuiteViewIdCamera);
+}
+
+bool camera_suite_scene_camera_on_event(void* context, SceneManagerEvent event) {
+    CameraSuite* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case CameraSuiteCustomEventSceneCameraLeft:
+        case CameraSuiteCustomEventSceneCameraRight:
+        case CameraSuiteCustomEventSceneCameraUp:
+        case CameraSuiteCustomEventSceneCameraDown:
+        case CameraSuiteCustomEventSceneCameraOk:
+            // Do nothing.
+            break;
+        case CameraSuiteCustomEventSceneCameraBack:
+            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_camera_on_exit(void* context) {
+    CameraSuite* app = context;
+    UNUSED(app);
+}

+ 5 - 0
camera_suite/scenes/camera_suite_scene_config.h

@@ -0,0 +1,5 @@
+ADD_SCENE(camera_suite, start, Start)
+ADD_SCENE(camera_suite, menu, Menu)
+ADD_SCENE(camera_suite, camera, Camera)
+ADD_SCENE(camera_suite, guide, Guide)
+ADD_SCENE(camera_suite, settings, Settings)

+ 51 - 0
camera_suite/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);
+}

+ 73 - 0
camera_suite/scenes/camera_suite_scene_menu.c

@@ -0,0 +1,73 @@
+#include "../camera_suite.h"
+
+enum SubmenuIndex {
+    /** Camera. */
+    SubmenuIndexSceneCamera = 10,
+    /** 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,
+        "Open Camera",
+        SubmenuIndexSceneCamera,
+        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 == SubmenuIndexSceneCamera) {
+            scene_manager_set_scene_state(
+                app->scene_manager, CameraSuiteSceneMenu, SubmenuIndexSceneCamera);
+            scene_manager_next_scene(app->scene_manager, CameraSuiteSceneCamera);
+            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);
+}

+ 226 - 0
camera_suite/scenes/camera_suite_scene_settings.c

@@ -0,0 +1,226 @@
+#include "../camera_suite.h"
+#include <lib/toolbox/value_index.h>
+
+// Camera orientation, in degrees.
+const char* const orientation_text[4] = {
+    "0",
+    "90",
+    "180",
+    "270",
+};
+
+const uint32_t orientation_value[4] = {
+    CameraSuiteOrientation0,
+    CameraSuiteOrientation90,
+    CameraSuiteOrientation180,
+    CameraSuiteOrientation270,
+};
+
+// Possible dithering types for the camera.
+const char* const dither_text[28] = {
+    "Floyd-Steinberg",
+    "Stucki",
+    "Jarvis-Judice-Ninke",
+};
+
+const uint32_t dither_value[4] = {
+    CameraSuiteDitherFloydSteinberg,
+    CameraSuiteDitherStucki,
+    CameraSuiteDitherJarvisJudiceNinke,
+};
+
+const char* const flash_text[2] = {
+    "OFF",
+    "ON",
+};
+
+const uint32_t flash_value[2] = {
+    CameraSuiteFlashOff,
+    CameraSuiteFlashOn,
+};
+
+const char* const jpeg_text[2] = {
+    "OFF",
+    "ON",
+};
+
+const uint32_t jpeg_value[2] = {
+    CameraSuiteJpegOff,
+    CameraSuiteJpegOn,
+};
+
+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_camera_orientation(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, orientation_text[index]);
+    app->orientation = orientation_value[index];
+}
+
+static void camera_suite_scene_settings_set_camera_dither(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, dither_text[index]);
+    app->dither = dither_value[index];
+}
+
+static void camera_suite_scene_settings_set_flash(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, flash_text[index]);
+    app->flash = flash_value[index];
+}
+
+static void camera_suite_scene_settings_set_jpeg(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, jpeg_text[index]);
+    app->jpeg = jpeg_value[index];
+}
+
+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;
+
+    // Camera Orientation
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Orientation:",
+        4,
+        camera_suite_scene_settings_set_camera_orientation,
+        app);
+    value_index = value_index_uint32(app->orientation, orientation_value, 4);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, orientation_text[value_index]);
+
+    // Camera Dither Type
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Dithering Type:",
+        3,
+        camera_suite_scene_settings_set_camera_dither,
+        app);
+    value_index = value_index_uint32(app->dither, dither_value, 3);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, dither_text[value_index]);
+
+    // Flash ON/OFF
+    item = variable_item_list_add(
+        app->variable_item_list, "Flash:", 2, camera_suite_scene_settings_set_flash, app);
+    value_index = value_index_uint32(app->flash, flash_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, flash_text[value_index]);
+
+    // @todo - Save picture to ESP32-CAM sd-card instead of Flipper Zero
+    // sd-card. This hides the setting for it, for now.
+    // Save JPEG to ESP32-CAM sd-card instead of Flipper Zero sd-card ON/OFF
+    // item = variable_item_list_add(
+    //     app->variable_item_list,
+    //     "Save JPEG to ext sdcard:",
+    //     2,
+    //     camera_suite_scene_settings_set_jpeg,
+    //     app);
+    // value_index = value_index_uint32(app->jpeg, jpeg_value, 2);
+    // variable_item_set_current_value_index(item, value_index);
+    // variable_item_set_current_value_text(item, jpeg_text[value_index]);
+    UNUSED(camera_suite_scene_settings_set_jpeg);
+
+    // 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
camera_suite/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:
+        case CameraSuiteCustomEventStartUp:
+        case CameraSuiteCustomEventStartDown:
+            // Do nothing.
+            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);
+}

BIN
camera_suite/screenshots/camera_preview.png


BIN
camera_suite/screenshots/guide.png


BIN
camera_suite/screenshots/main_menu.png


BIN
camera_suite/screenshots/settings.png


BIN
camera_suite/screenshots/start_screen.png


+ 569 - 0
camera_suite/views/camera_suite_view_camera.c

@@ -0,0 +1,569 @@
+#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"
+
+static void draw_pixel_by_orientation(Canvas* canvas, uint8_t x, uint8_t y, uint8_t orientation) {
+    furi_assert(canvas);
+    furi_assert(x);
+    furi_assert(y);
+    furi_assert(orientation);
+
+    switch(orientation) {
+    default:
+    case 0: { // Camera rotated 0 degrees (right side up, default)
+        canvas_draw_dot(canvas, x, y);
+        break;
+    }
+    case 1: { // Camera rotated 90 degrees
+
+        canvas_draw_dot(canvas, y, FRAME_WIDTH - 1 - x);
+        break;
+    }
+    case 2: { // Camera rotated 180 degrees (upside down)
+        canvas_draw_dot(canvas, FRAME_WIDTH - 1 - x, FRAME_HEIGHT - 1 - y);
+        break;
+    }
+    case 3: { // Camera rotated 270 degrees
+        canvas_draw_dot(canvas, FRAME_HEIGHT - 1 - y, x);
+        break;
+    }
+    }
+}
+
+static void camera_suite_view_camera_draw(Canvas* canvas, void* model) {
+    furi_assert(canvas);
+    furi_assert(model);
+
+    UartDumpModel* uartDumpModel = model;
+
+    // Clear the screen.
+    canvas_set_color(canvas, ColorBlack);
+
+    // Draw the frame.
+    canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGHT);
+
+    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((uartDumpModel->pixels[p] & (1 << (7 - i))) != 0) {
+                draw_pixel_by_orientation(canvas, (x * 8) + i, y, uartDumpModel->orientation);
+            }
+        }
+    }
+
+    // Draw the guide if the camera is not initialized.
+    if(!uartDumpModel->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 save_image(void* model) {
+    furi_assert(model);
+
+    UartDumpModel* uartDumpModel = model;
+
+    // This pointer is used to access the storage.
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    // This pointer is used to access the filesystem.
+    File* file = storage_file_alloc(storage);
+
+    // Store path in local variable.
+    const char* folderName = EXT_PATH("DCIM");
+
+    // Create the folder name for the image file if it does not exist.
+    if(storage_common_stat(storage, folderName, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(storage, folderName);
+    }
+
+    // This pointer is used to access the file name.
+    FuriString* file_name = furi_string_alloc();
+
+    // Get the current date and time.
+    FuriHalRtcDateTime datetime = {0};
+    furi_hal_rtc_get_datetime(&datetime);
+
+    // Create the file name.
+    furi_string_printf(
+        file_name,
+        EXT_PATH("DCIM/%.4d%.2d%.2d-%.2d%.2d%.2d.bmp"),
+        datetime.year,
+        datetime.month,
+        datetime.day,
+        datetime.hour,
+        datetime.minute,
+        datetime.second);
+
+    // Open the file for writing. If the file does not exist (it shouldn't),
+    // create it.
+    bool result =
+        storage_file_open(file, furi_string_get_cstr(file_name), FSAM_WRITE, FSOM_OPEN_ALWAYS);
+
+    // Free the file name after use.
+    furi_string_free(file_name);
+
+    if(!uartDumpModel->inverted) {
+        for(size_t i = 0; i < FRAME_BUFFER_LENGTH; ++i) {
+            uartDumpModel->pixels[i] = ~uartDumpModel->pixels[i];
+        }
+    }
+
+    // If the file was opened successfully, write the bitmap header and the
+    // image data.
+    if(result) {
+        // Write BMP Header
+        storage_file_write(file, bitmap_header, BITMAP_HEADER_LENGTH);
+
+        // @todo - Add a function for saving the image directly from the
+        // ESP32-CAM to the Flipper Zero SD card.
+
+        // Write locally to the Flipper Zero SD card in the DCIM folder.
+        int8_t row_buffer[ROW_BUFFER_LENGTH];
+
+        // @todo - Save image based on orientation.
+        for(size_t i = 64; i > 0; --i) {
+            for(size_t j = 0; j < ROW_BUFFER_LENGTH; ++j) {
+                row_buffer[j] = uartDumpModel->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j];
+            }
+            storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH);
+        }
+    }
+
+    // Close the file.
+    storage_file_close(file);
+
+    // Free up memory.
+    storage_file_free(file);
+}
+
+static void
+    camera_suite_view_camera_model_init(UartDumpModel* const model, CameraSuite* instance_context) {
+    furi_assert(model);
+    furi_assert(instance_context);
+
+    for(size_t i = 0; i < FRAME_BUFFER_LENGTH; i++) {
+        model->pixels[i] = 0;
+    }
+
+    uint32_t orientation = instance_context->orientation;
+    model->flash = instance_context->flash;
+    model->inverted = false;
+    model->orientation = orientation;
+}
+
+static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    furi_assert(event);
+
+    CameraSuiteViewCamera* instance = context;
+
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        default: // Stop all sounds, reset the LED.
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    camera_suite_play_bad_bump(instance->context);
+                    camera_suite_stop_all_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 0, 0, 0);
+                },
+                true);
+            break;
+        }
+    } else if(event->type == InputTypePress) {
+        uint8_t data[1] = {'X'};
+        switch(event->key) {
+        // Camera: Stop stream.
+        case InputKeyBack: {
+            // Set the camera flash to off.
+            uint8_t flash_off = 'f';
+            furi_hal_uart_tx(FuriHalUartIdUSART1, &flash_off, 1);
+            furi_delay_ms(50);
+            // Stop camera stream.
+            uint8_t stop_camera = 's';
+            furi_hal_uart_tx(FuriHalUartIdUSART1, &stop_camera, 1);
+            // Go back to the main menu.
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(CameraSuiteCustomEventSceneCameraBack, instance->context);
+                },
+                true);
+            break;
+        }
+        // Camera: Toggle invert on the ESP32-CAM.
+        case InputKeyLeft: {
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 0, 0, 255);
+                    if(model->inverted) {
+                        data[0] = 'i';
+                        model->inverted = false;
+                    } else {
+                        data[0] = 'I';
+                        model->inverted = true;
+                    }
+                    instance->callback(CameraSuiteCustomEventSceneCameraLeft, instance->context);
+                },
+                true);
+            break;
+        }
+        // Camera: Enable/disable dithering.
+        case InputKeyRight: {
+            data[0] = '>';
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 0, 0, 255);
+                    instance->callback(CameraSuiteCustomEventSceneCameraRight, instance->context);
+                },
+                true);
+            break;
+        }
+        // Camera: Increase contrast.
+        case InputKeyUp: {
+            data[0] = 'C';
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 0, 0, 255);
+                    instance->callback(CameraSuiteCustomEventSceneCameraUp, instance->context);
+                },
+                true);
+            break;
+        }
+        // Camera: Reduce contrast.
+        case InputKeyDown: {
+            data[0] = 'c';
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    UNUSED(model);
+                    camera_suite_play_happy_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 0, 0, 255);
+                    instance->callback(CameraSuiteCustomEventSceneCameraDown, instance->context);
+                },
+                true);
+            break;
+        }
+        // Camera: Take picture.
+        case InputKeyOk: {
+            with_view_model(
+                instance->view,
+                UartDumpModel * model,
+                {
+                    camera_suite_play_long_bump(instance->context);
+                    camera_suite_play_input_sound(instance->context);
+                    camera_suite_led_set_rgb(instance->context, 0, 0, 255);
+
+                    // Save picture directly to ESP32-CAM.
+                    // @todo - Add this functionality.
+                    // data[0] = 'P';
+                    // furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+
+                    // if(model->flash) {
+                    //     data[0] = 'F';
+                    //     furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+                    //     furi_delay_ms(50);
+                    // }
+
+                    // Take a picture.
+                    save_image(model);
+
+                    // if(model->flash) {
+                    //     data[0] = 'f';
+                    // }
+                    instance->callback(CameraSuiteCustomEventSceneCameraOk, instance->context);
+                },
+                true);
+            break;
+        }
+        // Camera: Do nothing.
+        case InputKeyMAX:
+        default: {
+            break;
+        }
+        }
+
+        if(data[0] != 'X') {
+            // Send `data` to the ESP32-CAM.
+            furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+        }
+    }
+    return true;
+}
+
+static void camera_suite_view_camera_exit(void* context) {
+    UNUSED(context);
+
+    // Set the camera flash to off.
+    uint8_t flash_off = 'f';
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &flash_off, 1);
+    furi_delay_ms(50);
+
+    // Stop camera stream.
+    uint8_t stop_camera = 's';
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &stop_camera, 1);
+    furi_delay_ms(50);
+}
+
+static void camera_suite_view_camera_enter(void* context) {
+    furi_assert(context);
+
+    // Get the camera suite instance context.
+    CameraSuiteViewCamera* instance = (CameraSuiteViewCamera*)context;
+
+    // Get the camera suite instance context.
+    CameraSuite* instance_context = instance->context;
+
+    // Start camera stream.
+    uint8_t start_camera = 'S';
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &start_camera, 1);
+    furi_delay_ms(75);
+
+    // Get/set dither type.
+    uint8_t dither_type = instance_context->dither;
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &dither_type, 1);
+    furi_delay_ms(75);
+
+    // Make sure the camera is not inverted.
+    uint8_t invert_camera = 'i';
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &invert_camera, 1);
+    furi_delay_ms(75);
+
+    // Toggle flash on or off based on the current state. This will keep the
+    // flash on initially. However we're toggling it for now on input.
+    uint8_t flash_state = instance_context->flash ? 'F' : 'f';
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &flash_state, 1);
+    furi_delay_ms(75);
+
+    // Make sure we start with the flash off.
+    // uint8_t flash_state = 'f';
+    // furi_hal_uart_tx(FuriHalUartIdUSART1, &flash_state, 1);
+    // furi_delay_ms(75);
+
+    with_view_model(
+        instance->view,
+        UartDumpModel * model,
+        { camera_suite_view_camera_model_init(model, instance_context); },
+        true);
+}
+
+static void camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* context) {
+    furi_assert(uartIrqEvent);
+    furi_assert(data);
+    furi_assert(context);
+
+    // Cast `context` to `CameraSuiteViewCamera*` and store it in `instance`.
+    CameraSuiteViewCamera* 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 const byte) {
+    furi_assert(model);
+    furi_assert(byte);
+
+    // The first HEADER_LENGTH bytes are reserved for header information.
+    if(model->ringbuffer_index < HEADER_LENGTH) {
+        // Validate the start of row characters 'Y' and ':'.
+        if(model->ringbuffer_index == 0 && byte != 'Y') {
+            // Incorrect start of frame; reset.
+            return;
+        }
+        if(model->ringbuffer_index == 1 && byte != ':') {
+            // Incorrect start of frame; reset.
+            model->ringbuffer_index = 0;
+            return;
+        }
+        if(model->ringbuffer_index == 2) {
+            // Assign the third byte as the row identifier.
+            model->row_identifier = byte;
+        }
+        model->ringbuffer_index++; // Increment index for the next byte.
+        return;
+    }
+
+    // Store pixel value directly after the header.
+    model->row_ringbuffer[model->ringbuffer_index - HEADER_LENGTH] = byte;
+    model->ringbuffer_index++; // Increment index for the next byte.
+
+    // Check whether the ring buffer is filled.
+    if(model->ringbuffer_index >= RING_BUFFER_LENGTH) {
+        model->ringbuffer_index = 0; // Reset the ring buffer index.
+        model->initialized = true; // Set the connection as successfully established.
+
+        // Compute the starting index for the row in the pixel buffer.
+        size_t row_start_index = model->row_identifier * ROW_BUFFER_LENGTH;
+
+        // Ensure the row start index is within the valid range.
+        if(row_start_index > LAST_ROW_INDEX) {
+            row_start_index = 0; // Reset to a safe value in case of an overflow.
+        }
+
+        // Flush the contents of the ring buffer to the pixel buffer.
+        for(size_t i = 0; i < ROW_BUFFER_LENGTH; ++i) {
+            model->pixels[row_start_index + i] = model->row_ringbuffer[i];
+        }
+    }
+}
+
+static int32_t camera_worker(void* context) {
+    furi_assert(context);
+
+    CameraSuiteViewCamera* 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);
+
+            with_view_model(
+                instance->view, UartDumpModel * model, { UNUSED(model); }, true);
+        }
+    }
+
+    return 0;
+}
+
+CameraSuiteViewCamera* camera_suite_view_camera_alloc() {
+    // Allocate memory for the instance
+    CameraSuiteViewCamera* instance = malloc(sizeof(CameraSuiteViewCamera));
+
+    // Allocate the view object
+    instance->view = view_alloc();
+
+    // Allocate a stream buffer
+    instance->rx_stream = furi_stream_buffer_alloc(2048, 1);
+
+    // Allocate model
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UartDumpModel));
+
+    // Set context for the view
+    view_set_context(instance->view, instance);
+
+    // Set draw callback
+    view_set_draw_callback(instance->view, (ViewDrawCallback)camera_suite_view_camera_draw);
+
+    // Set input callback
+    view_set_input_callback(instance->view, camera_suite_view_camera_input);
+
+    // Set enter callback
+    view_set_enter_callback(instance->view, camera_suite_view_camera_enter);
+
+    // Set exit callback
+    view_set_exit_callback(instance->view, camera_suite_view_camera_exit);
+
+    // Allocate a thread for this camera to run on.
+    FuriThread* thread = furi_thread_alloc_ex("UsbUartWorker", 2048, camera_worker, instance);
+    instance->worker_thread = thread;
+    furi_thread_start(instance->worker_thread);
+
+    // Enable uart listener
+    furi_hal_console_disable();
+
+    // 115200 is the default baud rate for the ESP32-CAM.
+    furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400);
+
+    // Enable UART1 and set the IRQ callback.
+    furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, camera_on_irq_cb, instance);
+
+    return instance;
+}
+
+void camera_suite_view_camera_free(CameraSuiteViewCamera* instance) {
+    furi_assert(instance);
+
+    // Remove the IRQ callback.
+    furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, NULL, NULL);
+
+    // Free the worker thread.
+    furi_thread_free(instance->worker_thread);
+
+    // Free the allocated stream buffer.
+    furi_stream_buffer_free(instance->rx_stream);
+
+    // Re-enable the console.
+    // furi_hal_console_enable();
+
+    with_view_model(
+        instance->view, UartDumpModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* instance) {
+    furi_assert(instance);
+    return instance->view;
+}
+
+void camera_suite_view_camera_set_callback(
+    CameraSuiteViewCamera* instance,
+    CameraSuiteViewCameraCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}

+ 75 - 0
camera_suite/views/camera_suite_view_camera.h

@@ -0,0 +1,75 @@
+#pragma once
+
+#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>
+
+#define BITMAP_HEADER_LENGTH 62
+#define FRAME_BIT_DEPTH 1
+#define FRAME_BUFFER_LENGTH 1024
+#define FRAME_HEIGHT 64
+#define FRAME_WIDTH 128
+#define HEADER_LENGTH 3 // 'Y', ':', and row identifier
+#define LAST_ROW_INDEX 1008
+#define RING_BUFFER_LENGTH 19
+#define ROW_BUFFER_LENGTH 16
+
+static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = {
+    0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00,
+    0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
+
+extern const Icon I_DolphinCommon_56x48;
+typedef enum {
+    WorkerEventReserved = (1 << 0), // Reserved for StreamBuffer internal event
+    WorkerEventStop = (1 << 1),
+    WorkerEventRx = (1 << 2),
+} WorkerEventFlags;
+
+#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx)
+
+// Forward declaration
+typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void* context);
+
+typedef struct CameraSuiteViewCamera {
+    CameraSuiteViewCameraCallback callback;
+    FuriStreamBuffer* rx_stream;
+    FuriThread* worker_thread;
+    NotificationApp* notification;
+    View* view;
+    void* context;
+} CameraSuiteViewCamera;
+
+typedef struct UartDumpModel {
+    bool flash;
+    bool initialized;
+    bool inverted;
+    int rotation_angle;
+    uint32_t orientation;
+    uint8_t pixels[FRAME_BUFFER_LENGTH];
+    uint8_t ringbuffer_index;
+    uint8_t row_identifier;
+    uint8_t row_ringbuffer[RING_BUFFER_LENGTH];
+} UartDumpModel;
+
+// Function Prototypes
+CameraSuiteViewCamera* camera_suite_view_camera_alloc();
+View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* camera_suite_static);
+void camera_suite_view_camera_free(CameraSuiteViewCamera* camera_suite_static);
+void camera_suite_view_camera_set_callback(
+    CameraSuiteViewCamera* camera_suite_view_camera,
+    CameraSuiteViewCameraCallback callback,
+    void* context);

+ 119 - 0
camera_suite/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 = Toggle invert");
+    canvas_draw_str_aligned(canvas, 0, 22, AlignLeft, AlignTop, "Right = Toggle dithering");
+    canvas_draw_str_aligned(canvas, 0, 32, AlignLeft, AlignTop, "Up = Contrast up");
+    canvas_draw_str_aligned(canvas, 0, 42, AlignLeft, AlignTop, "Down = Contrast 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
camera_suite/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
camera_suite/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
camera_suite/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);