MX 2 лет назад
Родитель
Сommit
eb0351e747

+ 28 - 0
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.

+ 1 - 0
application.fam

@@ -8,6 +8,7 @@ App(
     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,

+ 6 - 3
camera_suite.c

@@ -44,9 +44,12 @@ CameraSuite* camera_suite_app_alloc() {
 
     // Set defaults, in case no config loaded
     app->orientation = 0; // Orientation is "portrait", zero degrees by default.
-    app->haptic = 1; // Haptic is on by default
-    app->speaker = 1; // Speaker is on by default
-    app->led = 1; // LED is on 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);

+ 19 - 0
camera_suite.h

@@ -30,7 +30,10 @@ typedef struct {
     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;
@@ -51,6 +54,22 @@ typedef enum {
     CameraSuiteOrientation270,
 } CameraSuiteOrientationState;
 
+typedef enum {
+    CameraSuiteDitherFloydSteinberg,
+    CameraSuiteDitherStucki,
+    CameraSuiteDitherJarvisJudiceNinke,
+} CameraSuiteDitherState;
+
+typedef enum {
+    CameraSuiteFlashOff,
+    CameraSuiteFlashOn,
+} CameraSuiteFlashState;
+
+typedef enum {
+    CameraSuiteJpegOff,
+    CameraSuiteJpegOn,
+} CameraSuiteJpegState;
+
 typedef enum {
     CameraSuiteHapticOff,
     CameraSuiteHapticOn,

+ 37 - 9
docs/CHANGELOG.md

@@ -1,12 +1,40 @@
+## 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°, 90°, 180°, 270°).
-- Rename "Scene 1" to "Camera". No UX changes, strictly internal.
+- 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 (needs new module firmware, see https://github.com/CodyTolene/Flipper-Zero-Camera-Suite#firmware-installation):
-  - Add `Jarvis Judice` Ninke Dithering option
-  - Add `Stucki` dithering option.
-  - Add ability to toggle dithering options from default `Floyd-Steinberg` and back.
+- 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
 
@@ -14,7 +42,7 @@
 
 - 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
-- Repurpose and build upon the "[ESP32] Camera" software into the new "[ESP32] Camera Suite" application with new purpose:
-  - Adding more scene for a guide.
-  - Adding more scene for saveable settings.
+- 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.

+ 17 - 15
docs/README.md

@@ -2,34 +2,36 @@
 
 Software to run an ESP32-CAM module on your Flipper Zero device.
 
-## Software Guide <a name="software-guide"></a>
+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
 
-### Flipper Zero button mappings:
+Button mappings:
 
-🔼 = Contrast Up
+**Up** = Contrast Up
 
-🔽 = Contrast Down
+**Down** = Contrast Down
 
-◀️ = Toggle invert.
+**Left** = Toggle invert.
 
-▶️ = Toggle dithering on/off.
+**Right** = Toggle dithering on/off.
 
-⚪ = Cycle Floyd–Steinberg/Jarvis-Judice-Ninke/Stucki dithering types.
+**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.
 
-↩️ = Go back.
+**Back** = Go back.
 
-### Camera Suite settings:
+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.
-
-## Links
-
-Full setup, wiring guide, etc. in the main project README here: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite
-
-A firmware is needed for the ESP32-CAM module, see here for more information: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite#firmware-installation

+ 7 - 0
helpers/camera_suite_storage.c

@@ -51,6 +51,9 @@ void camera_suite_save_settings(void* context) {
         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);
@@ -100,8 +103,12 @@ void camera_suite_read_settings(void* context) {
         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);

+ 3 - 0
helpers/camera_suite_storage.h

@@ -10,6 +10,9 @@
 #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"

+ 89 - 0
scenes/camera_suite_scene_settings.c

@@ -16,6 +16,39 @@ const uint32_t orientation_value[4] = {
     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",
@@ -54,6 +87,30 @@ static void camera_suite_scene_settings_set_camera_orientation(VariableItem* ite
     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);
@@ -97,6 +154,38 @@ void camera_suite_scene_settings_on_enter(void* context) {
     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);

BIN
screenshots/settings.png


+ 299 - 104
views/camera_suite_view_camera.c

@@ -8,48 +8,39 @@
 #include "../helpers/camera_suite_speaker.h"
 #include "../helpers/camera_suite_led.h"
 
-static CameraSuiteViewCamera* current_instance = NULL;
-
-struct CameraSuiteViewCamera {
-    CameraSuiteViewCameraCallback callback;
-    FuriStreamBuffer* rx_stream;
-    FuriThread* worker_thread;
-    View* view;
-    void* context;
-};
-
-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;
-}
-
-// Function to draw pixels on the canvas based on camera orientation
 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) {
-    case 0: // Camera rotated 0 degrees (right side up, default)
+    default:
+    case 0: { // Camera rotated 0 degrees (right side up, default)
         canvas_draw_dot(canvas, x, y);
         break;
-    case 1: // Camera rotated 90 degrees
+    }
+    case 1: { // Camera rotated 90 degrees
+
         canvas_draw_dot(canvas, y, FRAME_WIDTH - 1 - x);
         break;
-    case 2: // Camera rotated 180 degrees (upside down)
+    }
+    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
+    }
+    case 3: { // Camera rotated 270 degrees
         canvas_draw_dot(canvas, FRAME_HEIGHT - 1 - y, x);
         break;
-    default:
-        break;
+    }
     }
 }
 
-static void camera_suite_view_camera_draw(Canvas* canvas, void* _model) {
-    UartDumpModel* model = _model;
+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);
@@ -57,21 +48,19 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* _model) {
     // Draw the frame.
     canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGHT);
 
-    CameraSuite* app = current_instance->context;
-
     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) {
-                draw_pixel_by_orientation(canvas, (x * 8) + i, y, app->orientation);
+            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(!model->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");
@@ -82,15 +71,106 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* _model) {
     }
 }
 
-static void camera_suite_view_camera_model_init(UartDumpModel* const model) {
+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.
@@ -106,13 +186,18 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                 true);
             break;
         }
-        // Send `data` to the ESP32-CAM
     } else if(event->type == InputTypePress) {
-        uint8_t data[1];
+        uint8_t data[1] = {'X'};
         switch(event->key) {
-        case InputKeyBack:
-            // Stop the camera stream.
-            data[0] = 's';
+        // 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,
@@ -123,9 +208,9 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                 },
                 true);
             break;
-        case InputKeyLeft:
-            // Camera: Invert.
-            data[0] = '<';
+        }
+        // Camera: Toggle invert on the ESP32-CAM.
+        case InputKeyLeft: {
             with_view_model(
                 instance->view,
                 UartDumpModel * model,
@@ -134,12 +219,20 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                     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;
-        case InputKeyRight:
-            // Camera: Enable/disable dithering.
+        }
+        // Camera: Enable/disable dithering.
+        case InputKeyRight: {
             data[0] = '>';
             with_view_model(
                 instance->view,
@@ -153,8 +246,9 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                 },
                 true);
             break;
-        case InputKeyUp:
-            // Camera: Increase contrast.
+        }
+        // Camera: Increase contrast.
+        case InputKeyUp: {
             data[0] = 'C';
             with_view_model(
                 instance->view,
@@ -168,8 +262,9 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                 },
                 true);
             break;
-        case InputKeyDown:
-            // Camera: Reduce contrast.
+        }
+        // Camera: Reduce contrast.
+        case InputKeyDown: {
             data[0] = 'c';
             with_view_model(
                 instance->view,
@@ -183,58 +278,113 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                 },
                 true);
             break;
-        case InputKeyOk:
-            // Switch dithering types.
-            data[0] = 'D';
+        }
+        // Camera: Take picture.
+        case InputKeyOk: {
             with_view_model(
                 instance->view,
                 UartDumpModel * model,
                 {
-                    UNUSED(model);
-                    camera_suite_play_happy_bump(instance->context);
+                    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;
         }
-        // Send `data` to the ESP32-CAM
-        furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+        }
+
+        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) {
-    furi_assert(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) {
-    // Check `context` for null. If it is null, abort program, else continue.
     furi_assert(context);
 
-    // Cast `context` to `CameraSuiteViewCamera*` and store it in `instance`.
+    // Get the camera suite instance context.
     CameraSuiteViewCamera* instance = (CameraSuiteViewCamera*)context;
 
-    // Assign the current instance to the global variable
-    current_instance = instance;
+    // 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);
 
-    uint8_t data[1];
-    data[0] = 'S'; // Uppercase `S` to start the camera
-    // Send `data` to the ESP32-CAM
-    furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+    // 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); },
+        { camera_suite_view_camera_model_init(model, instance_context); },
         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(uartIrqEvent);
+    furi_assert(data);
     furi_assert(context);
 
     // Cast `context` to `CameraSuiteViewCamera*` and store it in `instance`.
@@ -248,47 +398,57 @@ static void camera_on_irq_cb(UartIrqEvent uartIrqEvent, uint8_t data, void* cont
     }
 }
 
-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;
-    }
+static void process_ringbuffer(UartDumpModel* model, uint8_t const byte) {
+    furi_assert(model);
+    furi_assert(byte);
 
-    // Second char has to be ':' in the buffer or reset.
-    if(model->ringbuffer_index == 1 && byte != ':') {
-        model->ringbuffer_index = 0;
-        process_ringbuffer(model, 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;
     }
 
-    // Assign current byte to the ringbuffer.
-    model->row_ringbuffer[model->ringbuffer_index] = byte;
-    // Increment the ringbuffer index.
-    ++model->ringbuffer_index;
+    // 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.
 
-    // Let's wait 'till the buffer fills.
-    if(model->ringbuffer_index < RING_BUFFER_LENGTH) {
-        return;
-    }
+    // 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.
 
-    // 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
+        // Compute the starting index for the row in the pixel buffer.
+        size_t row_start_index = model->row_identifier * ROW_BUFFER_LENGTH;
 
-    if(row_start_index > LAST_ROW_INDEX) { // Failsafe
-        row_start_index = 0;
-    }
+        // 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.
+        }
 
-    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
+        // 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) {
@@ -328,32 +488,45 @@ static int32_t camera_worker(void* context) {
 }
 
 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);
 
-    // Set up views
+    // Allocate model
     view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(UartDumpModel));
-    view_set_context(instance->view, instance); // furi_assert crashes in events without this
+
+    // 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);
-    view_set_exit_callback(instance->view, camera_suite_view_camera_exit);
 
-    with_view_model(
-        instance->view,
-        UartDumpModel * model,
-        { camera_suite_view_camera_model_init(model); },
-        true);
+    // Set exit callback
+    view_set_exit_callback(instance->view, camera_suite_view_camera_exit);
 
-    instance->worker_thread = furi_thread_alloc_ex("UsbUartWorker", 2048, camera_worker, instance);
+    // 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;
@@ -362,6 +535,18 @@ CameraSuiteViewCamera* camera_suite_view_camera_alloc() {
 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);
@@ -371,4 +556,14 @@ void camera_suite_view_camera_free(CameraSuiteViewCamera* 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;
 }

+ 42 - 28
views/camera_suite_view_camera.h

@@ -1,3 +1,5 @@
+#pragma once
+
 #include "../helpers/camera_suite_custom_event.h"
 #include <furi.h>
 #include <furi_hal.h>
@@ -14,48 +16,60 @@
 #include <storage/filesystem_api_defines.h>
 #include <storage/storage.h>
 
-#pragma once
-
-#define FRAME_WIDTH 128
-#define FRAME_HEIGHT 64
+#define BITMAP_HEADER_LENGTH 62
 #define FRAME_BIT_DEPTH 1
 #define FRAME_BUFFER_LENGTH 1024
-#define ROW_BUFFER_LENGTH 16
-#define RING_BUFFER_LENGTH 19
+#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 UartDumpModel UartDumpModel;
+typedef struct CameraSuiteViewCamera {
+    CameraSuiteViewCameraCallback callback;
+    FuriStreamBuffer* rx_stream;
+    FuriThread* worker_thread;
+    NotificationApp* notification;
+    View* view;
+    void* context;
+} CameraSuiteViewCamera;
 
-struct UartDumpModel {
+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];
-};
-
-typedef struct CameraSuiteViewCamera CameraSuiteViewCamera;
-
-typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void* context);
+} 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);
-
-CameraSuiteViewCamera* camera_suite_view_camera_alloc();
-
-void camera_suite_view_camera_free(CameraSuiteViewCamera* camera_suite_static);
-
-View* camera_suite_view_camera_get_view(CameraSuiteViewCamera* 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)

+ 5 - 6
views/camera_suite_view_guide.c

@@ -32,12 +32,11 @@ void camera_suite_view_guide_draw(Canvas* canvas, CameraSuiteViewGuideModel* mod
     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");
-    // TODO: Possibly update to take picture instead.
-    canvas_draw_str_aligned(canvas, 0, 52, AlignLeft, AlignTop, "Center = Toggle Dither Type");
+    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) {