فهرست منبع

Merge pull request #25 from CodyTolene/ct/improvements

New auto flasher. Improve camera I/O, code, comments, and draw. Resolve bugs.
Cody Tolene 2 سال پیش
والد
کامیت
3741c73193
65فایلهای تغییر یافته به همراه1144 افزوده شده و 468 حذف شده
  1. BIN
      .github/images/esp32-cam-pinout-guide.png
  2. BIN
      .github/images/firmware-build-success.png
  3. 0 0
      .github/images/preview.png
  4. BIN
      .github/images/preview_02.png
  5. BIN
      .github/images/v1-1.gif
  6. BIN
      .github/images/v1-2.gif
  7. 1 1
      .github/workflows/deploy-main.yml
  8. 2 2
      .github/workflows/lint-pull-request.yml
  9. 7 5
      .gitignore
  10. 50 19
      README.md
  11. 27 0
      arduino-cli.yaml
  12. 1 1
      fap/application.fam
  13. 1 0
      fap/camera_suite.c
  14. 6 0
      fap/camera_suite.h
  15. 14 3
      fap/docs/CHANGELOG.md
  16. 0 0
      fap/docs/README.md
  17. 0 0
      fap/helpers/camera_suite_custom_event.h
  18. 0 0
      fap/helpers/camera_suite_haptic.c
  19. 0 0
      fap/helpers/camera_suite_haptic.h
  20. 0 0
      fap/helpers/camera_suite_led.c
  21. 0 0
      fap/helpers/camera_suite_led.h
  22. 0 0
      fap/helpers/camera_suite_speaker.c
  23. 0 0
      fap/helpers/camera_suite_speaker.h
  24. 2 0
      fap/helpers/camera_suite_storage.c
  25. 1 0
      fap/helpers/camera_suite_storage.h
  26. 0 0
      fap/icons/camera_suite.png
  27. 1 1
      fap/manifest.yml
  28. 0 0
      fap/scenes/camera_suite_scene.c
  29. 0 0
      fap/scenes/camera_suite_scene.h
  30. 0 0
      fap/scenes/camera_suite_scene_camera.c
  31. 0 0
      fap/scenes/camera_suite_scene_config.h
  32. 0 0
      fap/scenes/camera_suite_scene_guide.c
  33. 0 0
      fap/scenes/camera_suite_scene_menu.c
  34. 32 0
      fap/scenes/camera_suite_scene_settings.c
  35. 0 0
      fap/scenes/camera_suite_scene_start.c
  36. 0 0
      fap/screenshots/camera_preview.png
  37. 0 0
      fap/screenshots/guide.png
  38. 0 0
      fap/screenshots/main_menu.png
  39. 0 0
      fap/screenshots/settings.png
  40. 0 0
      fap/screenshots/start_screen.png
  41. 234 131
      fap/views/camera_suite_view_camera.c
  42. 36 29
      fap/views/camera_suite_view_camera.h
  43. 0 0
      fap/views/camera_suite_view_guide.c
  44. 0 0
      fap/views/camera_suite_view_guide.h
  45. 0 0
      fap/views/camera_suite_view_start.c
  46. 0 0
      fap/views/camera_suite_view_start.h
  47. 188 0
      firmware-flash.bat
  48. 22 0
      firmware/camera.h
  49. 47 0
      firmware/camera.ino
  50. 14 0
      firmware/camera_config.h
  51. 30 0
      firmware/camera_config.ino
  52. 43 0
      firmware/camera_model.h
  53. 13 0
      firmware/camera_model.ino
  54. 11 0
      firmware/dither_image.h
  55. 82 0
      firmware/dither_image.ino
  56. 12 0
      firmware/firmware.h
  57. 30 0
      firmware/firmware.ino
  58. 23 0
      firmware/pins.h
  59. 13 0
      firmware/process_image.h
  60. 54 0
      firmware/process_image.ino
  61. 13 0
      firmware/process_serial_commands.h
  62. 63 0
      firmware/process_serial_commands.ino
  63. 13 0
      firmware/save_picture_to_sd_card.h
  64. 58 0
      firmware/save_picture_to_sd_card.ino
  65. 0 276
      src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino

BIN
.github/images/esp32-cam-pinout-guide.png


BIN
.github/images/firmware-build-success.png


+ 0 - 0
.github/images/preview_01.png → .github/images/preview.png


BIN
.github/images/preview_02.png


BIN
.github/images/v1-1.gif


BIN
.github/images/v1-2.gif


+ 1 - 1
.github/workflows/deploy-main.yml

@@ -27,7 +27,7 @@ jobs:
         uses: flipperdevices/flipperzero-ufbt-action@v0.1.2
         id: build-app
         with:
-          app-dir: ./src-fap
+          app-dir: ./fap
           sdk-channel: ${{ matrix.sdk-channel }}
       - name: Upload app artifacts
         uses: actions/upload-artifact@v3

+ 2 - 2
.github/workflows/lint-pull-request.yml

@@ -3,7 +3,7 @@ on: pull_request
 jobs:
   ufbt-build-action:
     runs-on: ubuntu-latest
-    name: 'ufbt: Build for Dev branch'
+    name: "ufbt: Build for Dev branch"
     steps:
       - name: Checkout
         uses: actions/checkout@v3
@@ -13,7 +13,7 @@ jobs:
         uses: flipperdevices/flipperzero-ufbt-action@v0.1.2
         id: build-app
         with:
-          app-dir: ./src-fap
+          app-dir: ./fap
           sdk-channel: dev
       - name: Lint sources
         uses: flipperdevices/flipperzero-ufbt-action@v0.1.2

+ 7 - 5
.gitignore

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

+ 50 - 19
README.md

@@ -16,7 +16,7 @@
 
 ## Table of Contents <a name="index"></a>
 
-- [Previews](#previews)
+- [Introduction](#intro)
 - [Hardware Requirements](#hardware-requirements)
 - [Hardware Installation](#hardware-installation)
 - [Firmware Installation](#firmware-installation)
@@ -24,25 +24,21 @@
 - [Software Guide](#software-guide)
 - [Attributions](#attributions)
 - [Contributions](#contributions)
-- [Changelog](src-fap/docs/CHANGELOG.md)
+- [Changelog](fap/docs/CHANGELOG.md)
 
-## Previews <a name="previews"></a>
+## Introduction <a name="intro"></a>
 
-Greetings!
+<img align="center" src=".github/images/preview.png" />
 
-- <img align="center" src=".github/images/preview_01.png" />
+### Welcome to the ESP32-CAM Suite for Flipper Zero!
 
-Preview with a camera module attached to the Flipper Zero.
+Discover a new dimension of possibilities by connecting your ESP32-CAM module with your Flipper Zero device. The ESP32-CAM module, a compact ~~powerful~~ cheap camera module, enables you to capture images and stream a live video to your Flipper Zero. With this suite, your Flipper Zero becomes a hub of creativity and utility.
 
-- <img align="center" src=".github/images/preview_02.png" />
+**What You Can Do:**
+- **Capture Moments:** This custom Flipper Zero application empowers you to take pictures effortlessly. View real-time image previews on your Flipper Zero screen while you capture high quality blocky and pixelated memories! Hey it's still a memory and we're at least having fun...
+- **Personalize Your Experience:** Tailor your camera settings with ease. Adjust camera orientation, experiment with various dithering options, and toggle flash, haptic feedback, sound effects, and LED effects to match your preferences. Feel free to use this as a flashlight too, it's pretty bright and good at blinding yourself unexpectedly!
 
-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" />
+There will be many more features added in the future! If you have any ideas or suggestions, please let me know by opening an issue [here][issues-link].
 
 <p align="right">[ <a href="#index">Back to top</a> ]</p>
 
@@ -50,8 +46,7 @@ Version 1.2.0 and above now supports taking pictures, configurable dithering, an
 
 Requires an ESP32-CAM module (I've personally used these: [Amazon Link 1][amazon-esp32-cam-link-1] | [Amazon Link 2][amazon-esp32-cam-link-2]).
 
-<img src=".github/images/esp32-cam-front.png" />
-<img src=".github/images/esp32-cam-back.png" />
+<img src=".github/images/esp32-cam-front.png" /><img src=".github/images/esp32-cam-back.png" />
 
 <p align="right">[ <a href="#index">Back to top</a> ]</p>
 
@@ -74,6 +69,30 @@ On the ESP32-CAM module itself you'll also need to connect the `IO0` pin to `GND
 
 ## Firmware Installation <a name="firmware-installation"></a>
 
+The firmware is the software that runs on the ESP32-CAM module. It is required to use the camera module with your Flipper Zero. There are two ways to install the firmware, the new, easy way, and the old, hard way. The new way is a script that will do everything for you, the old way is a manual process that requires you to install the Arduino IDE and manually flash the firmware to the ESP32-CAM module. I recommend the new way, but if you have issues with it, try the old way.
+
+Below are the instructions for both ways. Choose one and follow the instructions to continue.
+
+<details>
+
+<summary>Firmware Flashing Utility (easy way)</summary>
+
+### Firmware Flashing Utility (Windows 10+ batch script)
+
+1. Download/clone this repository to your computer.
+2. Run the script found at the root of this directory: `firmware-flash.bat`.
+3. Follow the on screen instructions to continue to flash the firmware to your ESP32-CAM module.
+
+That's it, let me know if you have any issues!
+
+</details>
+
+<details>
+
+<summary>Arduino IDE (hard way)</summary>
+
+### Arduino IDE
+
 1. Download and install the Arduino IDE from [here][arduino-ide].
 2. Go to the [releases section][flipper-zero-camera-suite-releases] for this repo and download the `esp32_cam_uart_stream.zip` file.
 3. Extract the contents of `esp32_cam_uart_stream.zip` to disk. Be sure to keep the `.ino` file nested in the folder with the same name.
@@ -96,6 +115,12 @@ On the ESP32-CAM module itself you'll also need to connect the `IO0` pin to `GND
 
 Note the upload may fail a few times, this is normal, try again. If it still fails, try pressing the RST button on the back of the ESP32-CAM module again or checking your connections.
 
+On success, your screen should look like this:
+
+<img align="center" src=".github/images/firmware-build-success.png" />
+
+</details>
+
 <p align="right">[ <a href="#index">Back to top</a> ]</p>
 
 ## Software Installation <a name="software-installation"></a>
@@ -133,7 +158,7 @@ Note the upload may fail a few times, this is normal, try again. If it still fai
 
 ▶️ = Toggle dithering on/off.
 
-⚪ = 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.
+⚪ = 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 camera is opened.
 
 ↩️ = Go back.
 
@@ -143,7 +168,7 @@ Note the upload may fail a few times, this is normal, try again. If it still fai
 
 **Dithering Type** Change between the Cycle Floyd–Steinberg, Jarvis-Judice-Ninke, and Stucki dithering types.
 
-**Flash** Toggle the ESP32-CAM onboard LED on/off when taking a picture.
+**Flash** Toggle the ESP32-CAM onboard LED on/off when using the camera.
 
 **Haptic FX** = Toggle haptic feedback on/off.
 
@@ -159,6 +184,8 @@ This project is based on/forked from the [Flipper Zero Camera Application][flipp
 by [Z4urce][github-profile-z4urce] combined with the [Flipper Zero Boilerplate Application][flipper-zero-fap-boilerplate]
 by [Dave Lee][github-profile-leedave].
 
+The firmware provided here also works with Z4urce's [Flipper Zero Camera Application][flipperzero-camera] so if you have their application installed it too should work fine.
+
 <p align="right">[ <a href="#index">Back to top</a> ]</p>
 
 ## Contributions <a name="contributions"></a>
@@ -167,7 +194,9 @@ by [Dave Lee][github-profile-leedave].
 2. Create a new branch: `<username>/[<issue-#>]-<feature-or-bug-fix-desc>`
 3. Program. Commit changes, push.
 4. Request PR [here][pull-request-link], introduce work via your branch.
-5. Wait for review and merge. Thank you!
+5. Wait for review and merge. 
+
+Thank you!
 
 <p align="right">[ <a href="#index">Back to top</a> ]</p>
 
@@ -180,9 +209,11 @@ Cody
 [amazon-esp32-cam-link-1]: https://amzn.to/44rBFrb
 [amazon-esp32-cam-link-2]: https://amzn.to/45nDR45
 [arduino-ide]: https://www.arduino.cc/en/software
+[clang-format-tool]: https://releases.llvm.org/download.html
 [flipper-zero-camera-suite-releases]: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/releases
 [flipper-zero-fap-boilerplate]: https://github.com/leedave/flipper-zero-fap-boilerplate
 [flipperzero-camera]: https://github.com/Z4urce/flipperzero-camera
 [github-profile-leedave]: https://github.com/leedave
 [github-profile-z4urce]: https://github.com/Z4urce
+[issues-link]: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues
 [pull-request-link]: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/pulls

+ 27 - 0
arduino-cli.yaml

@@ -0,0 +1,27 @@
+board_manager:
+  additional_urls:
+    - https://dl.espressif.com/dl/package_esp32_index.json
+build_cache:
+  compilations_before_purge: 10
+  ttl: 720h0m0s
+daemon:
+  port: "50051"
+directories:
+  data: C:\temp\arduino-cli\data
+  downloads: C:\temp\arduino-cli\downloads
+  user: C:\temp\arduino-cli\user
+library:
+  enable_unsafe_install: false
+logging:
+  file: ""
+  format: text
+  level: info
+metrics:
+  addr: :9090
+  enabled: true
+output:
+  no_color: false
+sketch:
+  always_export_binaries: false
+updater:
+  enable_notification: true

+ 1 - 1
src-fap/application.fam → 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.2",
+    fap_version="1.3",
     fap_weburl="https://github.com/CodyTolene/Flipper-Zero-Cam",
     name="[ESP32] Camera Suite",
     order=1,

+ 1 - 0
src-fap/camera_suite.c → fap/camera_suite.c

@@ -47,6 +47,7 @@ CameraSuite* camera_suite_app_alloc() {
     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
 

+ 6 - 0
src-fap/camera_suite.h → fap/camera_suite.h

@@ -33,6 +33,7 @@ typedef struct {
     uint32_t dither;
     uint32_t flash;
     uint32_t haptic;
+    uint32_t jpeg;
     uint32_t speaker;
     uint32_t led;
     ButtonMenu* button_menu;
@@ -64,6 +65,11 @@ typedef enum {
     CameraSuiteFlashOn,
 } CameraSuiteFlashState;
 
+typedef enum {
+    CameraSuiteJpegOff,
+    CameraSuiteJpegOn,
+} CameraSuiteJpegState;
+
 typedef enum {
     CameraSuiteHapticOff,
     CameraSuiteHapticOn,

+ 14 - 3
src-fap/docs/CHANGELOG.md → fap/docs/CHANGELOG.md

@@ -1,9 +1,20 @@
 ## Roadmap
 
-- Full screen 90 degree and 270 degree fill.
-- In-camera GUI.
+- 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.2 (current)
+## 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.

+ 0 - 0
src-fap/docs/README.md → fap/docs/README.md


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


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


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


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


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


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


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


+ 2 - 0
src-fap/helpers/camera_suite_storage.c → fap/helpers/camera_suite_storage.c

@@ -53,6 +53,7 @@ void camera_suite_save_settings(void* context) {
         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);
@@ -107,6 +108,7 @@ void camera_suite_read_settings(void* context) {
         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);

+ 1 - 0
src-fap/helpers/camera_suite_storage.h → fap/helpers/camera_suite_storage.h

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

+ 0 - 0
src-fap/icons/camera_suite.png → fap/icons/camera_suite.png


+ 1 - 1
src-fap/manifest.yml → fap/manifest.yml

@@ -16,5 +16,5 @@ sourcecode:
   location:
     commit_sha: ...
     origin: https://github.com/CodyTolene/Flipper-Zero-Camera-Suite.git
-    subdir: src-fap
+    subdir: fap
   type: git

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


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


+ 0 - 0
src-fap/scenes/camera_suite_scene_camera.c → fap/scenes/camera_suite_scene_camera.c


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


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


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


+ 32 - 0
src-fap/scenes/camera_suite_scene_settings.c → fap/scenes/camera_suite_scene_settings.c

@@ -39,6 +39,16 @@ const uint32_t flash_value[2] = {
     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",
@@ -93,6 +103,14 @@ static void camera_suite_scene_settings_set_flash(VariableItem* item) {
     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);
@@ -154,6 +172,20 @@ 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, 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);

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


+ 0 - 0
src-fap/screenshots/camera_preview.png → fap/screenshots/camera_preview.png


+ 0 - 0
src-fap/screenshots/guide.png → fap/screenshots/guide.png


+ 0 - 0
src-fap/screenshots/main_menu.png → fap/screenshots/main_menu.png


+ 0 - 0
src-fap/screenshots/settings.png → fap/screenshots/settings.png


+ 0 - 0
src-fap/screenshots/start_screen.png → fap/screenshots/start_screen.png


+ 234 - 131
src-fap/views/camera_suite_view_camera.c → fap/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,8 +71,10 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* _model) {
     }
 }
 
-static void save_image(void* _model) {
-    UartDumpModel* model = _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);
@@ -125,14 +116,28 @@ static void save_image(void* _model) {
     // 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] = model->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j];
+                row_buffer[j] = uartDumpModel->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j];
             }
             storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH);
         }
@@ -141,19 +146,31 @@ static void save_image(void* _model) {
     // Close the file.
     storage_file_close(file);
 
-    // Freeing up memory.
+    // Free up memory.
     storage_file_free(file);
 }
 
-static void camera_suite_view_camera_model_init(UartDumpModel* const model) {
+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.
@@ -170,11 +187,17 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
             break;
         }
     } 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,
@@ -185,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,
@@ -196,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,
@@ -215,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,
@@ -230,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,
@@ -245,88 +278,113 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                 },
                 true);
             break;
+        }
+        // Camera: Take picture.
         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,
                 {
-                    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);
-            return 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;
 
-    uint8_t data[1];
-    data[0] = 'S'; // Uppercase `S` to start the camera
+    // Start camera stream.
+    uint8_t start_camera = 'S';
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &start_camera, 1);
+    furi_delay_ms(75);
 
-    // Send `data` to the ESP32-CAM
-    furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+    // Get/set dither type.
+    uint8_t dither_type = instance_context->dither;
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &dither_type, 1);
+    furi_delay_ms(75);
 
-    // Delay for 50ms to make sure the camera is started before sending any other commands.
-    furi_delay_ms(50);
+    // Make sure the camera is not inverted.
+    uint8_t invert_camera = 'i';
+    furi_hal_uart_tx(FuriHalUartIdUSART1, &invert_camera, 1);
+    furi_delay_ms(75);
 
-    // 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;
-    }
+    // 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);
 
-    // Send `data` to the ESP32-CAM
-    furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1);
+    // 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`.
@@ -340,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) {
@@ -420,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;
@@ -454,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);
@@ -463,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;
 }

+ 36 - 29
src-fap/views/camera_suite_view_camera.h → fap/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,16 +16,15 @@
 #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 BITMAP_HEADER_LENGTH 62
+#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,
@@ -32,37 +33,43 @@ static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = {
     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;
 
-typedef struct UartDumpModel UartDumpModel;
+#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx)
+
+// Forward declaration
+typedef void (*CameraSuiteViewCameraCallback)(CameraSuiteCustomEvent event, void* context);
 
-struct UartDumpModel {
+typedef struct CameraSuiteViewCamera {
+    CameraSuiteViewCameraCallback callback;
+    FuriStreamBuffer* rx_stream;
+    FuriThread* worker_thread;
+    NotificationApp* notification;
+    View* view;
+    void* context;
+} CameraSuiteViewCamera;
+
+typedef struct UartDumpModel {
+    bool flash;
     bool initialized;
+    bool inverted;
     int rotation_angle;
+    uint32_t orientation;
     uint8_t pixels[FRAME_BUFFER_LENGTH];
     uint8_t ringbuffer_index;
+    uint8_t row_identifier;
     uint8_t row_ringbuffer[RING_BUFFER_LENGTH];
-};
-
-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)

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


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


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


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


+ 188 - 0
firmware-flash.bat

@@ -0,0 +1,188 @@
+@echo off
+setlocal EnableDelayedExpansion
+
+rem λ
+
+set CLI_FOUND_FOLLOW_UP=0
+set CLI_TEMP=%TEMP%\arduino-cli
+set COMPILE_FLAG=firmware\.compile.flag
+set CONFIG_FILE=--config-file .\arduino-cli.yaml
+set DEFAULT_BOARD_FQBN=esp32:esp32:esp32cam
+set FIRMWARE_SRC=firmware\firmware.ino
+set SELECTED_BOARD=%DEFAULT_BOARD_FQBN%
+
+chcp 65001 > nul
+echo ┏┓   ┓    ┏┳┓  ┓
+echo ┃ ┏┓┏┫┓┏   ┃ ┏┓┃┏┓┏┓┏┓
+echo ┗┛┗┛┗┻┗┫   ┻ ┗┛┗┗ ┛┗┗
+echo        ┛  https://github.com/CodyTolene
+echo.
+echo Flipper Zero - ESP32-CAM Firmware Flasher - Windows 10+
+echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite
+echo.
+echo ------------------------------------------------------------------------------
+echo Before you begin please make sure your Flipper Zero is plugged into your PC.
+echo Then on your Flipper Zero, open the GPIO menu and select USB-UART Bridge.
+echo ------------------------------------------------------------------------------
+echo.
+pause
+echo.
+echo Initializing...
+
+:checkCLI
+if not exist "arduino-cli.exe" (
+    echo.
+    echo The "arduino-cli.exe" file cannot be found. Please download it manually from the following link: 
+    echo https://arduino.github.io/arduino-cli/latest/installation/#download
+    echo Extract the "arduino-cli.exe" file to the same directory as this script, root of the project.
+    echo.
+    echo When the file is ready, press any key to check again.
+    set /a CLI_FOUND_FOLLOW_UP+=1
+    if %CLI_FOUND_FOLLOW_UP% geq 2 (
+        echo If you're still having issues, feel free to open a ticket at the following link:
+        echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues
+    )
+    pause
+    goto :checkCLI
+)
+if %CLI_FOUND_FOLLOW_UP% geq 1 (
+    echo File "arduino-cli.exe" found. Continuing...
+)
+
+echo Checking configs...
+arduino-cli %CONFIG_FILE% config set directories.data %CLI_TEMP%\data
+arduino-cli %CONFIG_FILE% config set directories.downloads %CLI_TEMP%\downloads
+arduino-cli %CONFIG_FILE% config set directories.user %CLI_TEMP%\user %*
+
+echo Fetching assets...
+set DATA_FLAG=0
+if not exist "%CLI_TEMP%\data" (
+    set /a "DATA_FLAG+=1"
+)
+if not exist "%CLI_TEMP%\downloads" (
+    set /a "DATA_FLAG+=1"
+)
+if %DATA_FLAG% gtr 0 (
+    arduino-cli %CONFIG_FILE% core update-index
+    arduino-cli %CONFIG_FILE% core install esp32:esp32
+) else (
+    echo Assets already installed. Skipping...
+)
+
+if not exist "%COMPILE_FLAG%" (
+    goto :compileFirmware
+)
+
+if exist "%COMPILE_FLAG%" (
+    echo.
+    set /p RECOMPILE="A previous firmware build was found, would you like to use it? (Y/N): "
+    if /i "!RECOMPILE!"=="N" (
+        goto :compileFirmware
+    )
+)
+:exitCompileFirmware
+
+echo.
+echo Firmware ready for serial installation...
+echo Fetching and displaying USB devices for upload...
+echo.
+arduino-cli board list
+echo Please find your Flipper Zero USB port name from the list above (may show as unknown).
+set /p PORT_NUMBER="Enter the port name here. For example 'COM3' capitalized with no quotes: "
+echo.
+echo Your ESP32-CAM is ready to be flashed. Please follow the instructions below:
+
+:uploadFirmware
+echo.
+echo 1. Ensure IO0 pin on ESP32-CAM is grounded to the proper GND pin.
+echo 2. Hold reset, and plug in your ESP32-CAM; hold for a few seconds and release.
+echo 3. Try to time your release simultaneously with continuing to the next step.
+echo 4. ESP32-CAM should now be in flash mode; allow some time for firmware upload.
+echo 5. Failure is common; verify all connections if errors persist and try again.
+echo 6. Disconnecting and reconnecting USB between attempts may sometimes work.
+echo.
+pause
+
+set RETRY_COUNT=1
+
+:uploadLoop
+echo.
+echo Preparing firmware upload... Attempt number !RETRY_COUNT!...
+arduino-cli %CONFIG_FILE% upload -p %PORT_NUMBER% --fqbn !SELECTED_BOARD! %FIRMWARE_SRC%
+if !ERRORLEVEL! EQU 0 (
+    goto :uploadSuccess
+) else (
+    if !RETRY_COUNT! lss 5 (
+        set /a RETRY_COUNT+=1
+        goto :uploadLoop
+    ) else (
+        echo.
+        set /p UPLOAD_TRY_AGAIN="Upload failed after 5 attempts, dont give up friend. Would you like to try again? (Y/N): "
+        if /i "!UPLOAD_TRY_AGAIN!"=="Y" (
+            set RETRY_COUNT=1
+            goto :uploadFirmware
+        ) else (
+            echo.
+            echo If you're still having issues, feel free to open a ticket at the following link:
+            echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite/issues
+            echo.
+            set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): "
+            if /i "!DELETE_TEMP!"=="Y" (
+                rmdir /s /q %CLI_TEMP%
+            )
+            echo.
+            pause
+            exit /b
+        )
+    )
+)
+
+:uploadSuccess
+echo.
+echo Firmware upload was successful.
+echo Cleaning up...
+echo Restoring default configs...
+arduino-cli %CONFIG_FILE% config set directories.data C:\temp\arduino-cli\data
+arduino-cli %CONFIG_FILE% config set directories.downloads C:\temp\arduino-cli\staging
+arduino-cli %CONFIG_FILE% config set directories.user C:\temp\arduino-cli\user
+set /p DELETE_TEMP="Would you like to delete the temporary files? (Y/N): "
+if /i "!DELETE_TEMP!"=="Y" (
+    rmdir /s /q %CLI_TEMP%
+)
+echo.
+echo Fin. Happy programming friend.
+echo.
+pause
+exit /b
+
+:compileFirmware
+set /p USE_DEFAULT_BOARD="Install to default AI-Thinker ESP32-CAM board with FQBN '%DEFAULT_BOARD_FQBN%'? (Y/N): "
+if /i "%USE_DEFAULT_BOARD%"=="N" (
+    echo Warning - This script has not been tested with other boards. Please use at your own risk.
+    set /p SHOW_BOARDS="Display all possible ESP32 board names and FQBN's? (Y/N): "
+    if /i "!SHOW_BOARDS!"=="Y" (
+        echo.
+        arduino-cli board listall
+    )
+    set /p SELECTED_BOARD="Please enter your board FQBN. For example '%DEFAULT_BOARD_FQBN%' with no quotes: "
+)
+echo.
+echo Compiling firmware, this will take a moment...
+echo.
+arduino-cli %CONFIG_FILE% compile --fqbn !SELECTED_BOARD! %FIRMWARE_SRC%
+if %ERRORLEVEL% EQU 0 (
+    echo.
+    echo Firmware compiled successfully.
+    type nul > %COMPILE_FLAG%
+) else (
+    echo.
+    set /p TRY_COMPILE_AGAIN="Firmware failed to compile. Please see the error log above. Try again? (Y/N): "
+    echo.
+    if /i "!TRY_COMPILE_AGAIN!"=="Y" (
+        goto :compileFirmware
+    )
+    pause
+    exit /b
+)
+
+goto :exitCompileFirmware

+ 22 - 0
firmware/camera.h

@@ -0,0 +1,22 @@
+#ifndef INITIALIZE_CAMERA_H
+#define INITIALIZE_CAMERA_H
+
+#include <FS.h>
+#include <esp_camera.h>
+
+#include "camera_config.h"
+#include "camera_model.h"
+
+/** Initialize the camera. */
+void initialize_camera();
+
+/** Turn the flash off. */
+void toggle_flash_off();
+
+/** Turn the flash on. */
+void toggle_flash_on();
+
+/** If the flash state ever gets out of sync with the camera model, fix it. */
+void handle_flash_state();
+
+#endif

+ 47 - 0
firmware/camera.ino

@@ -0,0 +1,47 @@
+#include "camera.h"
+
+void initialize_camera() {
+    // Initialize camera.
+    esp_err_t err = esp_camera_init(&camera_config);
+    if (err != ESP_OK) {
+        return;
+    }
+
+    // Check if the flash is already on, if it is turn it off.
+    if (camera_model.isFlashEnabled) {
+        toggle_flash_off();
+    }
+
+    // Get the camera sensor reference.
+    sensor_t* cam = esp_camera_sensor_get();
+
+    cam->set_contrast(cam, 0); // Set initial contrast.
+    cam->set_vflip(cam, true); // Set initial vertical flip.
+
+    // cam->set_hmirror(cam, false); // Set initial horizontal mirror.
+    // cam->set_brightness(cam, 0);  // Set initial brightness.
+    // cam->set_saturation(cam, 0); // Set initial saturation.
+    // cam->set_sharpness(cam, 0); // Set initial sharpness.
+}
+
+void toggle_flash_off() {
+    pinMode(FLASH_GPIO_NUM, OUTPUT);
+    digitalWrite(FLASH_GPIO_NUM, LOW);
+    camera_model.isFlashEnabled = false;
+}
+
+void toggle_flash_on() {
+    pinMode(FLASH_GPIO_NUM, OUTPUT);
+    digitalWrite(FLASH_GPIO_NUM, HIGH);
+    camera_model.isFlashEnabled = true;
+}
+
+void handle_flash_state() {
+    // If the flash state ever gets out of sync with the camera model, fix it.
+    if (!camera_model.isFlashEnabled) {
+        int flashState = digitalRead(FLASH_GPIO_NUM);
+        if (flashState == HIGH) {
+            toggle_flash_off();
+        }
+    }
+}

+ 14 - 0
firmware/camera_config.h

@@ -0,0 +1,14 @@
+#ifndef CAMERA_CONFIG_H
+#define CAMERA_CONFIG_H
+
+#include <esp_camera.h>
+
+#include "pins.h"
+
+/** The camera configuration model. */
+extern camera_config_t camera_config;
+
+/** Initialize the camera configuration. */
+void initialize_camera_config();
+
+#endif

+ 30 - 0
firmware/camera_config.ino

@@ -0,0 +1,30 @@
+#include "camera_config.h"
+
+/** The camera configuration model. */
+camera_config_t camera_config;
+
+void initialize_camera_config() {
+    // Set initial camera configurations.
+    camera_config.ledc_channel = LEDC_CHANNEL_0;
+    camera_config.ledc_timer = LEDC_TIMER_0;
+    camera_config.pin_d0 = Y2_GPIO_NUM;
+    camera_config.pin_d1 = Y3_GPIO_NUM;
+    camera_config.pin_d2 = Y4_GPIO_NUM;
+    camera_config.pin_d3 = Y5_GPIO_NUM;
+    camera_config.pin_d4 = Y6_GPIO_NUM;
+    camera_config.pin_d5 = Y7_GPIO_NUM;
+    camera_config.pin_d6 = Y8_GPIO_NUM;
+    camera_config.pin_d7 = Y9_GPIO_NUM;
+    camera_config.pin_xclk = XCLK_GPIO_NUM;
+    camera_config.pin_pclk = PCLK_GPIO_NUM;
+    camera_config.pin_vsync = VSYNC_GPIO_NUM;
+    camera_config.pin_href = HREF_GPIO_NUM;
+    camera_config.pin_sscb_sda = SIOD_GPIO_NUM;
+    camera_config.pin_sscb_scl = SIOC_GPIO_NUM;
+    camera_config.pin_pwdn = PWDN_GPIO_NUM;
+    camera_config.pin_reset = RESET_GPIO_NUM;
+    camera_config.xclk_freq_hz = 20000000;
+    camera_config.pixel_format = PIXFORMAT_GRAYSCALE;
+    camera_config.frame_size = FRAMESIZE_QQVGA;
+    camera_config.fb_count = 1;
+}

+ 43 - 0
firmware/camera_model.h

@@ -0,0 +1,43 @@
+#ifndef CAMERA_MODEL_H
+#define CAMERA_MODEL_H
+
+#include <stdint.h>
+
+/**
+ * The dithering algorithms available.
+ */
+typedef enum {
+    FLOYD_STEINBERG,
+    JARVIS_JUDICE_NINKE,
+    STUCKI,
+} DitheringAlgorithm;
+
+typedef struct {
+    /**
+     * Flag to enable or disable dithering.
+     */
+    bool isDitheringEnabled;
+    /**
+     * Flag to represent the flash state when saving pictures to the Flipper.
+     */
+    bool isFlashEnabled;
+    /**
+     * Flag to invert pixel colors.
+     */
+    bool isInvertEnabled;
+    /**
+     * Flag to stop or start the stream.
+     */
+    bool isStreamEnabled;
+    /**
+     * Holds the currently selected dithering algorithm.
+     */
+    DitheringAlgorithm ditherAlgorithm;
+} CameraModel;
+
+/** The camera model. */
+extern CameraModel camera_model;
+
+void initialize_camera_model();
+
+#endif

+ 13 - 0
firmware/camera_model.ino

@@ -0,0 +1,13 @@
+#include "camera_model.h"
+
+/** The camera model. */
+CameraModel camera_model;
+
+void initialize_camera_model() {
+    // Set up camera model defaults.
+    camera_model.isDitheringEnabled = true;
+    camera_model.isFlashEnabled = false;
+    camera_model.isInvertEnabled = false;
+    camera_model.isStreamEnabled = true;
+    camera_model.ditherAlgorithm = FLOYD_STEINBERG;
+}

+ 11 - 0
firmware/dither_image.h

@@ -0,0 +1,11 @@
+#ifndef DITHER_IMAGE_H
+#define DITHER_IMAGE_H
+
+#include <esp_camera.h>
+
+#include "camera_model.h"
+
+/** Dither the image using the selected algorithm. */
+void dither_image(camera_fb_t* frame_buffer);
+
+#endif

+ 82 - 0
firmware/dither_image.ino

@@ -0,0 +1,82 @@
+#include "dither_image.h"
+
+void dither_image(camera_fb_t* frame_buffer) {
+    for (uint8_t y = 0; y < frame_buffer->height; ++y) {
+        for (uint8_t x = 0; x < frame_buffer->width; ++x) {
+            size_t current = (y * frame_buffer->width) + x;
+            uint8_t oldpixel = frame_buffer->buf[current];
+            uint8_t newpixel = oldpixel >= 128 ? 255 : 0;
+            frame_buffer->buf[current] = newpixel;
+            int8_t quant_error = oldpixel - newpixel;
+
+            // Apply error diffusion based on the selected algorithm
+            switch (camera_model.ditherAlgorithm) {
+            case JARVIS_JUDICE_NINKE:
+                frame_buffer->buf[(y * frame_buffer->width) + x + 1] +=
+                    quant_error * 7 / 48;
+                frame_buffer->buf[(y * frame_buffer->width) + x + 2] +=
+                    quant_error * 5 / 48;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x - 2] +=
+                    quant_error * 3 / 48;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x - 1] +=
+                    quant_error * 5 / 48;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x] +=
+                    quant_error * 7 / 48;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x + 1] +=
+                    quant_error * 5 / 48;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x + 2] +=
+                    quant_error * 3 / 48;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x - 2] +=
+                    quant_error * 1 / 48;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x - 1] +=
+                    quant_error * 3 / 48;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x] +=
+                    quant_error * 5 / 48;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x + 1] +=
+                    quant_error * 3 / 48;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x + 2] +=
+                    quant_error * 1 / 48;
+                break;
+            case STUCKI:
+                frame_buffer->buf[(y * frame_buffer->width) + x + 1] +=
+                    quant_error * 8 / 42;
+                frame_buffer->buf[(y * frame_buffer->width) + x + 2] +=
+                    quant_error * 4 / 42;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x - 2] +=
+                    quant_error * 2 / 42;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x - 1] +=
+                    quant_error * 4 / 42;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x] +=
+                    quant_error * 8 / 42;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x + 1] +=
+                    quant_error * 4 / 42;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x + 2] +=
+                    quant_error * 2 / 42;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x - 2] +=
+                    quant_error * 1 / 42;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x - 1] +=
+                    quant_error * 2 / 42;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x] +=
+                    quant_error * 4 / 42;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x + 1] +=
+                    quant_error * 2 / 42;
+                frame_buffer->buf[(y + 2) * frame_buffer->width + x + 2] +=
+                    quant_error * 1 / 42;
+                break;
+            case FLOYD_STEINBERG:
+            default:
+                // Default to Floyd-Steinberg dithering if an invalid
+                // algorithm is selected
+                frame_buffer->buf[(y * frame_buffer->width) + x + 1] +=
+                    quant_error * 7 / 16;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x - 1] +=
+                    quant_error * 3 / 16;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x] +=
+                    quant_error * 5 / 16;
+                frame_buffer->buf[(y + 1) * frame_buffer->width + x + 1] +=
+                    quant_error * 1 / 16;
+                break;
+            }
+        }
+    }
+}

+ 12 - 0
firmware/firmware.h

@@ -0,0 +1,12 @@
+#ifndef FIRMWARE_H
+#define FIRMWARE_H
+
+#include <esp_camera.h>
+
+#include "camera.h"
+#include "camera_config.h"
+#include "camera_model.h"
+#include "process_image.h"
+#include "process_serial_commands.h"
+
+#endif

+ 30 - 0
firmware/firmware.ino

@@ -0,0 +1,30 @@
+#include "firmware.h"
+
+void setup() {
+    // Begin serial communication.
+    Serial.begin(230400); // 115200
+
+    // Initialize the camera model.
+    initialize_camera_model();
+
+    // Initialize the camera configuration.
+    initialize_camera_config();
+
+    // Initialize the camera.
+    initialize_camera();
+}
+
+// Main loop of the program.
+void loop() {
+    if (camera_model.isStreamEnabled) {
+        camera_fb_t* frame_buffer = esp_camera_fb_get();
+        if (frame_buffer) {
+            process_image(frame_buffer);
+            // Return the frame buffer back to the camera driver.
+            esp_camera_fb_return(frame_buffer);
+        }
+        delay(50);
+    }
+    handle_flash_state();
+    process_serial_commands();
+}

+ 23 - 0
firmware/pins.h

@@ -0,0 +1,23 @@
+#ifndef PINS_H
+#define PINS_H
+
+// Define Pin numbers used by the camera.
+#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 SIOC_GPIO_NUM 27
+#define SIOD_GPIO_NUM 26
+#define VSYNC_GPIO_NUM 25
+#define XCLK_GPIO_NUM 0
+#define Y2_GPIO_NUM 5
+#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
+
+#endif

+ 13 - 0
firmware/process_image.h

@@ -0,0 +1,13 @@
+#ifndef PROCESS_IMAGE_H
+#define PROCESS_IMAGE_H
+
+#include <FS.h>
+#include <esp_camera.h>
+
+#include "camera_model.h"
+#include "dither_image.h"
+
+/** Process and send grayscale images back to the Flipper Zero. */
+void process_image(camera_fb_t* frame_buffer);
+
+#endif

+ 54 - 0
firmware/process_image.ino

@@ -0,0 +1,54 @@
+#include "process_image.h"
+
+void process_image(camera_fb_t* frame_buffer) {
+    // If dithering is not disabled, perform dithering on the image. Dithering
+    // is the process of approximating the look of a high-resolution grayscale
+    // image in a lower resolution by binary values (black & white), thereby
+    // representing different shades of gray.
+    if (camera_model.isDitheringEnabled) {
+        // Invokes the dithering process on the frame buffer.
+        dither_image(frame_buffer);
+    }
+
+    uint8_t flipper_y = 0;
+
+    // Iterating over specific rows of the frame buffer.
+    for (uint8_t y = 28; y < 92; ++y) {
+        Serial.print("Y:");      // Print "Y:" for every new row.
+        Serial.write(flipper_y); // Send the row identifier as a byte.
+
+        // Calculate the actual y index in the frame buffer 1D array by
+        // multiplying the y value with the width of the frame buffer. This
+        // gives the starting index of the row in the 1D array.
+        size_t true_y = y * frame_buffer->width;
+
+        // Iterating over specific columns of each row in the frame buffer.
+        for (uint8_t x = 16; x < 144;
+             x += 8) { // step by 8 as we're packing 8 pixels per byte.
+            uint8_t packed_pixels = 0;
+            // Packing 8 pixel values into one byte.
+            for (uint8_t bit = 0; bit < 8; ++bit) {
+                // Check the invert flag and pack the pixels accordingly.
+                if (camera_model.isInvertEnabled) {
+                    // If invert is true, consider pixel as 1 if it's more than
+                    // 127.
+                    if (frame_buffer->buf[true_y + x + bit] > 127) {
+                        packed_pixels |= (1 << (7 - bit));
+                    }
+                } else {
+                    // If invert is false, consider pixel as 1 if it's less than
+                    // 127.
+                    if (frame_buffer->buf[true_y + x + bit] < 127) {
+                        packed_pixels |= (1 << (7 - bit));
+                    }
+                }
+            }
+            Serial.write(packed_pixels); // Sending packed pixel byte.
+        }
+        // Move to the next row.
+        ++flipper_y;
+        // Ensure all data in the Serial buffer is sent before moving to the
+        // next iteration.
+        Serial.flush();
+    }
+}

+ 13 - 0
firmware/process_serial_commands.h

@@ -0,0 +1,13 @@
+#ifndef PROCESS_SERIAL_COMMANDS_H
+#define PROCESS_SERIAL_COMMANDS_H
+
+#include <esp_camera.h>
+
+#include "camera.h"
+#include "camera_model.h"
+#include "pins.h"
+
+/** Handle the serial input commands coming from the Flipper Zero. */
+void process_serial_commands();
+
+#endif

+ 63 - 0
firmware/process_serial_commands.ino

@@ -0,0 +1,63 @@
+#include "process_serial_commands.h"
+
+void process_serial_commands() {
+    if (Serial.available() > 0) {
+        char input = Serial.read();
+        sensor_t* cam = esp_camera_sensor_get();
+
+        switch (input) {
+        case '>': // Toggle dithering.
+            camera_model.isDitheringEnabled = !camera_model.isDitheringEnabled;
+            break;
+        case 'i': // Turn invert off.
+            camera_model.isInvertEnabled = false;
+            break;
+        case 'I': // Turn invert on.
+            camera_model.isInvertEnabled = true;
+            break;
+        case 'b': // Remove brightness.
+            cam->set_contrast(cam, cam->status.brightness - 1);
+            break;
+        case 'B': // Add brightness.
+            cam->set_contrast(cam, cam->status.brightness + 1);
+            break;
+        case 'c': // Remove contrast.
+            cam->set_contrast(cam, cam->status.contrast - 1);
+            break;
+        case 'C': // Add contrast.
+            cam->set_contrast(cam, cam->status.contrast + 1);
+            break;
+        case 'f': // Turn the flash off.
+            toggle_flash_off();
+            break;
+        case 'F': // Turn the flash on.
+            toggle_flash_on();
+            break;
+        case 'P': // Save image to the onboard SD card.
+            // @todo - Future feature.
+            // save_picture_to_sd_card();
+            break;
+        case 'M': // Toggle Mirror.
+            cam->set_hmirror(cam, !cam->status.hmirror);
+            break;
+        case 's': // Stop stream.
+            camera_model.isStreamEnabled = false;
+            break;
+        case 'S': // Start stream.
+            camera_model.isStreamEnabled = true;
+            break;
+        case '0': // Use Floyd Steinberg dithering.
+            camera_model.ditherAlgorithm = FLOYD_STEINBERG;
+            break;
+        case '1': // Use Jarvis Judice dithering.
+            camera_model.ditherAlgorithm = JARVIS_JUDICE_NINKE;
+            break;
+        case '2': // Use Stucki dithering.
+            camera_model.ditherAlgorithm = STUCKI;
+            break;
+        default:
+            // Do nothing.
+            break;
+        }
+    }
+}

+ 13 - 0
firmware/save_picture_to_sd_card.h

@@ -0,0 +1,13 @@
+#ifndef SAVE_PICTURE_TO_SD_CARD_H
+#define SAVE_PICTURE_TO_SD_CARD_H
+
+#include <SD_MMC.h>
+#include <esp_camera.h>
+
+/**
+ * Save the current picture to the onboard SD card.
+ * @todo - Future feature.
+ */
+void save_picture_to_sd_card();
+
+#endif

+ 58 - 0
firmware/save_picture_to_sd_card.ino

@@ -0,0 +1,58 @@
+#include "save_picture_to_sd_card.h"
+
+void save_picture_to_sd_card() {
+    sensor_t* cam = esp_camera_sensor_get();
+
+    // Check if the sensor is valid.
+    if (!cam) {
+        Serial.println("Failed to acquire camera sensor");
+        return;
+    }
+
+    // Set pixel format to JPEG for saving picture.
+    cam->set_pixformat(cam, PIXFORMAT_JPEG);
+
+    // Set frame size based on available PSRAM.
+    if (psramFound()) {
+        cam->set_framesize(cam, FRAMESIZE_UXGA);
+    } else {
+        cam->set_framesize(cam, FRAMESIZE_SVGA);
+    }
+
+    // Get a frame buffer from camera.
+    camera_fb_t* frame_buffer = esp_camera_fb_get();
+    if (!frame_buffer) {
+        // Camera capture failed
+        return;
+    }
+
+    if (!SD_MMC.begin()) {
+        // SD Card Mount Failed.
+        esp_camera_fb_return(frame_buffer);
+        return;
+    }
+
+    // Generate a unique filename.
+    String path = "/picture";
+    path += String(millis());
+    path += ".jpg";
+
+    fs::FS& fs = SD_MMC;
+    File file = fs.open(path.c_str(), FILE_WRITE);
+
+    if (!file) {
+        // Failed to open file in writing mode
+    } else {
+        if (file.write(frame_buffer->buf, frame_buffer->len) !=
+            frame_buffer->len) {
+            // Failed to write the image to the file
+        }
+        file.close(); // Close the file in any case.
+    }
+
+    // Update framesize back to the default.
+    cam->set_framesize(cam, FRAMESIZE_QQVGA);
+
+    // Return the frame buffer back to the camera driver.
+    esp_camera_fb_return(frame_buffer);
+}

+ 0 - 276
src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino

@@ -1,276 +0,0 @@
-#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 SIOC_GPIO_NUM     27
-#define SIOD_GPIO_NUM     26
-#define XCLK_GPIO_NUM      0
-#define VSYNC_GPIO_NUM    25
-
-#define Y2_GPIO_NUM        5
-#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;
-
-// Function prototypes
-void handleSerialInput();
-void initializeCamera();
-void processImage(camera_fb_t* fb);
-void ditherImage(camera_fb_t* fb);
-bool isDarkBit(uint8_t bit);
-
-// Dithering algorithm options
-enum DitheringAlgorithm {
-  FLOYD_STEINBERG,
-  JARVIS_JUDICE_NINKE,
-  STUCKI
-};
-
-// Default dithering algorithm
-DitheringAlgorithm ditherAlgorithm = FLOYD_STEINBERG;
-
-// Serial input flags
-bool disableDithering = false;
-bool invert = false;
-bool isFlashOn = false;
-bool rotated = false;
-bool stopStream = false;
-
-void setup() {
-  Serial.begin(230400);
-  initializeCamera();
-}
-
-void loop() {
-  if (!stopStream) {
-    // Frame buffer capture and processing
-    camera_fb_t* fb = esp_camera_fb_get();
-    if (fb) {
-      processImage(fb);
-      esp_camera_fb_return(fb);
-    }
-    delay(50);
-  }
-
-  handleSerialInput(); // Process serial input commands
-}
-
-void handleSerialInput() {
-  if (Serial.available() > 0) {
-    char input = Serial.read();
-    sensor_t* cameraSensor = esp_camera_sensor_get();
-
-    switch (input) {
-      case '>': // Toggle dithering
-        disableDithering = !disableDithering;
-        break;
-      case '<': // Toggle invert
-        invert = !invert;
-        break;
-      case 'B': // Add brightness
-        cameraSensor->set_contrast(cameraSensor, cameraSensor->status.brightness + 1);
-        break;
-      case 'b': // Remove brightness
-        cameraSensor->set_contrast(cameraSensor, cameraSensor->status.brightness - 1);
-        break;
-      case 'C': // Add contrast
-        cameraSensor->set_contrast(cameraSensor, cameraSensor->status.contrast + 1);
-        break;
-      case 'c': // Remove contrast
-        cameraSensor->set_contrast(cameraSensor, cameraSensor->status.contrast - 1);
-        break;
-      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);
-        break;
-      case 'S': // Start stream
-        stopStream = false;
-        break;
-      case 's': // Stop stream
-        stopStream = true;
-        break;
-      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;
-    }
-  }
-}
-
-void initializeCamera() {
-  // Set camera configuration
-  config.ledc_channel = LEDC_CHANNEL_0;
-  config.ledc_timer = LEDC_TIMER_0;
-  config.pin_d0 = Y2_GPIO_NUM;
-  config.pin_d1 = Y3_GPIO_NUM;
-  config.pin_d2 = Y4_GPIO_NUM;
-  config.pin_d3 = Y5_GPIO_NUM;
-  config.pin_d4 = Y6_GPIO_NUM;
-  config.pin_d5 = Y7_GPIO_NUM;
-  config.pin_d6 = Y8_GPIO_NUM;
-  config.pin_d7 = Y9_GPIO_NUM;
-  config.pin_xclk = XCLK_GPIO_NUM;
-  config.pin_pclk = PCLK_GPIO_NUM;
-  config.pin_vsync = VSYNC_GPIO_NUM;
-  config.pin_href = HREF_GPIO_NUM;
-  config.pin_sscb_sda = SIOD_GPIO_NUM;
-  config.pin_sscb_scl = SIOC_GPIO_NUM;
-  config.pin_pwdn = PWDN_GPIO_NUM;
-  config.pin_reset = RESET_GPIO_NUM;
-  config.xclk_freq_hz = 20000000;
-  config.pixel_format = PIXFORMAT_GRAYSCALE;
-  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) {
-    Serial.printf("Camera init failed with error 0x%x", err);
-    return;
-  }
-
-  // Set high contrast to make dithering easier
-  sensor_t* s = esp_camera_sensor_get();
-  s->set_contrast(s, 2);
-
-  // Set rotation
-  s->set_vflip(s, true);  // Vertical flip
-  s->set_hmirror(s, true);  // Horizontal mirror
-}
-
-void processImage(camera_fb_t* frameBuffer) {
-  if (!disableDithering) {
-    ditherImage(frameBuffer);
-  }
-
-  uint8_t flipper_y = 0;
-  for (uint8_t y = 28; y < 92; ++y) {
-    // Print the Y coordinate.
-    Serial.print("Y:");
-    Serial.print((char)flipper_y);
-
-    // Print the character.
-    // The y value to use in the frame buffer array.
-    size_t true_y = y * frameBuffer->width;
-
-    // For each column of 8 pixels in the current row.
-    for (uint8_t x = 16; x < 144; x += 8) {
-      // The current character being constructed.
-      char c = 0;
-
-      // For each pixel in the current column of 8.
-      for (uint8_t j = 0; j < 8; ++j) {
-        if (isDarkBit(frameBuffer->buf[true_y + x + (7 - j)])) {
-          // Shift the bit into the right position
-          c |= (uint8_t)1 << (uint8_t)j;
-        }
-      }
-
-      // Output the character.
-      Serial.print(c);
-    }
-
-    // Move to the next line.
-    ++flipper_y;
-    Serial.flush();
-  }
-}
-
-void ditherImage(camera_fb_t* fb) {
-  for (uint8_t y = 0; y < fb->height; ++y) {
-    for (uint8_t x = 0; x < fb->width; ++x) {
-      size_t current = (y * fb->width) + x;
-      uint8_t oldpixel = fb->buf[current];
-      uint8_t newpixel = oldpixel >= 128 ? 255 : 0;
-      fb->buf[current] = newpixel;
-      int8_t quant_error = oldpixel - newpixel;
-
-      // Apply error diffusion based on the selected algorithm
-      switch (ditherAlgorithm) {
-        case JARVIS_JUDICE_NINKE:
-          fb->buf[(y * fb->width) + x + 1] += quant_error * 7 / 48;
-          fb->buf[(y * fb->width) + x + 2] += quant_error * 5 / 48;
-          fb->buf[(y + 1) * fb->width + x - 2] += quant_error * 3 / 48;
-          fb->buf[(y + 1) * fb->width + x - 1] += quant_error * 5 / 48;
-          fb->buf[(y + 1) * fb->width + x] += quant_error * 7 / 48;
-          fb->buf[(y + 1) * fb->width + x + 1] += quant_error * 5 / 48;
-          fb->buf[(y + 1) * fb->width + x + 2] += quant_error * 3 / 48;
-          fb->buf[(y + 2) * fb->width + x - 2] += quant_error * 1 / 48;
-          fb->buf[(y + 2) * fb->width + x - 1] += quant_error * 3 / 48;
-          fb->buf[(y + 2) * fb->width + x] += quant_error * 5 / 48;
-          fb->buf[(y + 2) * fb->width + x + 1] += quant_error * 3 / 48;
-          fb->buf[(y + 2) * fb->width + x + 2] += quant_error * 1 / 48;
-          break;
-        case STUCKI:
-          fb->buf[(y * fb->width) + x + 1] += quant_error * 8 / 42;
-          fb->buf[(y * fb->width) + x + 2] += quant_error * 4 / 42;
-          fb->buf[(y + 1) * fb->width + x - 2] += quant_error * 2 / 42;
-          fb->buf[(y + 1) * fb->width + x - 1] += quant_error * 4 / 42;
-          fb->buf[(y + 1) * fb->width + x] += quant_error * 8 / 42;
-          fb->buf[(y + 1) * fb->width + x + 1] += quant_error * 4 / 42;
-          fb->buf[(y + 1) * fb->width + x + 2] += quant_error * 2 / 42;
-          fb->buf[(y + 2) * fb->width + x - 2] += quant_error * 1 / 42;
-          fb->buf[(y + 2) * fb->width + x - 1] += quant_error * 2 / 42;
-          fb->buf[(y + 2) * fb->width + x] += quant_error * 4 / 42;
-          fb->buf[(y + 2) * fb->width + x + 1] += quant_error * 2 / 42;
-          fb->buf[(y + 2) * fb->width + x + 2] += quant_error * 1 / 42;
-          break;
-        case FLOYD_STEINBERG:
-        default:
-          // Default to Floyd-Steinberg dithering if an invalid algorithm is selected
-          fb->buf[(y * fb->width) + x + 1] += quant_error * 7 / 16;
-          fb->buf[(y + 1) * fb->width + x - 1] += quant_error * 3 / 16;
-          fb->buf[(y + 1) * fb->width + x] += quant_error * 5 / 16;
-          fb->buf[(y + 1) * fb->width + x + 1] += quant_error * 1 / 16;
-          break;
-      }
-    }
-  }
-}
-
-// Returns true if the bit is "dark".
-bool isDarkBit(uint8_t bit) {
-  if (invert) {
-    return bit >= 128;
-  } else {
-    return bit < 128;
-  }
-}