├── funding.yml ├── images ├── 1.png ├── 2.png └── 3.gif ├── fap_source └── camera │ ├── icon.png │ ├── application.fam │ ├── camera.h │ └── camera.c ├── README.md ├── LICENSE └── esp32_firmware └── esp32_cam_uart_stream └── esp32_cam_uart_stream.ino /funding.yml: -------------------------------------------------------------------------------- 1 | ko_fi: Z4urce -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Z4urce/flipperzero-camera/HEAD/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Z4urce/flipperzero-camera/HEAD/images/2.png -------------------------------------------------------------------------------- /images/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Z4urce/flipperzero-camera/HEAD/images/3.gif -------------------------------------------------------------------------------- /fap_source/camera/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Z4urce/flipperzero-camera/HEAD/fap_source/camera/icon.png -------------------------------------------------------------------------------- /fap_source/camera/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="camera", 3 | name="[ESP32] Camera", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="camera_app", 6 | cdefines=["APP_CAMERA"], 7 | requires=["gui"], 8 | stack_size=8*1024, 9 | order=1, 10 | fap_icon="icon.png", 11 | fap_category="GPIO", 12 | fap_description="ESP32-CAM live feed and photo capture", 13 | fap_author="Z4urce", 14 | fap_weburl="https://github.com/Z4urce/flipper-camera" 15 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Camera application for Flipper Zero 2 | Buy Me a Coffee at ko-fi.com 3 | 4 | Requires: [Esp32-Cam module](https://amzn.to/3WMEO0I). 5 | 6 | Download and install instructions: [Latest release](https://github.com/Z4urce/flipperzero-camera/releases). 7 | 8 | Usage: 9 | - Up - Increase contrast 10 | - Down - Decrease contract 11 | - Left - Invert colors 12 | - Right - Toggle dithering 13 | - Ok - Save photos to SD card (DCIM folder) 14 | - Back - Close application 15 | 16 | How it looks like: 17 | 18 | ![How it looks like](images/3.gif) 19 | 20 | Captures from the qFlipper: 21 | 22 | ![Standard capture](images/1.png) 23 | ![With dithering](images/2.png) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Zalán Kórósi 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /fap_source/camera/camera.h: -------------------------------------------------------------------------------- 1 | // TODO 2 | // (DONE) Fix performance when not being charged 3 | // (DONE) Add UART command parsing to Esp32 4 | // (DONE) Prepare application and icon on github 5 | // (DONE) Write snapshots to SD card 6 | // 5. Set a constant refresh rate to the Flipper's display 7 | // 6. Emulate grayscale 8 | // 7. Photo browser app 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #define THREAD_ALLOC 2048 25 | 26 | #define FRAME_WIDTH 128 27 | #define FRAME_HEIGTH 64 28 | #define FRAME_BIT_DEPTH 1 29 | #define FRAME_BUFFER_LENGTH (FRAME_WIDTH * FRAME_HEIGTH * FRAME_BIT_DEPTH / 8) // 128*64*1 / 8 = 1024 30 | #define ROW_BUFFER_LENGTH (FRAME_WIDTH / 8) // 128/8 = 16 31 | #define LAST_ROW_INDEX (FRAME_BUFFER_LENGTH - ROW_BUFFER_LENGTH) // 1024 - 16 = 1008 32 | #define RING_BUFFER_LENGTH (ROW_BUFFER_LENGTH + 3) // ROW_BUFFER_LENGTH + Header => 16 + 3 = 19 33 | #define BITMAP_HEADER_LENGTH 62 34 | #define IMAGE_FILE_DIRECTORY_PATH EXT_PATH("DCIM") 35 | 36 | static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = { 37 | 0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 38 | 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 39 | 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 40 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 41 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 42 | 0xFF, 0x00 43 | }; 44 | 45 | const uint8_t _I_DolphinCommon_56x48_0[] = {0x01,0x00,0xdf,0x00,0x00,0x1f,0xfe,0x0e,0x05,0x3f,0x04,0x06,0x78,0x06,0x30,0x20,0xf8,0x00,0xc6,0x12,0x1c,0x04,0x0c,0x0a,0x38,0x08,0x08,0x0c,0x60,0xc0,0x21,0xe0,0x04,0x0a,0x18,0x02,0x1b,0x00,0x18,0xa3,0x00,0x21,0x90,0x01,0x8a,0x20,0x02,0x19,0x80,0x18,0x80,0x64,0x09,0x20,0x89,0x81,0x8c,0x3e,0x41,0xe2,0x80,0x50,0x00,0x43,0x08,0x01,0x0c,0xfc,0x68,0x40,0x61,0xc0,0x50,0x30,0x00,0x63,0xa0,0x7f,0x80,0xc4,0x41,0x19,0x07,0xff,0x02,0x06,0x18,0x24,0x03,0x41,0xf3,0x2b,0x10,0x19,0x38,0x10,0x30,0x31,0x7f,0xe0,0x34,0x08,0x30,0x19,0x60,0x80,0x65,0x86,0x0a,0x4c,0x0c,0x30,0x81,0xb9,0x41,0xa0,0x54,0x08,0xc7,0xe2,0x06,0x8a,0x18,0x25,0x02,0x21,0x0f,0x19,0x88,0xd8,0x6e,0x1b,0x01,0xd1,0x1b,0x86,0x39,0x66,0x3a,0xa4,0x1a,0x50,0x06,0x48,0x18,0x18,0xd0,0x03,0x01,0x41,0x98,0xcc,0x60,0x39,0x01,0x49,0x2d,0x06,0x03,0x50,0xf8,0x40,0x3e,0x02,0xc1,0x82,0x86,0xc7,0xfe,0x0f,0x28,0x2c,0x91,0xd2,0x90,0x9a,0x18,0x19,0x3e,0x6d,0x73,0x12,0x16,0x00,0x32,0x49,0x72,0xc0,0x7e,0x5d,0x44,0xba,0x2c,0x08,0xa4,0xc8,0x82,0x06,0x17,0xe0,0x81,0x90,0x2a,0x40,0x61,0xe1,0xa2,0x44,0x0c,0x76,0x2b,0xe8,0x89,0x26,0x43,0x83,0x31,0x8c,0x78,0x0c,0xb0,0x48,0x10,0x1a,0xe0,0x00,0x63,}; 46 | const uint8_t* const _I_DolphinCommon_56x48[] = {_I_DolphinCommon_56x48_0}; 47 | const Icon I_DolphinCommon_56x48 = {.width=56,.height=48,.frame_count=1,.frame_rate=0,.frames=_I_DolphinCommon_56x48}; 48 | 49 | typedef struct UartDumpModel UartDumpModel; 50 | 51 | typedef struct { 52 | Gui* gui; 53 | NotificationApp* notification; 54 | ViewDispatcher* view_dispatcher; 55 | View* view; 56 | FuriThread* worker_thread; 57 | FuriStreamBuffer* rx_stream; 58 | } UartEchoApp; 59 | 60 | struct UartDumpModel { 61 | uint8_t pixels[FRAME_BUFFER_LENGTH]; 62 | 63 | bool initialized; 64 | 65 | uint8_t row_ringbuffer[RING_BUFFER_LENGTH]; 66 | uint8_t ringbuffer_index; 67 | }; 68 | 69 | typedef enum { 70 | WorkerEventReserved = (1 << 0), // Reserved for StreamBuffer internal event 71 | WorkerEventStop = (1 << 1), 72 | WorkerEventRx = (1 << 2), 73 | } WorkerEventFlags; 74 | 75 | #define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) 76 | 77 | const NotificationSequence sequence_notification = { 78 | &message_display_backlight_on, 79 | &message_delay_10, 80 | NULL, 81 | }; -------------------------------------------------------------------------------- /esp32_firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino: -------------------------------------------------------------------------------- 1 | #include "esp_camera.h" 2 | 3 | #define PWDN_GPIO_NUM 32 4 | #define RESET_GPIO_NUM -1 5 | #define XCLK_GPIO_NUM 0 6 | #define SIOD_GPIO_NUM 26 7 | #define SIOC_GPIO_NUM 27 8 | 9 | #define Y9_GPIO_NUM 35 10 | #define Y8_GPIO_NUM 34 11 | #define Y7_GPIO_NUM 39 12 | #define Y6_GPIO_NUM 36 13 | #define Y5_GPIO_NUM 21 14 | #define Y4_GPIO_NUM 19 15 | #define Y3_GPIO_NUM 18 16 | #define Y2_GPIO_NUM 5 17 | #define VSYNC_GPIO_NUM 25 18 | #define HREF_GPIO_NUM 23 19 | #define PCLK_GPIO_NUM 22 20 | 21 | 22 | void setup() { 23 | Serial.begin(230400); 24 | 25 | camera_config_t config; 26 | config.ledc_channel = LEDC_CHANNEL_0; 27 | config.ledc_timer = LEDC_TIMER_0; 28 | config.pin_d0 = Y2_GPIO_NUM; 29 | config.pin_d1 = Y3_GPIO_NUM; 30 | config.pin_d2 = Y4_GPIO_NUM; 31 | config.pin_d3 = Y5_GPIO_NUM; 32 | config.pin_d4 = Y6_GPIO_NUM; 33 | config.pin_d5 = Y7_GPIO_NUM; 34 | config.pin_d6 = Y8_GPIO_NUM; 35 | config.pin_d7 = Y9_GPIO_NUM; 36 | config.pin_xclk = XCLK_GPIO_NUM; 37 | config.pin_pclk = PCLK_GPIO_NUM; 38 | config.pin_vsync = VSYNC_GPIO_NUM; 39 | config.pin_href = HREF_GPIO_NUM; 40 | config.pin_sscb_sda = SIOD_GPIO_NUM; 41 | config.pin_sscb_scl = SIOC_GPIO_NUM; 42 | config.pin_pwdn = PWDN_GPIO_NUM; 43 | config.pin_reset = RESET_GPIO_NUM; 44 | config.xclk_freq_hz = 20000000; 45 | config.pixel_format = PIXFORMAT_GRAYSCALE; 46 | 47 | // We don't need a big frame 48 | config.frame_size = FRAMESIZE_QQVGA; 49 | config.fb_count = 1; 50 | 51 | // camera init 52 | esp_err_t err = esp_camera_init(&config); 53 | if (err != ESP_OK) { 54 | Serial.printf("Camera init failed with error 0x%x", err); 55 | return; 56 | } 57 | 58 | // Setting high contrast to make easier to dither 59 | sensor_t * s = esp_camera_sensor_get(); 60 | s->set_contrast(s, 2); 61 | } 62 | 63 | bool stop_stream = false; 64 | bool disable_dithering = false; 65 | bool invert = false; 66 | 67 | void loop() { 68 | 69 | // Reading serial 70 | if (Serial.available() > 0) { 71 | char r = Serial.read(); 72 | sensor_t * s = esp_camera_sensor_get(); 73 | 74 | switch(r) { 75 | case 'S': 76 | stop_stream = false; 77 | break; 78 | case 's': 79 | stop_stream = true; 80 | break; 81 | case 'D': 82 | disable_dithering = false; 83 | break; 84 | case 'd': 85 | disable_dithering = true; 86 | break; 87 | case 'C': 88 | s->set_contrast(s, s->status.contrast + 1); 89 | break; 90 | case 'c': 91 | s->set_contrast(s, s->status.contrast - 1); 92 | break; 93 | case 'B': 94 | s->set_contrast(s, s->status.brightness + 1); 95 | break; 96 | case 'b': 97 | s->set_contrast(s, s->status.brightness - 1); 98 | break; 99 | 100 | // Toggle cases 101 | case 'M': // Toggle Mirror 102 | s->set_hmirror(s, !s->status.hmirror); 103 | break; 104 | case '>': 105 | disable_dithering = !disable_dithering; 106 | break; 107 | case '<': 108 | invert = !invert; 109 | default: 110 | break; 111 | } 112 | } 113 | 114 | if (stop_stream){ 115 | return; 116 | } 117 | 118 | camera_fb_t* fb = esp_camera_fb_get(); 119 | 120 | if (!fb) { 121 | return; 122 | } 123 | 124 | //Length: 19200 125 | //Width: 160 126 | //Height: 120 127 | //Format: 2 128 | //Target: 128x64 129 | 130 | if (!disable_dithering) { 131 | DitherImage(fb); 132 | } 133 | 134 | uint8_t flipper_y = 0; 135 | for(uint8_t y = 28; y < 92; ++y) { 136 | Serial.print("Y:"); 137 | Serial.print((char)flipper_y); 138 | 139 | size_t true_y = y * fb->width; 140 | for (uint8_t x = 16; x < 144; x+=8){ 141 | char c = 0; 142 | for(uint8_t j = 0; j < 8; ++j){ 143 | if (IsDarkBit(fb->buf[true_y + x + (7-j)])){ 144 | c |= 1 << j; 145 | } 146 | } 147 | Serial.print(c); 148 | } 149 | 150 | ++flipper_y; 151 | Serial.flush(); 152 | } 153 | 154 | esp_camera_fb_return(fb); 155 | fb = NULL; 156 | delay(50); 157 | } 158 | 159 | bool IsDarkBit(uint8_t bit){ 160 | bool result = bit < 128; 161 | 162 | if (invert){ 163 | result = !result; 164 | } 165 | 166 | return result; 167 | } 168 | 169 | void DitherImage(camera_fb_t* fb) { 170 | for(uint8_t y = 0; y < fb->height; ++y){ 171 | for (uint8_t x = 0; x < fb->width; ++x){ 172 | size_t current = (y*fb->width) + x; 173 | uint8_t oldpixel = fb->buf[current]; 174 | uint8_t newpixel = oldpixel >= 128 ? 255 : 0; 175 | fb->buf[current] = newpixel; 176 | uint8_t quant_error = oldpixel - newpixel; 177 | fb->buf[(y*fb->width) + x + 1] = fb->buf[(y*fb->width) + x + 1] + quant_error * 7 / 16; 178 | fb->buf[(y+1*fb->width) + x-1] = fb->buf[(y+1*fb->width) + x-1] + quant_error * 3 / 16; 179 | fb->buf[(y + 1*fb->width) + x] = fb->buf[(y + 1*fb->width) + x] + quant_error * 5 / 16; 180 | fb->buf[(y+1*fb->width) + x+1] = fb->buf[(y+1*fb->width) + x+1] + quant_error * 1 / 16; 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /fap_source/camera/camera.c: -------------------------------------------------------------------------------- 1 | #include "camera.h" 2 | 3 | 4 | static void camera_view_draw_callback(Canvas* canvas, void* _model) { 5 | UartDumpModel* model = _model; 6 | 7 | // Prepare canvas 8 | //canvas_clear(canvas); 9 | canvas_set_color(canvas, ColorBlack); 10 | canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGTH); 11 | 12 | for(size_t p = 0; p < FRAME_BUFFER_LENGTH; ++p) { 13 | uint8_t x = p % ROW_BUFFER_LENGTH; // 0 .. 15 14 | uint8_t y = p / ROW_BUFFER_LENGTH; // 0 .. 63 15 | 16 | for(uint8_t i = 0; i < 8; ++i) { 17 | if((model->pixels[p] & (1 << (7 - i))) != 0) { 18 | canvas_draw_dot(canvas, (x * 8) + i, y); 19 | } 20 | } 21 | } 22 | 23 | if (!model->initialized){ 24 | canvas_draw_icon(canvas, 74, 16, &I_DolphinCommon_56x48); 25 | canvas_set_font(canvas, FontSecondary); 26 | canvas_draw_str(canvas, 8, 12, "Connect the ESP32-CAM"); 27 | canvas_draw_str(canvas, 20, 24, "VCC - 3V3"); 28 | canvas_draw_str(canvas, 20, 34, "GND - GND"); 29 | canvas_draw_str(canvas, 20, 44, "U0R - TX"); 30 | canvas_draw_str(canvas, 20, 54, "U0T - RX"); 31 | } 32 | } 33 | 34 | void get_timefilename(FuriString* name) { 35 | FuriHalRtcDateTime datetime = {0}; 36 | furi_hal_rtc_get_datetime(&datetime); 37 | furi_string_printf( 38 | name, 39 | EXT_PATH("DCIM/%.4d%.2d%.2d-%.2d%.2d%.2d.bmp"), 40 | datetime.year, 41 | datetime.month, 42 | datetime.day, 43 | datetime.hour, 44 | datetime.minute, 45 | datetime.second); 46 | } 47 | 48 | static void save_image(void* context) { 49 | UartEchoApp* app = context; 50 | furi_assert(app); 51 | 52 | NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION); 53 | 54 | // We need a storage struct (gain accesso to the filesystem API ) 55 | Storage* storage = furi_record_open(RECORD_STORAGE); 56 | 57 | // storage_file_alloc gives to us a File pointer using the Storage API. 58 | File* file = storage_file_alloc(storage); 59 | 60 | if(storage_common_stat(storage, IMAGE_FILE_DIRECTORY_PATH, NULL) == FSE_NOT_EXIST) { 61 | storage_simply_mkdir(storage, IMAGE_FILE_DIRECTORY_PATH); 62 | } 63 | 64 | // create file name 65 | FuriString* file_name = furi_string_alloc(); 66 | get_timefilename(file_name); 67 | 68 | // this functions open a file, using write access and creates new file if not exist. 69 | bool result = storage_file_open(file, furi_string_get_cstr(file_name), FSAM_WRITE, FSOM_OPEN_ALWAYS); 70 | //bool result = storage_file_open(file, EXT_PATH("DCIM/test.bmp"), FSAM_WRITE, FSOM_OPEN_ALWAYS); 71 | furi_string_free(file_name); 72 | 73 | if (result){ 74 | storage_file_write(file, bitmap_header, BITMAP_HEADER_LENGTH); 75 | with_view_model(app->view, UartDumpModel * model, { 76 | int8_t row_buffer[ROW_BUFFER_LENGTH]; 77 | for (size_t i = 64; i > 0; --i) { 78 | for (size_t j = 0; j < ROW_BUFFER_LENGTH; ++j){ 79 | row_buffer[j] = model->pixels[((i-1)*ROW_BUFFER_LENGTH) + j]; 80 | } 81 | storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH); 82 | } 83 | 84 | }, false); 85 | } 86 | 87 | // Closing the "file descriptor" 88 | storage_file_close(file); 89 | 90 | // Freeing up memory 91 | storage_file_free(file); 92 | 93 | notification_message(notifications, result ? &sequence_success : &sequence_error); 94 | } 95 | 96 | static bool camera_view_input_callback(InputEvent* event, void* context) { 97 | if (event->type == InputTypePress){ 98 | uint8_t data[1]; 99 | if (event->key == InputKeyUp){ 100 | data[0] = 'C'; 101 | } 102 | else if (event->key == InputKeyDown){ 103 | data[0] = 'c'; 104 | } 105 | else if (event->key == InputKeyRight){ 106 | data[0] = '>'; 107 | } 108 | else if (event->key == InputKeyLeft){ 109 | data[0] = '<'; 110 | } 111 | else if (event->key == InputKeyOk){ 112 | save_image(context); 113 | } 114 | furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1); 115 | } 116 | 117 | return false; 118 | } 119 | 120 | static uint32_t camera_exit(void* context) { 121 | UNUSED(context); 122 | return VIEW_NONE; 123 | } 124 | 125 | static void camera_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) { 126 | furi_assert(context); 127 | UartEchoApp* app = context; 128 | 129 | if(ev == UartIrqEventRXNE) { 130 | furi_stream_buffer_send(app->rx_stream, &data, 1, 0); 131 | furi_thread_flags_set(furi_thread_get_id(app->worker_thread), WorkerEventRx); 132 | } 133 | } 134 | 135 | static void process_ringbuffer(UartDumpModel* model, uint8_t byte) { 136 | //// 1. Phase: filling the ringbuffer 137 | if (model->ringbuffer_index == 0 && byte != 'Y'){ // First char has to be 'Y' in the buffer. 138 | return; 139 | } 140 | 141 | if (model->ringbuffer_index == 1 && byte != ':'){ // Second char has to be ':' in the buffer or reset. 142 | model->ringbuffer_index = 0; 143 | process_ringbuffer(model, byte); 144 | return; 145 | } 146 | 147 | model->row_ringbuffer[model->ringbuffer_index] = byte; // Assign current byte to the ringbuffer; 148 | ++model->ringbuffer_index; // Increment the ringbuffer index 149 | 150 | if (model->ringbuffer_index < RING_BUFFER_LENGTH){ // Let's wait 'till the buffer fills. 151 | return; 152 | } 153 | 154 | //// 2. Phase: flushing the ringbuffer to the framebuffer 155 | model->ringbuffer_index = 0; // Let's reset the ringbuffer 156 | model->initialized = true; // We've successfully established the connection 157 | size_t row_start_index = model->row_ringbuffer[2] * ROW_BUFFER_LENGTH; // Third char will determine the row number 158 | 159 | if (row_start_index > LAST_ROW_INDEX){ // Failsafe 160 | row_start_index = 0; 161 | } 162 | 163 | for (size_t i = 0; i < ROW_BUFFER_LENGTH; ++i) { 164 | model->pixels[row_start_index + i] = model->row_ringbuffer[i+3]; // Writing the remaining 16 bytes into the frame buffer 165 | } 166 | } 167 | 168 | static int32_t camera_worker(void* context) { 169 | furi_assert(context); 170 | UartEchoApp* app = context; 171 | 172 | while(1) { 173 | uint32_t events = 174 | furi_thread_flags_wait(WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); 175 | furi_check((events & FuriFlagError) == 0); 176 | 177 | if(events & WorkerEventStop) break; 178 | if(events & WorkerEventRx) { 179 | size_t length = 0; 180 | do { 181 | size_t intended_data_size = 64; 182 | uint8_t data[intended_data_size]; 183 | length = furi_stream_buffer_receive(app->rx_stream, data, intended_data_size, 0); 184 | 185 | if(length > 0) { 186 | //furi_hal_uart_tx(FuriHalUartIdUSART1, data, length); 187 | with_view_model( 188 | app->view, 189 | UartDumpModel * model, { 190 | for(size_t i = 0; i < length; i++) { 191 | process_ringbuffer(model, data[i]); 192 | } 193 | }, 194 | false); 195 | } 196 | } while(length > 0); 197 | 198 | notification_message(app->notification, &sequence_notification); 199 | with_view_model(app->view, UartDumpModel * model, { UNUSED(model); }, true); 200 | } 201 | } 202 | 203 | return 0; 204 | } 205 | 206 | static UartEchoApp* camera_app_alloc() { 207 | UartEchoApp* app = malloc(sizeof(UartEchoApp)); 208 | 209 | app->rx_stream = furi_stream_buffer_alloc(2048, 1); 210 | 211 | // Gui 212 | app->gui = furi_record_open(RECORD_GUI); 213 | app->notification = furi_record_open(RECORD_NOTIFICATION); 214 | 215 | // View dispatcher 216 | app->view_dispatcher = view_dispatcher_alloc(); 217 | view_dispatcher_enable_queue(app->view_dispatcher); 218 | view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); 219 | 220 | // Views 221 | app->view = view_alloc(); 222 | view_set_context(app->view, app); 223 | view_set_draw_callback(app->view, camera_view_draw_callback); 224 | view_set_input_callback(app->view, camera_view_input_callback); 225 | view_allocate_model(app->view, ViewModelTypeLocking, sizeof(UartDumpModel)); 226 | 227 | with_view_model( 228 | app->view, 229 | UartDumpModel * model, 230 | { 231 | for(size_t i = 0; i < FRAME_BUFFER_LENGTH; i++) { 232 | model->pixels[i] = 0; 233 | } 234 | }, 235 | true); 236 | 237 | view_set_previous_callback(app->view, camera_exit); 238 | view_dispatcher_add_view(app->view_dispatcher, 0, app->view); 239 | view_dispatcher_switch_to_view(app->view_dispatcher, 0); 240 | 241 | app->worker_thread = furi_thread_alloc_ex("UsbUartWorker", 2048, camera_worker, app); 242 | furi_thread_start(app->worker_thread); 243 | 244 | // Enable uart listener 245 | furi_hal_console_disable(); 246 | furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400); 247 | furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, camera_on_irq_cb, app); 248 | 249 | return app; 250 | } 251 | 252 | static void camera_app_free(UartEchoApp* app) { 253 | furi_assert(app); 254 | 255 | furi_hal_console_enable(); // this will also clear IRQ callback so thread is no longer referenced 256 | 257 | furi_thread_flags_set(furi_thread_get_id(app->worker_thread), WorkerEventStop); 258 | furi_thread_join(app->worker_thread); 259 | furi_thread_free(app->worker_thread); 260 | 261 | // Free views 262 | view_dispatcher_remove_view(app->view_dispatcher, 0); 263 | 264 | view_free(app->view); 265 | view_dispatcher_free(app->view_dispatcher); 266 | 267 | // Close gui record 268 | furi_record_close(RECORD_GUI); 269 | furi_record_close(RECORD_NOTIFICATION); 270 | app->gui = NULL; 271 | 272 | furi_stream_buffer_free(app->rx_stream); 273 | 274 | // Free rest 275 | free(app); 276 | } 277 | 278 | int32_t camera_app(void* p) { 279 | UNUSED(p); 280 | UartEchoApp* app = camera_app_alloc(); 281 | view_dispatcher_run(app->view_dispatcher); 282 | camera_app_free(app); 283 | return 0; 284 | } 285 | --------------------------------------------------------------------------------