Repository: CodyTolene/Flipper-Zero-Camera-Suite
Pull request #64: Fixed camera image rotation issue
Author: login: rnadyrshin
Labels: name: update-fap
description: New feature or request
Created: 2025-05-15T12:44:35Z | Merged: 2025-05-16T18:39:50Z

ESP32 now initializes the camera in 128×128 mode and sends the full frame buffer. Flipper Zero uses the appropriate frame region based on the image rotation mode.

Note: To ensure compatibility with the latest version of the application, please update the ESP32 firmware (/firmware/firmware.ino).




Diff:
diff --git a/fap/views/camera_suite_view_camera.c b/fap/views/camera_suite_view_camera.c
index caabae2..35e830c 100644
--- a/fap/views/camera_suite_view_camera.c
+++ b/fap/views/camera_suite_view_camera.c
@@ -8,31 +8,51 @@
 #include "../helpers/camera_suite_speaker.h"
 #include "../helpers/camera_suite_led.h"
 
-static void draw_pixel_by_orientation(Canvas* canvas, uint8_t x, uint8_t y, uint8_t orientation) {
+static uint8_t get_pixel(uint8_t* buf, uint8_t x, uint8_t y) {
+    uint32_t pix_cnt = (y * FRAME_WIDTH) + x;
+    uint32_t idx = pix_cnt / 8;
+    uint8_t bit = pix_cnt % 8;
+    return buf[idx] & (1 << (7 - bit)) ? 1 : 0;
+}
+
+static void draw_image(Canvas* canvas, uint8_t* cam_buf, uint8_t orientation) {
     furi_assert(canvas);
-    furi_assert(x);
-    furi_assert(y);
     furi_assert(orientation);
 
-    switch(orientation) {
-    default:
-    case 0: { // Camera rotated 0 degrees (right side up, default)
-        canvas_draw_dot(canvas, x, y);
-        break;
-    }
-    case 1: { // Camera rotated 90 degrees
+    for(size_t y = 0; y < FRAME_HEIGHT; y++) {
+        for(size_t x = 0; x < FRAME_WIDTH; x++) {
+            uint8_t x_cam;
+            uint8_t y_cam;
+
+            switch(orientation) {
+            default:
+            case 0: { // Camera rotated 0 degrees (right side up, default)
+                x_cam = x;
+                y_cam = y + 32;
+                break;
+            }
+            case 1: { // Camera rotated 90 degrees
+                x_cam = FRAME_WIDTH - y - 32 - 1;
+                y_cam = x;
+                break;
+            }
+            case 2: { // Camera rotated 180 degrees (upside down)
+                x_cam = FRAME_WIDTH - x - 1;
+                y_cam = FRAME_WIDTH - y - 32 - 1;
+                break;
+            }
+            case 3: { // Camera rotated 270 degrees
+                x_cam = y + 32;
+                y_cam = FRAME_WIDTH - x - 1;
+                break;
+            }
+            }
 
-        canvas_draw_dot(canvas, y, FRAME_WIDTH - 1 - x);
-        break;
-    }
-    case 2: { // Camera rotated 180 degrees (upside down)
-        canvas_draw_dot(canvas, x, FRAME_HEIGHT - 1 - y);
-        break;
-    }
-    case 3: { // Camera rotated 270 degrees
-        canvas_draw_dot(canvas, FRAME_HEIGHT - 1 - y, x);
-        break;
-    }
+            uint8_t pixel = get_pixel(cam_buf, x_cam, y_cam);
+            if(pixel) {
+                canvas_draw_dot(canvas, x, FRAME_HEIGHT - y - 1);
+            }
+        }
     }
 }
 
@@ -48,16 +68,8 @@ static void camera_suite_view_camera_draw(Canvas* canvas, void* model) {
     // Draw the frame.
     canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGHT);
 
-    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((uartDumpModel->pixels[p] & (1 << (7 - i))) != 0) {
-                draw_pixel_by_orientation(canvas, (x * 8) + i, y, uartDumpModel->orientation);
-            }
-        }
-    }
+    // Draw the image
+    draw_image(canvas, uartDumpModel->pixels, uartDumpModel->orientation);
 
     // Draw the pinout guide if the camera is not initialized.
     if(!uartDumpModel->is_initialized) {
@@ -215,7 +227,7 @@ static void save_image_to_flipper_sd_card(void* model) {
         // @todo - Save image based on orientation.
         for(size_t i = 64; i > 0; --i) {
             for(size_t j = 0; j < ROW_BUFFER_LENGTH; ++j) {
-                row_buffer[j] = uartDumpModel->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j];
+                row_buffer[j] = uartDumpModel->pixels[((i + 32 - 1) * ROW_BUFFER_LENGTH) + j];
             }
             storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH);
         }
diff --git a/fap/views/camera_suite_view_camera.h b/fap/views/camera_suite_view_camera.h
index 69ea188..3a88519 100644
--- a/fap/views/camera_suite_view_camera.h
+++ b/fap/views/camera_suite_view_camera.h
@@ -49,11 +49,11 @@
 
 #define BITMAP_HEADER_LENGTH 62
 #define FRAME_BIT_DEPTH      1
-#define FRAME_BUFFER_LENGTH  1024
+#define FRAME_BUFFER_LENGTH  (2 * 1024)
 #define FRAME_HEIGHT         64
 #define FRAME_WIDTH          128
 #define HEADER_LENGTH        3 // 'Y', ':', and row identifier
-#define LAST_ROW_INDEX       1008
+#define LAST_ROW_INDEX       (2 * 1008)
 #define RING_BUFFER_LENGTH   19
 #define ROW_BUFFER_LENGTH    16
 
diff --git a/firmware-flash.bat b/firmware-flash.bat
index 5f95698..1eb7bae 100644
--- a/firmware-flash.bat
+++ b/firmware-flash.bat
@@ -12,10 +12,8 @@ set FIRMWARE_SRC=%CD%\firmware\firmware.ino
 set SELECTED_BOARD=%DEFAULT_BOARD_FQBN%
 
 chcp 65001 > nul
-echo ┏┓   ┓    ┏┳┓  ┓
-echo ┃ ┏┓┏┫┓┏   ┃ ┏┓┃┏┓┏┓┏┓
-echo ┗┛┗┛┗┻┗┫   ┻ ┗┛┗┗ ┛┗┗
-echo        ┛  https://github.com/CodyTolene
+echo Cody Tolene
+echo https://github.com/CodyTolene
 echo.
 echo Flipper Zero - ESP32-CAM Firmware Flasher - Windows 10+
 echo https://github.com/CodyTolene/Flipper-Zero-Camera-Suite
diff --git a/firmware/camera_config.cpp b/firmware/camera_config.cpp
index 614c8e7..6af34fe 100644
--- a/firmware/camera_config.cpp
+++ b/firmware/camera_config.cpp
@@ -27,5 +27,5 @@ void set_camera_config_defaults() {
   camera_config.pixel_format = PIXFORMAT_GRAYSCALE;
   camera_config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
   camera_config.fb_count = 1;
-  camera_config.frame_size = FRAMESIZE_QQVGA;
+  camera_config.frame_size = FRAMESIZE_128X128;
 }
diff --git a/firmware/stream_to_serial.cpp b/firmware/stream_to_serial.cpp
index 92e23ff..5899481 100644
--- a/firmware/stream_to_serial.cpp
+++ b/firmware/stream_to_serial.cpp
@@ -14,8 +14,8 @@ void stream_to_serial() {
 
   uint8_t flipper_y = 0;
 
-  // Iterating over specific rows of the frame buffer.
-  for (uint8_t y = 28; y < 92; ++y) {
+  // Iterating over all rows of the frame buffer.
+  for (uint8_t y = 0; y < 128; y++) {
     Serial.print("Y:");      // Print "Y:" for every new row.
     Serial.write(flipper_y); // Send the row identifier as a byte.
 
@@ -24,9 +24,8 @@ void stream_to_serial() {
     // 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.
+    // Iterating over all columns of each row in the frame buffer.
+    for (uint8_t x = 0; x < 128; 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) {


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes the camera image rotation issue by updating the ESP32 firmware to initialize the camera in 128×128 mode and adjusting the frame buffer processing to send the full image. Key changes include:
- Updating the row and column iteration boundaries in the serial stream to cover the full 128×128 image.
- Changing the camera configuration from QQVGA to 128×128 mode.
- Adjusting buffer size macros and pixel offset calculations in the camera suite view to accommodate the new image dimensions.

### Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

| File                                        | Description                                                                       |
| ------------------------------------------- | --------------------------------------------------------------------------------- |
| firmware/stream_to_serial.cpp               | Updated loop boundaries to process rows 0–127 and columns 0–127 for full image data |
| firmware/camera_config.cpp                  | Changed frame size from FRAMESIZE_QQVGA to FRAMESIZE_128X128                        |
| fap/views/camera_suite_view_camera.h        | Increased FRAME_BUFFER_LENGTH and LAST_ROW_INDEX to support a larger buffer         |
| fap/views/camera_suite_view_camera.c        | Modified image drawing and saving offsets to correctly map the new camera output     |




state: COMMENTED
author_association: NONE
submitted_at: 2025-05-16T18:23:36Z
user: login: CodyTolene
body: Tested and working good @rnadyrshin!

I will get these updates pushed up to the official app store and give credit to you.

Thank you
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-16T18:39:35Z

Review comments:
diff_hunk: @@ -15,7 +15,7 @@ void stream_to_serial() {
   uint8_t flipper_y = 0;
 
   // Iterating over specific rows of the frame buffer.
-  for (uint8_t y = 28; y < 92; ++y) {
+  for (uint8_t y = 0; y < 128; y++) {
path: firmware/stream_to_serial.cpp
user: login: Copilot
body: The comment above this loop indicates iterating over specific rows, but the new boundaries cover the full frame (0–127). Please update the comment to reflect that the entire frame is now processed.
created_at: 2025-05-16T18:23:35Z
author_association: NONE
diff_hunk: @@ -27,5 +27,5 @@ void set_camera_config_defaults() {
   camera_config.pixel_format = PIXFORMAT_GRAYSCALE;
   camera_config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
   camera_config.fb_count = 1;
-  camera_config.frame_size = FRAMESIZE_QQVGA;
+  camera_config.frame_size = FRAMESIZE_128X128; // FRAMESIZE_QQVGA;
path: firmware/camera_config.cpp
user: login: Copilot
body: [nitpick] Consider removing or clarifying the commented FRAMESIZE_QQVGA if it is no longer needed, to avoid confusion about the intended camera mode.
created_at: 2025-05-16T18:23:35Z
author_association: NONE
diff_hunk: @@ -49,11 +49,11 @@
 
 #define BITMAP_HEADER_LENGTH 62
 #define FRAME_BIT_DEPTH      1
-#define FRAME_BUFFER_LENGTH  1024
+#define FRAME_BUFFER_LENGTH  2 * 1024
 #define FRAME_HEIGHT         64
 #define FRAME_WIDTH          128
 #define HEADER_LENGTH        3 // 'Y', ':', and row identifier
-#define LAST_ROW_INDEX       1008
+#define LAST_ROW_INDEX       2 * 1008
path: fap/views/camera_suite_view_camera.h
user: login: Copilot
body: Enclose the multiplication in parentheses (e.g., (#define LAST_ROW_INDEX (2 * 1008))) to ensure correct evaluation and improve maintainability.
```suggestion
#define LAST_ROW_INDEX       (2 * 1008)
```
created_at: 2025-05-16T18:23:35Z
author_association: NONE
diff_hunk: @@ -215,7 +227,7 @@ static void save_image_to_flipper_sd_card(void* model) {
         // @todo - Save image based on orientation.
         for(size_t i = 64; i > 0; --i) {
             for(size_t j = 0; j < ROW_BUFFER_LENGTH; ++j) {
-                row_buffer[j] = uartDumpModel->pixels[((i - 1) * ROW_BUFFER_LENGTH) + j];
+                row_buffer[j] = uartDumpModel->pixels[((i + 32 - 1) * ROW_BUFFER_LENGTH) + j];
path: fap/views/camera_suite_view_camera.c
user: login: Copilot
body: [nitpick] Extract the magic number 32 into a named constant (e.g., VERTICAL_OFFSET) to improve code clarity and maintainability, given its repeated use across image processing logic.
```suggestion
                row_buffer[j] = uartDumpModel->pixels[((i + VERTICAL_OFFSET - 1) * ROW_BUFFER_LENGTH) + j];
```
created_at: 2025-05-16T18:23:36Z
author_association: NONE

PR comments:
user: login: CodyTolene
created_at: 2025-05-16T00:57:01Z
body: Thank you for your work on this! This is awesome!

I will try to review this as soon as I can, likely this weekend, and get it worked in.

Also it looks like there are a few linting errors reporting in the build check. Let me know if you need any help or guidance on that. 

Thanks again, I'll check back in soon 🙂
author_association: OWNER
user: login: rnadyrshin
created_at: 2025-05-16T11:42:02Z
body: Oh, thanks for the feedback. I’ve fixed the code formatting
author_association: CONTRIBUTOR
user: login: CodyTolene
created_at: 2025-05-16T19:02:46Z
body: App update submitted to the Flipper Zero team:
https://github.com/flipperdevices/flipper-application-catalog/pull/752

Your changes should be up on the Application Catalog once this get's merged.

Cheers mate, thanks again
author_association: OWNER