From a1f16faabd2915c342afe3bab382b308fb833b75 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 6 Aug 2023 12:07:59 -0500 Subject: [PATCH 1/8] Add gitmodules. --- .clang-format | 190 -------------------------------------------------- .gitignore | 11 ++- .gitmodules | 7 ++ 3 files changed, 17 insertions(+), 191 deletions(-) delete mode 100644 .clang-format create mode 100644 .gitmodules diff --git a/.clang-format b/.clang-format deleted file mode 100644 index c1f0c90..0000000 --- a/.clang-format +++ /dev/null @@ -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 -... diff --git a/.gitignore b/.gitignore index 600d2d3..7fd1501 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,10 @@ -.vscode \ No newline at end of file +*.zip +.DS_Store +.clang-format +.editorconfig +.idea +.vscode +/venv +__pycache__ +dist/* +src-fap/.gitignore diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0066241 --- /dev/null +++ b/.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 From 01a8f573e564d8db32af7a68c9a46de191690242 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 6 Aug 2023 15:55:47 -0500 Subject: [PATCH 2/8] Update gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7fd1501..1e88ea4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__ dist/* src-fap/.gitignore +.submodules/* From 8ab7d1e1532ce8b844118b7de70ff2e7817bb8b6 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 6 Aug 2023 15:56:07 -0500 Subject: [PATCH 3/8] Add support for camera flash and saving pictures. --- src-fap/views/camera_suite_view_camera.c | 80 ++++++++++++++++++- src-fap/views/camera_suite_view_camera.h | 12 ++- .../esp32_cam_uart_stream.ino | 49 +++++++++--- 3 files changed, 123 insertions(+), 18 deletions(-) diff --git a/src-fap/views/camera_suite_view_camera.c b/src-fap/views/camera_suite_view_camera.c index 5d71004..c8cd3fe 100644 --- a/src-fap/views/camera_suite_view_camera.c +++ b/src-fap/views/camera_suite_view_camera.c @@ -82,6 +82,73 @@ 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 +173,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) { @@ -185,19 +251,25 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { break; case InputKeyOk: // Switch dithering types. - data[0] = 'D'; + // data[0] = 'D'; + 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; } diff --git a/src-fap/views/camera_suite_view_camera.h b/src-fap/views/camera_suite_view_camera.h index 5ccbac7..20506c2 100644 --- a/src-fap/views/camera_suite_view_camera.h +++ b/src-fap/views/camera_suite_view_camera.h @@ -23,6 +23,16 @@ #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 +68,4 @@ typedef enum { WorkerEventRx = (1 << 2), } WorkerEventFlags; -#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) +#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) \ No newline at end of file diff --git a/src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino b/src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino index 722f68a..d01a7c6 100644 --- a/src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino +++ b/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); @@ -102,6 +115,9 @@ void handleSerialInput() { case 'D': // Change dithering algorithm. ditherAlgorithm = static_cast((ditherAlgorithm + 1) % 3); break; + default: + // Do nothing. + break; } } } @@ -131,6 +147,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) { From 500bad195906c7f6e8fb0b1deff0b39726251f07 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 6 Aug 2023 16:37:30 -0500 Subject: [PATCH 4/8] Remove dithering type from center button and add to options. --- src-fap/camera_suite.c | 1 + src-fap/camera_suite.h | 7 ++++ src-fap/helpers/camera_suite_storage.c | 12 +++---- src-fap/helpers/camera_suite_storage.h | 1 + src-fap/scenes/camera_suite_scene_settings.c | 32 +++++++++++++++++++ src-fap/views/camera_suite_view_camera.c | 24 ++++++++++++-- .../esp32_cam_uart_stream.ino | 10 ++++-- 7 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src-fap/camera_suite.c b/src-fap/camera_suite.c index cbe7e3d..a3c6c15 100644 --- a/src-fap/camera_suite.c +++ b/src-fap/camera_suite.c @@ -44,6 +44,7 @@ CameraSuite* camera_suite_app_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->haptic = 1; // Haptic is on by default app->speaker = 1; // Speaker is on by default app->led = 1; // LED is on by default diff --git a/src-fap/camera_suite.h b/src-fap/camera_suite.h index a8b9825..63b18cc 100644 --- a/src-fap/camera_suite.h +++ b/src-fap/camera_suite.h @@ -30,6 +30,7 @@ typedef struct { CameraSuiteViewCamera* camera_suite_view_camera; CameraSuiteViewGuide* camera_suite_view_guide; uint32_t orientation; + uint32_t dither; uint32_t haptic; uint32_t speaker; uint32_t led; @@ -51,6 +52,12 @@ typedef enum { CameraSuiteOrientation270, } CameraSuiteOrientationState; +typedef enum { + CameraSuiteDitherFloydSteinberg, + CameraSuiteDitherStucki, + CameraSuiteDitherJarvisJudiceNinke, +} CameraSuiteDitherState; + typedef enum { CameraSuiteHapticOff, CameraSuiteHapticOn, diff --git a/src-fap/helpers/camera_suite_storage.c b/src-fap/helpers/camera_suite_storage.c index 38a5f08..16ba3e2 100644 --- a/src-fap/helpers/camera_suite_storage.c +++ b/src-fap/helpers/camera_suite_storage.c @@ -47,10 +47,9 @@ void camera_suite_save_settings(void* context) { } // 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_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_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 +99,9 @@ void camera_suite_read_settings(void* context) { return; } - flipper_format_read_uint32( - fff_file, BOILERPLATE_SETTINGS_KEY_ORIENTATION, &app->orientation, 1); + // 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_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); diff --git a/src-fap/helpers/camera_suite_storage.h b/src-fap/helpers/camera_suite_storage.h index 37e82d7..fd2cb87 100644 --- a/src-fap/helpers/camera_suite_storage.h +++ b/src-fap/helpers/camera_suite_storage.h @@ -10,6 +10,7 @@ #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_HAPTIC "Haptic" #define BOILERPLATE_SETTINGS_KEY_LED "Led" #define BOILERPLATE_SETTINGS_KEY_SPEAKER "Speaker" diff --git a/src-fap/scenes/camera_suite_scene_settings.c b/src-fap/scenes/camera_suite_scene_settings.c index a06b45f..d38ecee 100644 --- a/src-fap/scenes/camera_suite_scene_settings.c +++ b/src-fap/scenes/camera_suite_scene_settings.c @@ -16,6 +16,19 @@ 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 haptic_text[2] = { "OFF", "ON", @@ -54,6 +67,14 @@ 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_haptic(VariableItem* item) { CameraSuite* app = variable_item_get_context(item); uint8_t index = variable_item_get_current_value_index(item); @@ -97,6 +118,17 @@ 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]); + // Haptic FX ON/OFF item = variable_item_list_add( app->variable_item_list, "Haptic FX:", 2, camera_suite_scene_settings_set_haptic, app); diff --git a/src-fap/views/camera_suite_view_camera.c b/src-fap/views/camera_suite_view_camera.c index c8cd3fe..cd83a10 100644 --- a/src-fap/views/camera_suite_view_camera.c +++ b/src-fap/views/camera_suite_view_camera.c @@ -250,8 +250,7 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { true); break; case InputKeyOk: - // Switch dithering types. - // data[0] = 'D'; + // Camera: Initialize take picture mode. data[0] = 'P'; // Initialize the ESP32-CAM onboard torch immediately. furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1); @@ -295,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); diff --git a/src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino b/src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino index d01a7c6..edcba90 100644 --- a/src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino +++ b/src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino @@ -112,8 +112,14 @@ void handleSerialInput() { case 's': // Stop stream stopStream = true; break; - case 'D': // Change dithering algorithm. - ditherAlgorithm = static_cast((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. From aa46a5da936d0824da198adac9e2819b62ca02f3 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 6 Aug 2023 17:05:07 -0500 Subject: [PATCH 5/8] Make flash a configurable option. Update readme. --- README.md | 4 +++- src-fap/application.fam | 2 +- src-fap/camera_suite.c | 7 +++--- src-fap/camera_suite.h | 6 +++++ src-fap/docs/CHANGELOG.md | 10 +++++--- src-fap/docs/README.md | 4 +++- src-fap/helpers/camera_suite_storage.c | 2 ++ src-fap/helpers/camera_suite_storage.h | 1 + src-fap/scenes/camera_suite_scene_settings.c | 25 ++++++++++++++++++++ src-fap/views/camera_suite_view_camera.c | 18 ++++++++------ 10 files changed, 63 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9ebe294..654d19a 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,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 +130,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. diff --git a/src-fap/application.fam b/src-fap/application.fam index 0c33ffa..83a356f 100644 --- a/src-fap/application.fam +++ b/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, diff --git a/src-fap/camera_suite.c b/src-fap/camera_suite.c index a3c6c15..07888ab 100644 --- a/src-fap/camera_suite.c +++ b/src-fap/camera_suite.c @@ -45,9 +45,10 @@ CameraSuite* camera_suite_app_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->haptic = 1; // Haptic is on by default - app->speaker = 1; // Speaker is on by default - app->led = 1; // LED is on 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); diff --git a/src-fap/camera_suite.h b/src-fap/camera_suite.h index 63b18cc..9460b80 100644 --- a/src-fap/camera_suite.h +++ b/src-fap/camera_suite.h @@ -31,6 +31,7 @@ typedef struct { CameraSuiteViewGuide* camera_suite_view_guide; uint32_t orientation; uint32_t dither; + uint32_t flash; uint32_t haptic; uint32_t speaker; uint32_t led; @@ -58,6 +59,11 @@ typedef enum { CameraSuiteDitherJarvisJudiceNinke, } CameraSuiteDitherState; +typedef enum { + CameraSuiteFlashOff, + CameraSuiteFlashOn, +} CameraSuiteFlashState; + typedef enum { CameraSuiteHapticOff, CameraSuiteHapticOn, diff --git a/src-fap/docs/CHANGELOG.md b/src-fap/docs/CHANGELOG.md index 06fa5ac..cb459da 100644 --- a/src-fap/docs/CHANGELOG.md +++ b/src-fap/docs/CHANGELOG.md @@ -1,11 +1,15 @@ ## 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. + +## 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. diff --git a/src-fap/docs/README.md b/src-fap/docs/README.md index 1bf385b..d1d135d 100644 --- a/src-fap/docs/README.md +++ b/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. diff --git a/src-fap/helpers/camera_suite_storage.c b/src-fap/helpers/camera_suite_storage.c index 16ba3e2..61e9a03 100644 --- a/src-fap/helpers/camera_suite_storage.c +++ b/src-fap/helpers/camera_suite_storage.c @@ -50,6 +50,7 @@ void camera_suite_save_settings(void* context) { 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_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); @@ -102,6 +103,7 @@ void camera_suite_read_settings(void* context) { // 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); diff --git a/src-fap/helpers/camera_suite_storage.h b/src-fap/helpers/camera_suite_storage.h index fd2cb87..394eb38 100644 --- a/src-fap/helpers/camera_suite_storage.h +++ b/src-fap/helpers/camera_suite_storage.h @@ -11,6 +11,7 @@ #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" diff --git a/src-fap/scenes/camera_suite_scene_settings.c b/src-fap/scenes/camera_suite_scene_settings.c index d38ecee..9a26b3b 100644 --- a/src-fap/scenes/camera_suite_scene_settings.c +++ b/src-fap/scenes/camera_suite_scene_settings.c @@ -29,6 +29,16 @@ const uint32_t dither_value[4] = { 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", @@ -75,6 +85,14 @@ static void camera_suite_scene_settings_set_camera_dither(VariableItem* item) { 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); @@ -129,6 +147,13 @@ 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, 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); diff --git a/src-fap/views/camera_suite_view_camera.c b/src-fap/views/camera_suite_view_camera.c index cd83a10..a86cfba 100644 --- a/src-fap/views/camera_suite_view_camera.c +++ b/src-fap/views/camera_suite_view_camera.c @@ -249,13 +249,16 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { }, true); break; - case InputKeyOk: - // Camera: Initialize take picture mode. - 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); + 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, @@ -269,6 +272,7 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { }, true); return true; + } case InputKeyMAX: break; } From 02be440d190096059f1e2bb18242b45b69e4b0d7 Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 6 Aug 2023 17:17:06 -0500 Subject: [PATCH 6/8] Formatted and linted per clang format. --- src-fap/helpers/camera_suite_storage.c | 9 ++++-- src-fap/views/camera_suite_view_camera.c | 40 +++++++++++------------- src-fap/views/camera_suite_view_camera.h | 11 +++---- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src-fap/helpers/camera_suite_storage.c b/src-fap/helpers/camera_suite_storage.c index 61e9a03..50d9426 100644 --- a/src-fap/helpers/camera_suite_storage.c +++ b/src-fap/helpers/camera_suite_storage.c @@ -47,8 +47,10 @@ void camera_suite_save_settings(void* context) { } // 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_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_HAPTIC, &app->haptic, 1); @@ -101,7 +103,8 @@ void camera_suite_read_settings(void* context) { } // Read settings - flipper_format_read_uint32(fff_file, BOILERPLATE_SETTINGS_KEY_ORIENTATION, &app->orientation, 1); + 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); diff --git a/src-fap/views/camera_suite_view_camera.c b/src-fap/views/camera_suite_view_camera.c index a86cfba..6560d94 100644 --- a/src-fap/views/camera_suite_view_camera.c +++ b/src-fap/views/camera_suite_view_camera.c @@ -115,28 +115,24 @@ static void save_image(void* _model) { datetime.day, datetime.hour, datetime.minute, - datetime.second - ); + datetime.second); - // Open the file for writing. If the file does not exist (it shouldn't), + // 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 - ); + 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){ + 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]; + 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); } @@ -272,7 +268,7 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) { }, true); return true; - } + } case InputKeyMAX: break; } @@ -308,15 +304,15 @@ static void camera_suite_view_camera_enter(void* context) { // 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; + 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 diff --git a/src-fap/views/camera_suite_view_camera.h b/src-fap/views/camera_suite_view_camera.h index 20506c2..40afac7 100644 --- a/src-fap/views/camera_suite_view_camera.h +++ b/src-fap/views/camera_suite_view_camera.h @@ -26,13 +26,10 @@ #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 -}; + 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; From f819a8be1bd6496d01eef569fe2b3ee426c291cf Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 6 Aug 2023 17:19:25 -0500 Subject: [PATCH 7/8] Update changelog. --- src-fap/docs/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src-fap/docs/CHANGELOG.md b/src-fap/docs/CHANGELOG.md index cb459da..b9f5f31 100644 --- a/src-fap/docs/CHANGELOG.md +++ b/src-fap/docs/CHANGELOG.md @@ -8,6 +8,11 @@ - 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 From ba56342e85ff81bcda02416bb1c4300d2a2bfcba Mon Sep 17 00:00:00 2001 From: Cody Tolene Date: Sun, 6 Aug 2023 17:35:36 -0500 Subject: [PATCH 8/8] Update readme --- .github/images/{preview.gif => v1-1.gif} | Bin .github/images/v1-2.gif | Bin 0 -> 110964 bytes README.md | 17 ++++++++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) rename .github/images/{preview.gif => v1-1.gif} (100%) create mode 100644 .github/images/v1-2.gif diff --git a/.github/images/preview.gif b/.github/images/v1-1.gif similarity index 100% rename from .github/images/preview.gif rename to .github/images/v1-1.gif diff --git a/.github/images/v1-2.gif b/.github/images/v1-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..54e64be8ba6fedb043100edfaad87decf1ee1182 GIT binary patch literal 110964 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 - +Greetings! - +- - +Preview with a camera module attached to the Flipper Zero. + +- + +Version 1.1.0 and above now supports new dithering options and bug fixes! + +- + +Version 1.2.0 and above now supports taking pictures, configurable dithering, and LED flash! + +-

[ Back to top ]