Просмотр исходного кода

Add new auto-flash utility for the ESP-32 firmware.

Cody Tolene 2 лет назад
Родитель
Сommit
7cfa5c01a1
43 измененных файлов с 571 добавлено и 304 удалено
  1. 1 1
      .github/workflows/deploy-main.yml
  2. 2 2
      .github/workflows/lint-pull-request.yml
  3. 11 2
      .gitignore
  4. 1 1
      README.md
  5. 0 0
      fap/application.fam
  6. 1 0
      fap/camera_suite.c
  7. 6 0
      fap/camera_suite.h
  8. 0 0
      fap/docs/CHANGELOG.md
  9. 0 0
      fap/docs/README.md
  10. 0 0
      fap/helpers/camera_suite_custom_event.h
  11. 0 0
      fap/helpers/camera_suite_haptic.c
  12. 0 0
      fap/helpers/camera_suite_haptic.h
  13. 0 0
      fap/helpers/camera_suite_led.c
  14. 0 0
      fap/helpers/camera_suite_led.h
  15. 0 0
      fap/helpers/camera_suite_speaker.c
  16. 0 0
      fap/helpers/camera_suite_speaker.h
  17. 2 0
      fap/helpers/camera_suite_storage.c
  18. 1 0
      fap/helpers/camera_suite_storage.h
  19. 0 0
      fap/icons/camera_suite.png
  20. 0 0
      fap/manifest.yml
  21. 0 0
      fap/scenes/camera_suite_scene.c
  22. 0 0
      fap/scenes/camera_suite_scene.h
  23. 0 0
      fap/scenes/camera_suite_scene_camera.c
  24. 0 0
      fap/scenes/camera_suite_scene_config.h
  25. 0 0
      fap/scenes/camera_suite_scene_guide.c
  26. 0 0
      fap/scenes/camera_suite_scene_menu.c
  27. 29 0
      fap/scenes/camera_suite_scene_settings.c
  28. 0 0
      fap/scenes/camera_suite_scene_start.c
  29. 0 0
      fap/screenshots/camera_preview.png
  30. 0 0
      fap/screenshots/guide.png
  31. 0 0
      fap/screenshots/main_menu.png
  32. 0 0
      fap/screenshots/settings.png
  33. 0 0
      fap/screenshots/start_screen.png
  34. 34 22
      fap/views/camera_suite_view_camera.c
  35. 0 0
      fap/views/camera_suite_view_camera.h
  36. 0 0
      fap/views/camera_suite_view_guide.c
  37. 0 0
      fap/views/camera_suite_view_guide.h
  38. 0 0
      fap/views/camera_suite_view_start.c
  39. 0 0
      fap/views/camera_suite_view_start.h
  40. 27 0
      firmware/cli/arduino-cli.yaml
  41. 105 0
      firmware/cli/cli-install.bat
  42. 351 0
      firmware/firmware.ino
  43. 0 276
      src-firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino

+ 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

+ 11 - 2
.gitignore

@@ -1,11 +1,20 @@
 *.zip
+*.exe
 .DS_Store
 .clang-format
 .editorconfig
 .idea
+.submodules/*
 .vscode
+/firmware/cli/*
+/firmware/cli/**/*
+!/firmware/cli/cli-install.bat
+!/firmware/cli/arduino-cli.yaml
+/firmware/compile.flag
 /venv
 __pycache__
 dist/*
-src-fap/.gitignore
-.submodules/*
+fap/.clang-format
+fap/.editorconfig
+fap/.vscode
+fap/dist/*

+ 1 - 1
README.md

@@ -24,7 +24,7 @@
 - [Software Guide](#software-guide)
 - [Attributions](#attributions)
 - [Contributions](#contributions)
-- [Changelog](src-fap/docs/CHANGELOG.md)
+- [Changelog](fap/docs/CHANGELOG.md)
 
 ## Previews <a name="previews"></a>
 

+ 0 - 0
src-fap/application.fam → fap/application.fam


+ 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,

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


+ 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


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


+ 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


+ 29 - 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,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, flash_text[value_index]);
 
+    // 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 as JPEG to ESP32-CAM sd-card:",
+        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]);
+
     // 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


+ 34 - 22
src-fap/views/camera_suite_view_camera.c → fap/views/camera_suite_view_camera.c

@@ -130,21 +130,41 @@ static void save_image(void* _model) {
     // 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];
-        if(is_inverted) {
-            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);
+        CameraSuite* app = current_instance->context;
+        if(app->flash) {
+            if(app->jpeg) {
+                // Turn on local jpeg save. When this is enabled the ESP32-CAM
+                // will save the image to the SD card and saving the image to
+                // the Flipper SD card will be disabled/skipped.
+                furi_hal_uart_tx(FuriHalUartIdUSART1, 'J', 1);
+            } else {
+                // Turn off local jpeg save.
+                furi_hal_uart_tx(FuriHalUartIdUSART1, 'j', 1);
             }
-        } else {
-            for(size_t i = 0; i < 64; ++i) {
-                for(size_t j = 0; j < ROW_BUFFER_LENGTH; ++j) {
-                    row_buffer[j] = model->pixels[i * ROW_BUFFER_LENGTH + j];
+            // Initiate the onboard ESP32-CAM picture sequence. So far this
+            // includes turning on the flash and potentially saving jpeg
+            // locally to the ESP32-CAM SD card.
+            furi_hal_uart_tx(FuriHalUartIdUSART1, 'P', 1);
+        }
+        // If saving jpeg is enabled locally to the ESP32-CAM SD card, skip
+        // writing the image data to the Flipper Zero SD card.
+        if(!app->saveJpeg) {
+            // Write locally to the Flipper Zero SD card in the DCIM folder.
+            int8_t row_buffer[ROW_BUFFER_LENGTH];
+            if(is_inverted) {
+                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);
+                }
+            } else {
+                for(size_t i = 0; i < 64; ++i) {
+                    for(size_t j = 0; j < ROW_BUFFER_LENGTH; ++j) {
+                        row_buffer[j] = model->pixels[i * ROW_BUFFER_LENGTH + j];
+                    }
+                    storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH);
                 }
-                storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH);
             }
         }
     }
@@ -259,20 +279,12 @@ static bool camera_suite_view_camera_input(InputEvent* event, void* context) {
                 true);
             break;
         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 25ms to make sure flash is on before taking picture.
-                furi_delay_ms(25);
-            }
             // Take picture.
             with_view_model(
                 instance->view,
                 UartDumpModel * model,
                 {
+                    // If flash is enabled, flash the onboard ESP32-CAM LED.
                     camera_suite_play_happy_bump(instance->context);
                     camera_suite_play_input_sound(instance->context);
                     camera_suite_led_set_rgb(instance->context, 0, 0, 255);

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


+ 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


+ 27 - 0
firmware/cli/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:\Users\NULL\AppData\Local\Temp\arduino-cli\data
+  downloads: C:\Users\NULL\AppData\Local\Temp\arduino-cli\downloads
+  user: C:\Users\NULL\AppData\Local\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

+ 105 - 0
firmware/cli/cli-install.bat

@@ -0,0 +1,105 @@
+@echo off
+setlocal EnableDelayedExpansion
+
+set CLI_TEMP=%TEMP%\arduino-cli
+set CONFIG_FILE=--config-file .\arduino-cli.yaml
+set DEFAULT_BOARD_FQBN=esp32:esp32:esp32cam
+set SELECTED_BOARD=%DEFAULT_BOARD_FQBN%
+set CLI_FOUND_FOLLOW_UP=0
+set COMPILE_FLAG=..\compile.flag
+
+echo.
+
+:checkCLI
+if not exist "arduino-cli.exe" (
+    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
+    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...
+if not exist "%CLI_TEMP%" (
+    arduino-cli %CONFIG_FILE% core update-index
+    arduino-cli %CONFIG_FILE% core install esp32:esp32
+) else (
+    echo Assets already installed. Skipping...
+)
+
+echo Ready for installation...
+
+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" (
+    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: "
+)
+
+if exist "%COMPILE_FLAG%" (
+    set /p RECOMPILE="A previous firmware build was found, would you like to use it? (Y/N): "
+    if /i "!RECOMPILE!"=="N" (
+        goto :compileFirmware
+    )
+) else (
+    :compileFirmware
+    echo Compiling firmware, this will take a moment...
+    arduino-cli %CONFIG_FILE% compile --fqbn !SELECTED_BOARD! ..\firmware.ino
+    if %ERRORLEVEL% EQU 0 (
+        echo Compile complete. Ready to upload.
+        type nul > %COMPILE_FLAG%
+    ) else (
+        echo Compilation failed. Please correct the errors and try again.
+        exit /b
+    )
+)
+
+echo.
+arduino-cli board list
+echo Please find your Flipper Zero USB port from the list above (may show as unknown).
+set /p PORT_NUMBER="Enter it 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. Make sure your ESP32-CAM module is unplugged from your Flipper Zero.
+echo 2. Make sure you've grounded your IO0 pin on your ESP32-CAM module to the correct GND pin.
+echo 3. Plug your ESP32-CAM module with the reset button pressed right after going to the next step.
+echo 4. When "Connecting..." is displayed unpress the reset button.
+echo 5. It is common for this to fail a few times, keep trying and double check your connections.
+echo.
+pause
+
+echo.
+echo Preparing firmware upload...
+arduino-cli %CONFIG_FILE% upload -p %PORT_NUMBER% --fqbn !SELECTED_BOARD! ..\firmware.ino
+if %ERRORLEVEL% NEQ 0 (
+    echo.
+    set /p UPLOAD_TRY_AGAIN="Upload failed, would you like to retry? (Y/N): "
+    if /i "!UPLOAD_TRY_AGAIN!"=="Y" (
+        goto :uploadFirmware
+    )
+)
+
+echo.
+echo Fin. Happy programming friend.
+echo.
+pause
+exit /b

+ 351 - 0
firmware/firmware.ino

@@ -0,0 +1,351 @@
+#include "esp_camera.h"
+#include "FS.h"
+#include "SD_MMC.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 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
+
+// Structure to hold the camera configuration parameters.
+camera_config_t config;
+
+// Function prototypes.
+void handleSerialInput(camera_fb_t * fb);
+void initializeCamera();
+void processImage(camera_fb_t * fb);
+void ditherImage(camera_fb_t * fb);
+void saveFrameBufferToSDCard(camera_fb_t * fb);
+
+// Enumeration to represent the available dithering algorithms.
+enum DitheringAlgorithm {
+  FLOYD_STEINBERG,
+  JARVIS_JUDICE_NINKE,
+  STUCKI
+};
+
+// Holds the currently selected dithering algorithm.
+DitheringAlgorithm ditherAlgorithm = FLOYD_STEINBERG;
+
+// Flag to enable or disable dithering.
+bool disableDithering = false;
+
+// Flag to invert pixel colors.
+bool invert = false;
+
+// Flag to represent the flash state.
+bool isFlashOn = false;
+
+// Flag to represent whether the image is rotated.
+bool rotated = false;
+
+// Flag to stop or start the stream.
+bool stopStream = false;
+
+// Flag to store jpeg images to sd card.
+bool storeJpeg = false;
+
+void setup() {
+  // Start serial communication at 230400 baud rate.
+  Serial.begin(230400);
+  initializeCamera();
+}
+
+void loop() {
+  // Capture and process the frame buffer if streaming is enabled.
+  camera_fb_t * fb = esp_camera_fb_get();
+  
+  if (!stopStream) {
+    
+    if (fb) {
+      processImage(fb);
+      // Return the frame buffer back to the camera driver.
+      esp_camera_fb_return(fb);
+    }
+    // Delay for 10ms between each frame.
+    delay(10); 
+  }
+  
+  // Handle any available serial input commands.
+  handleSerialInput(fb); 
+}
+
+void handleSerialInput(camera_fb_t * fb) {
+  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 'j': // Toggle store jpeg to sd card off.
+      storeJpeg = false;
+      break;
+    case 'J': // Toggle store jpeg to sd card on.
+      storeJpeg = true;
+      break;
+    case 'P': // Picture sequence.
+      if (!isFlashOn) {
+        isFlashOn = true;
+        // Set up the flash light control pin (number 4) as an "output"
+        // so we can  turn the torch ON and OFF.
+        pinMode(FLASH_GPIO_NUM, OUTPUT);
+        // Turn on torch.
+        digitalWrite(FLASH_GPIO_NUM, HIGH);
+        if (storeJpeg) {
+          // Save jpeg image to sd card.
+          saveFrameBufferToSDCard(fb);
+          // Return the frame buffer back to the camera driver.
+          esp_camera_fb_return(fb);
+        }
+        // Give some time for Flipper to save locally with flash on.
+        delay(15); 
+        // Turn off torch.
+        digitalWrite(FLASH_GPIO_NUM, LOW);
+        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 configurations
+  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 initial contrast.
+  sensor_t * s = esp_camera_sensor_get();
+  s -> set_contrast(s, 0);
+
+  // Set rotation
+  s -> set_vflip(s, true); // Vertical flip
+  s -> set_hmirror(s, true); // Horizontal mirror
+}
+
+void processImage(camera_fb_t * frameBuffer) {
+  // 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 (!disableDithering) {
+    ditherImage(frameBuffer); // Invokes the dithering process on the 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 * frameBuffer -> 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 (invert) {
+          // If invert is true, consider pixel as 1 if it's more than 127.
+          if (frameBuffer -> 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 (frameBuffer -> buf[true_y + x + bit] < 127) {
+            packed_pixels |= (1 << (7 - bit));
+          }
+        }
+      }
+      Serial.write(packed_pixels); // Sending packed pixel byte.
+    }
+
+    ++flipper_y; // Move to the next row.
+    Serial.flush(); // Ensure all data in the Serial buffer is sent before moving to the next iteration.
+  }
+}
+
+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;
+      }
+    }
+  }
+}
+
+void saveFrameBufferToSDCard(camera_fb_t * fb) {
+  if (!SD_MMC.begin()) {
+    // Serial.println("SD Card Mount Failed");
+    return;
+  }
+
+  uint8_t cardType = SD_MMC.cardType();
+  if (cardType == CARD_NONE) {
+    // Serial.println("No SD Card attached");
+    return;
+  }
+
+  // Generate a unique filename
+  String path = "/picture";
+  path += String(millis());
+  path += ".jpg";
+
+  File file = SD_MMC.open(path.c_str(), FILE_WRITE);
+  if (!file) {
+    // Serial.println("Failed to open file for writing");
+    return;
+  }
+
+  // Write frame buffer to file
+  file.write(fb -> buf, fb -> len);
+
+  // Serial.println("File written to SD card");
+  file.close();
+}

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

@@ -1,276 +0,0 @@
-#include "esp_camera.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 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
-
-// Structure to hold the camera configuration parameters
-camera_config_t config;
-
-// Function prototypes
-void handleSerialInput();
-void initializeCamera();
-void processImage(camera_fb_t* fb);
-void ditherImage(camera_fb_t* fb);
-
-// Enumeration to represent the available dithering algorithms
-enum DitheringAlgorithm {
-  FLOYD_STEINBERG,
-  JARVIS_JUDICE_NINKE,
-  STUCKI
-};
-
-// Variables to hold state and configurations
-DitheringAlgorithm ditherAlgorithm = FLOYD_STEINBERG; // Holds the currently selected dithering algorithm
-bool disableDithering = false; // Flag to enable or disable dithering
-bool invert = false; // Flag to invert pixel colors
-bool isFlashOn = false; // Flag to represent the flash state
-bool rotated = false; // Flag to represent whether the image is rotated
-bool stopStream = false; // Flag to stop or start the stream
-
-void setup() {
-  // Start serial communication at 230400 baud rate
-  Serial.begin(230400);
-  initializeCamera();
-}
-
-void loop() {
-  if (!stopStream) {
-    // Capture and process the frame buffer if streaming is enabled
-    camera_fb_t* fb = esp_camera_fb_get(); // Get the frame buffer from the camera
-    if (fb) {
-      processImage(fb);
-      // Return the frame buffer back to the camera driver
-      esp_camera_fb_return(fb);
-    }
-    delay(50); // Delay for 50ms between each frame
-  }
-
-  handleSerialInput(); // Handle any available serial input commands
-}
-
-// Function to handle the serial input commands and perform the associated actions
-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 configurations
-  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 initial contrast.
-  sensor_t* s = esp_camera_sensor_get();
-  s->set_contrast(s, 0);
-
-  // Set rotation
-  s->set_vflip(s, true);  // Vertical flip
-  s->set_hmirror(s, true);  // Horizontal mirror
-}
-
-void processImage(camera_fb_t* frameBuffer) {
-  // 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 (!disableDithering) {
-    ditherImage(frameBuffer); // Invokes the dithering process on the 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 * frameBuffer->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(invert) {
-                // If invert is true, consider pixel as 1 if it's more than 127.
-                if(frameBuffer->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(frameBuffer->buf[true_y + x + bit] < 127) {
-                    packed_pixels |= (1 << (7 - bit));
-                }
-            }
-        }
-        Serial.write(packed_pixels); // Sending packed pixel byte.
-    }
-
-    ++flipper_y; // Move to the next row.
-    Serial.flush(); // Ensure all data in the Serial buffer is sent before moving to the next iteration.
-  }
-}
-
-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;
-      }
-    }
-  }
-}