Explorar o código

Merge pull request #22 from CodyTolene/ct/picture-and-flash-support

Save picture support + flash support + configurable dithering in settings.
Cody Tolene %!s(int64=2) %!d(string=hai) anos
pai
achega
1fee78d323

+ 0 - 190
.clang-format

@@ -1,190 +0,0 @@
----
-Language:        Cpp
-AccessModifierOffset: -4
-AlignAfterOpenBracket: AlwaysBreak
-AlignArrayOfStructures: None
-AlignConsecutiveMacros: None
-AlignConsecutiveAssignments: None
-AlignConsecutiveBitFields: None
-AlignConsecutiveDeclarations: None
-AlignEscapedNewlines: Left
-AlignOperands:   Align
-AlignTrailingComments: false
-AllowAllArgumentsOnNextLine: true
-AllowAllParametersOfDeclarationOnNextLine: false
-AllowShortEnumsOnASingleLine: true
-AllowShortBlocksOnASingleLine: Never
-AllowShortCaseLabelsOnASingleLine: false
-AllowShortFunctionsOnASingleLine: None
-AllowShortLambdasOnASingleLine: All
-AllowShortIfStatementsOnASingleLine: WithoutElse
-AllowShortLoopsOnASingleLine: true
-AlwaysBreakAfterDefinitionReturnType: None
-AlwaysBreakAfterReturnType: None
-AlwaysBreakBeforeMultilineStrings: false
-AlwaysBreakTemplateDeclarations: Yes
-AttributeMacros:
-  - __capability
-BinPackArguments: false
-BinPackParameters: false
-BraceWrapping:
-  AfterCaseLabel:  false
-  AfterClass:      false
-  AfterControlStatement: Never
-  AfterEnum:       false
-  AfterFunction:   false
-  AfterNamespace:  false
-  AfterObjCDeclaration: false
-  AfterStruct:     false
-  AfterUnion:      false
-  AfterExternBlock: false
-  BeforeCatch:     false
-  BeforeElse:      false
-  BeforeLambdaBody: false
-  BeforeWhile:     false
-  IndentBraces:    false
-  SplitEmptyFunction: true
-  SplitEmptyRecord: true
-  SplitEmptyNamespace: true
-BreakBeforeBinaryOperators: None
-BreakBeforeConceptDeclarations: true
-BreakBeforeBraces: Attach
-BreakBeforeInheritanceComma: false
-BreakInheritanceList: BeforeColon
-BreakBeforeTernaryOperators: false
-BreakConstructorInitializersBeforeComma: false
-BreakConstructorInitializers: BeforeComma
-BreakAfterJavaFieldAnnotations: false
-BreakStringLiterals: false
-ColumnLimit:     99
-CommentPragmas:  '^ IWYU pragma:'
-QualifierAlignment: Leave
-CompactNamespaces: false
-ConstructorInitializerIndentWidth: 4
-ContinuationIndentWidth: 4
-Cpp11BracedListStyle: true
-DeriveLineEnding: true
-DerivePointerAlignment: false
-DisableFormat:   false
-EmptyLineAfterAccessModifier: Never
-EmptyLineBeforeAccessModifier: LogicalBlock
-ExperimentalAutoDetectBinPacking: false
-PackConstructorInitializers: BinPack
-BasedOnStyle:    ''
-ConstructorInitializerAllOnOneLineOrOnePerLine: false
-AllowAllConstructorInitializersOnNextLine: true
-FixNamespaceComments: false
-ForEachMacros:
-  - foreach
-  - Q_FOREACH
-  - BOOST_FOREACH
-IfMacros:
-  - KJ_IF_MAYBE
-IncludeBlocks:   Preserve
-IncludeCategories:
-  - Regex:           '.*'
-    Priority:        1
-    SortPriority:    0
-    CaseSensitive:   false
-  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
-    Priority:        3
-    SortPriority:    0
-    CaseSensitive:   false
-  - Regex:           '.*'
-    Priority:        1
-    SortPriority:    0
-    CaseSensitive:   false
-IncludeIsMainRegex: '(Test)?$'
-IncludeIsMainSourceRegex: ''
-IndentAccessModifiers: false
-IndentCaseLabels: false
-IndentCaseBlocks: false
-IndentGotoLabels: true
-IndentPPDirectives: None
-IndentExternBlock: AfterExternBlock
-IndentRequires:  false
-IndentWidth:     4
-IndentWrappedFunctionNames: true
-InsertTrailingCommas: None
-JavaScriptQuotes: Leave
-JavaScriptWrapImports: true
-KeepEmptyLinesAtTheStartOfBlocks: false
-LambdaBodyIndentation: Signature
-MacroBlockBegin: ''
-MacroBlockEnd:   ''
-MaxEmptyLinesToKeep: 1
-NamespaceIndentation: None
-ObjCBinPackProtocolList: Auto
-ObjCBlockIndentWidth: 4
-ObjCBreakBeforeNestedBlockParam: true
-ObjCSpaceAfterProperty: true
-ObjCSpaceBeforeProtocolList: true
-PenaltyBreakAssignment: 10
-PenaltyBreakBeforeFirstCallParameter: 30
-PenaltyBreakComment: 10
-PenaltyBreakFirstLessLess: 0
-PenaltyBreakOpenParenthesis: 0
-PenaltyBreakString: 10
-PenaltyBreakTemplateDeclaration: 10
-PenaltyExcessCharacter: 100
-PenaltyReturnTypeOnItsOwnLine: 60
-PenaltyIndentedWhitespace: 0
-PointerAlignment: Left
-PPIndentWidth:   -1
-ReferenceAlignment: Pointer
-ReflowComments:  false
-RemoveBracesLLVM: false
-SeparateDefinitionBlocks: Leave
-ShortNamespaceLines: 1
-SortIncludes:    Never
-SortJavaStaticImport: Before
-SortUsingDeclarations: false
-SpaceAfterCStyleCast: false
-SpaceAfterLogicalNot: false
-SpaceAfterTemplateKeyword: true
-SpaceBeforeAssignmentOperators: true
-SpaceBeforeCaseColon: false
-SpaceBeforeCpp11BracedList: false
-SpaceBeforeCtorInitializerColon: true
-SpaceBeforeInheritanceColon: true
-SpaceBeforeParens: Never
-SpaceBeforeParensOptions:
-  AfterControlStatements: false
-  AfterForeachMacros: false
-  AfterFunctionDefinitionName: false
-  AfterFunctionDeclarationName: false
-  AfterIfMacros:   false
-  AfterOverloadedOperator: false
-  BeforeNonEmptyParentheses: false
-SpaceAroundPointerQualifiers: Default
-SpaceBeforeRangeBasedForLoopColon: true
-SpaceInEmptyBlock: false
-SpaceInEmptyParentheses: false
-SpacesBeforeTrailingComments: 1
-SpacesInAngles:  Never
-SpacesInConditionalStatement: false
-SpacesInContainerLiterals: false
-SpacesInCStyleCastParentheses: false
-SpacesInLineCommentPrefix:
-  Minimum:         1
-  Maximum:         -1
-SpacesInParentheses: false
-SpacesInSquareBrackets: false
-SpaceBeforeSquareBrackets: false
-BitFieldColonSpacing: Both
-Standard:        c++03
-StatementAttributeLikeMacros:
-  - Q_EMIT
-StatementMacros:
-  - Q_UNUSED
-  - QT_REQUIRE_VERSION
-TabWidth:        4
-UseCRLF:         false
-UseTab:          Never
-WhitespaceSensitiveMacros:
-  - STRINGIZE
-  - PP_STRINGIZE
-  - BOOST_PP_STRINGIZE
-  - NS_SWIFT_NAME
-  - CF_SWIFT_NAME
-...

+ 0 - 0
.github/images/preview.gif → .github/images/v1-1.gif


BIN=BIN
.github/images/v1-2.gif


+ 11 - 1
.gitignore

@@ -1 +1,11 @@
-.vscode
+*.zip
+.DS_Store
+.clang-format
+.editorconfig
+.idea
+.vscode
+/venv
+__pycache__
+dist/*
+src-fap/.gitignore
+.submodules/*

+ 7 - 0
.gitmodules

@@ -0,0 +1,7 @@
+[submodule ".submodules/flipper-application-catalog"]
+	path = .submodules/flipper-application-catalog
+	url = https://github.com/flipperdevices/flipper-application-catalog
+[submodule ".submodules/flipperzero-firmware"]
+	path = .submodules/flipperzero-firmware
+	url = https://github.com/flipperdevices/flipperzero-firmware
+	branch = dev

+ 17 - 4
README.md

@@ -24,14 +24,25 @@
 - [Software Guide](#software-guide)
 - [Attributions](#attributions)
 - [Contributions](#contributions)
+- [Changelog](CHANGELOG.md)
 
 ## Previews <a name="previews"></a>
 
-<img align="center" src=".github/images/preview_01.png" />
+Greetings!
 
-<img align="center" src=".github/images/preview_02.png" />
+- <img align="center" src=".github/images/preview_01.png" />
 
-<img align="center" src=".github/images/preview.gif" />
+Preview with a camera module attached to the Flipper Zero.
+
+- <img align="center" src=".github/images/preview_02.png" />
+
+Version 1.1.0 and above now supports new dithering options and bug fixes!
+
+- <img align="center" src=".github/images/v1-1.gif" />
+
+Version 1.2.0 and above now supports taking pictures, configurable dithering, and LED flash!
+
+- <img align="center" src=".github/images/v1-2.gif" />
 
 <p align="right">[ <a href="#index">Back to top</a> ]</p>
 
@@ -122,7 +133,7 @@ Note the upload may fail a few times, this is normal, try again. If it still fai
 
 ▶️ = Toggle dithering on/off.
 
-⚪ = Cycle Floyd–Steinberg/Jarvis-Judice-Ninke/Stucki dithering types.
+⚪ = 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.
 
@@ -130,6 +141,8 @@ Note the upload may fail a few times, this is normal, try again. If it still fai
 
 **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.
 
+**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.

+ 1 - 1
src-fap/application.fam

@@ -8,7 +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.1",
+    fap_version="1.2",
     fap_weburl="https://github.com/CodyTolene/Flipper-Zero-Cam",
     name="[ESP32] Camera Suite",
     order=1,

+ 5 - 3
src-fap/camera_suite.c

@@ -44,9 +44,11 @@ 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->speaker = 1; // Speaker is enabled by default
+    app->led = 1; // LED is enabled by default
 
     // Load configs
     camera_suite_read_settings(app);

+ 13 - 0
src-fap/camera_suite.h

@@ -30,6 +30,8 @@ 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 speaker;
     uint32_t led;
@@ -51,6 +53,17 @@ typedef enum {
     CameraSuiteOrientation270,
 } CameraSuiteOrientationState;
 
+typedef enum {
+    CameraSuiteDitherFloydSteinberg,
+    CameraSuiteDitherStucki,
+    CameraSuiteDitherJarvisJudiceNinke,
+} CameraSuiteDitherState;
+
+typedef enum {
+    CameraSuiteFlashOff,
+    CameraSuiteFlashOn,
+} CameraSuiteFlashState;
+
 typedef enum {
     CameraSuiteHapticOff,
     CameraSuiteHapticOn,

+ 12 - 3
src-fap/docs/CHANGELOG.md

@@ -1,11 +1,20 @@
 ## Roadmap
 
-- Save image support.
 - Full screen 90 degree and 270 degree fill.
-- Camera flash support.
 - In-camera GUI.
 
-## v1.1 (current)
+## v1.2 (current)
+
+- 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.

+ 3 - 1
src-fap/docs/README.md

@@ -18,7 +18,7 @@ Button mappings:
 
 **Right** = Toggle dithering on/off.
 
-**Center** = 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.
 
 **Back** = Go back.
 
@@ -26,6 +26,8 @@ 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.
 
+**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.

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

@@ -51,6 +51,8 @@ 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_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 +102,11 @@ 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_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);

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

@@ -10,6 +10,8 @@
 #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_HAPTIC "Haptic"
 #define BOILERPLATE_SETTINGS_KEY_LED "Led"
 #define BOILERPLATE_SETTINGS_KEY_SPEAKER "Speaker"

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

@@ -16,6 +16,29 @@ 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 haptic_text[2] = {
     "OFF",
     "ON",
@@ -54,6 +77,22 @@ 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_haptic(VariableItem* item) {
     CameraSuite* app = variable_item_get_context(item);
     uint8_t index = variable_item_get_current_value_index(item);
@@ -97,6 +136,24 @@ 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]);
+
     // Haptic FX ON/OFF
     item = variable_item_list_add(
         app->variable_item_list, "Haptic FX:", 2, camera_suite_scene_settings_set_haptic, app);

+ 98 - 6
src-fap/views/camera_suite_view_camera.c

@@ -82,6 +82,69 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* _model) {
     }
 }
 
+static void save_image(void* _model) {
+    UartDumpModel* model = _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 the file was opened successfully, write the bitmap header and the
+    // image data.
+    if(result) {
+        storage_file_write(file, bitmap_header, BITMAP_HEADER_LENGTH);
+        int8_t row_buffer[ROW_BUFFER_LENGTH];
+        for(size_t i = 64; i > 0; --i) {
+            for(size_t j = 0; j < ROW_BUFFER_LENGTH; ++j) {
+                row_buffer[j] = model->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j];
+            }
+            storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH);
+        }
+    }
+
+    // Close the file.
+    storage_file_close(file);
+
+    // Freeing up memory.
+    storage_file_free(file);
+}
+
 static void camera_suite_view_camera_model_init(UartDumpModel* const model) {
     for(size_t i = 0; i < FRAME_BUFFER_LENGTH; i++) {
         model->pixels[i] = 0;
@@ -106,7 +169,6 @@ 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];
         switch(event->key) {
@@ -183,21 +245,30 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                 },
                 true);
             break;
-        case InputKeyOk:
-            // Switch dithering types.
-            data[0] = 'D';
+        case InputKeyOk: {
+            CameraSuite* app = current_instance->context;
+            // If flash is enabled, flash the onboard ESP32-CAM LED.
+            if(app->flash) {
+                data[0] = 'P';
+                // Initialize the ESP32-CAM onboard torch immediately.
+                furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+                // Delay for 500ms to make sure flash is on before taking picture.
+                furi_delay_ms(500);
+            }
+            // Take picture.
             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);
+                    save_image(model);
                     instance->callback(CameraSuiteCustomEventSceneCameraOk, instance->context);
                 },
                 true);
-            break;
+            return true;
+        }
         case InputKeyMAX:
             break;
         }
@@ -223,6 +294,27 @@ static void camera_suite_view_camera_enter(void* context) {
 
     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);
+
+    // Delay for 50ms to make sure the camera is started before sending any other commands.
+    furi_delay_ms(50);
+
+    // Initialize the camera with the selected dithering option from options.
+    CameraSuite* instanceContext = instance->context;
+    switch(instanceContext->dither) {
+    case 0: // Floyd Steinberg
+        data[0] = '0';
+        break;
+    case 1: // Stucki
+        data[0] = '1';
+        break;
+    case 2: // Jarvis Judice Ninke
+        data[0] = '2';
+        break;
+    }
+
     // Send `data` to the ESP32-CAM
     furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
 

+ 8 - 1
src-fap/views/camera_suite_view_camera.h

@@ -23,6 +23,13 @@
 #define ROW_BUFFER_LENGTH 16
 #define RING_BUFFER_LENGTH 19
 #define LAST_ROW_INDEX 1008
+#define BITMAP_HEADER_LENGTH 62
+
+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;
 
@@ -58,4 +65,4 @@ typedef enum {
     WorkerEventRx = (1 << 2),
 } WorkerEventFlags;
 
-#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx)
+#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx)

+ 44 - 15
src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino

@@ -1,23 +1,24 @@
 #include "esp_camera.h"
 
 // Pin definitions
+#define FLASH_GPIO_NUM    4
+#define HREF_GPIO_NUM     23
+#define PCLK_GPIO_NUM     22
 #define PWDN_GPIO_NUM     32
 #define RESET_GPIO_NUM    -1
-#define XCLK_GPIO_NUM      0
-#define SIOD_GPIO_NUM     26
 #define SIOC_GPIO_NUM     27
+#define SIOD_GPIO_NUM     26
+#define XCLK_GPIO_NUM      0
+#define VSYNC_GPIO_NUM    25
 
-#define Y9_GPIO_NUM       35
-#define Y8_GPIO_NUM       34
-#define Y7_GPIO_NUM       39
-#define Y6_GPIO_NUM       36
-#define Y5_GPIO_NUM       21
-#define Y4_GPIO_NUM       19
-#define Y3_GPIO_NUM       18
 #define Y2_GPIO_NUM        5
-#define VSYNC_GPIO_NUM    25
-#define HREF_GPIO_NUM     23
-#define PCLK_GPIO_NUM     22
+#define Y3_GPIO_NUM       18
+#define Y4_GPIO_NUM       19
+#define Y5_GPIO_NUM       21
+#define Y6_GPIO_NUM       36
+#define Y7_GPIO_NUM       39
+#define Y8_GPIO_NUM       34
+#define Y9_GPIO_NUM       35
 
 // Camera configuration
 camera_config_t config;
@@ -42,6 +43,7 @@ DitheringAlgorithm ditherAlgorithm = FLOYD_STEINBERG;
 // Serial input flags
 bool disableDithering = false;
 bool invert = false;
+bool isFlashOn = false;
 bool rotated = false;
 bool stopStream = false;
 
@@ -88,7 +90,18 @@ void handleSerialInput() {
       case 'c': // Remove contrast
         cameraSensor->set_contrast(cameraSensor, cameraSensor->status.contrast - 1);
         break;
-      case 'P': // TODO: Take a picture
+      case 'P': // Picture sequence.
+        if (!isFlashOn) {
+          isFlashOn = true;
+          pinMode(FLASH_GPIO_NUM, OUTPUT);
+          // Turn on torch.
+          digitalWrite(FLASH_GPIO_NUM, HIGH); 
+          delay(2000);
+          // Turn off torch.
+          digitalWrite(FLASH_GPIO_NUM, LOW); 
+          delay(50);
+          isFlashOn = false;
+        }
         break;
       case 'M': // Toggle Mirror
         cameraSensor->set_hmirror(cameraSensor, !cameraSensor->status.hmirror);
@@ -99,8 +112,17 @@ void handleSerialInput() {
       case 's': // Stop stream
         stopStream = true;
         break;
-      case 'D': // Change dithering algorithm.
-        ditherAlgorithm = static_cast<DitheringAlgorithm>((ditherAlgorithm + 1) % 3);
+      case '0': // Use Floyd Steinberg dithering.
+        ditherAlgorithm = FLOYD_STEINBERG;
+        break;
+      case '1': // Use Jarvis Judice dithering.
+        ditherAlgorithm = JARVIS_JUDICE_NINKE;
+        break;
+      case '2': // Use Stucki dithering.
+        ditherAlgorithm = STUCKI;
+        break;
+      default:
+        // Do nothing.
         break;
     }
   }
@@ -131,6 +153,13 @@ void initializeCamera() {
   config.frame_size = FRAMESIZE_QQVGA;
   config.fb_count = 1;
 
+  if (isFlashOn) {
+    pinMode(FLASH_GPIO_NUM, OUTPUT);
+    // Turn off torch.
+    digitalWrite(FLASH_GPIO_NUM, LOW); 
+    isFlashOn = false;
+  }
+
   // Initialize camera
   esp_err_t err = esp_camera_init(&config);
   if (err != ESP_OK) {