diff --git a/esp32_firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino b/esp32_firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino new file mode 100644 index 0000000..1758b62 --- /dev/null +++ b/esp32_firmware/esp32_cam_uart_stream/esp32_cam_uart_stream.ino @@ -0,0 +1,183 @@ +#include "esp_camera.h" + +#define PWDN_GPIO_NUM 32 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 0 +#define SIOD_GPIO_NUM 26 +#define SIOC_GPIO_NUM 27 + +#define Y9_GPIO_NUM 35 +#define Y8_GPIO_NUM 34 +#define Y7_GPIO_NUM 39 +#define Y6_GPIO_NUM 36 +#define Y5_GPIO_NUM 21 +#define Y4_GPIO_NUM 19 +#define Y3_GPIO_NUM 18 +#define Y2_GPIO_NUM 5 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 23 +#define PCLK_GPIO_NUM 22 + + +void setup() { + Serial.begin(230400); + + camera_config_t config; + 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; + + // We don't need a big frame + config.frame_size = FRAMESIZE_QQVGA; + config.fb_count = 1; + + // camera init + esp_err_t err = esp_camera_init(&config); + if (err != ESP_OK) { + Serial.printf("Camera init failed with error 0x%x", err); + return; + } + + // Setting high contrast to make easier to dither + sensor_t * s = esp_camera_sensor_get(); + s->set_contrast(s, 2); +} + +bool stop_stream = false; +bool disable_dithering = false; +bool invert = false; + +void loop() { + + // Reading serial + if (Serial.available() > 0) { + char r = Serial.read(); + sensor_t * s = esp_camera_sensor_get(); + + switch(r) { + case 'S': + stop_stream = false; + break; + case 's': + stop_stream = true; + break; + case 'D': + disable_dithering = false; + break; + case 'd': + disable_dithering = true; + break; + case 'C': + s->set_contrast(s, s->status.contrast + 1); + break; + case 'c': + s->set_contrast(s, s->status.contrast - 1); + break; + case 'B': + s->set_contrast(s, s->status.brightness + 1); + break; + case 'b': + s->set_contrast(s, s->status.brightness - 1); + break; + + // Toggle cases + case 'M': // Toggle Mirror + s->set_hmirror(s, !s->status.hmirror); + break; + case '>': + disable_dithering = !disable_dithering; + break; + case '<': + invert = !invert; + default: + break; + } + } + + if (stop_stream){ + return; + } + + camera_fb_t* fb = esp_camera_fb_get(); + + if (!fb) { + return; + } + + //Length: 19200 + //Width: 160 + //Height: 120 + //Format: 2 + //Target: 128x64 + + if (!disable_dithering) { + DitherImage(fb); + } + + uint8_t flipper_y = 0; + for(uint8_t y = 28; y < 92; ++y) { + Serial.print("Y:"); + Serial.print((char)flipper_y); + + size_t true_y = y * fb->width; + for (uint8_t x = 16; x < 144; x+=8){ + char c = 0; + for(uint8_t j = 0; j < 8; ++j){ + if (IsDarkBit(fb->buf[true_y + x + (7-j)])){ + c |= 1 << j; + } + } + Serial.print(c); + } + + ++flipper_y; + Serial.flush(); + } + + esp_camera_fb_return(fb); + fb = NULL; + delay(50); +} + +bool IsDarkBit(uint8_t bit){ + bool result = bit < 128; + + if (invert){ + result = !result; + } + + return result; +} + +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; + uint8_t quant_error = oldpixel - newpixel; + fb->buf[(y*fb->width) + x + 1] = fb->buf[(y*fb->width) + x + 1] + quant_error * 7 / 16; + fb->buf[(y+1*fb->width) + x-1] = fb->buf[(y+1*fb->width) + x-1] + quant_error * 3 / 16; + fb->buf[(y + 1*fb->width) + x] = fb->buf[(y + 1*fb->width) + x] + quant_error * 5 / 16; + fb->buf[(y+1*fb->width) + x+1] = fb->buf[(y+1*fb->width) + x+1] + quant_error * 1 / 16; + } + } +} \ No newline at end of file diff --git a/fap_source/camera/application.fam b/fap_source/camera/application.fam new file mode 100644 index 0000000..f16e263 --- /dev/null +++ b/fap_source/camera/application.fam @@ -0,0 +1,15 @@ +App( + appid="camera", + name="[ESP32] Camera", + apptype=FlipperAppType.EXTERNAL, + entry_point="camera_app", + cdefines=["APP_CAMERA"], + requires=["gui"], + stack_size=8*1024, + order=1, + fap_icon="icon.png", + fap_category="GPIO", + fap_description="ESP32-CAM live feed and photo capture", + fap_author="Z4urce", + fap_weburl="https://github.com/Z4urce/flipper-camera" +) \ No newline at end of file diff --git a/fap_source/camera/camera.c b/fap_source/camera/camera.c new file mode 100644 index 0000000..4d65be6 --- /dev/null +++ b/fap_source/camera/camera.c @@ -0,0 +1,284 @@ +#include "camera.h" + + +static void camera_view_draw_callback(Canvas* canvas, void* _model) { + UartDumpModel* model = _model; + + // Prepare canvas + //canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGTH); + + 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) { + canvas_draw_dot(canvas, (x * 8) + i, y); + } + } + } + + if (!model->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"); + canvas_draw_str(canvas, 20, 24, "VCC - 3V3"); + canvas_draw_str(canvas, 20, 34, "GND - GND"); + canvas_draw_str(canvas, 20, 44, "U0R - TX"); + canvas_draw_str(canvas, 20, 54, "U0T - RX"); + } +} + +void get_timefilename(FuriString* name) { + FuriHalRtcDateTime datetime = {0}; + furi_hal_rtc_get_datetime(&datetime); + furi_string_printf( + name, + EXT_PATH("DCIM/%.4d%.2d%.2d-%.2d%.2d%.2d.bmp"), + datetime.year, + datetime.month, + datetime.day, + datetime.hour, + datetime.minute, + datetime.second); +} + +static void save_image(void* context) { + UartEchoApp* app = context; + furi_assert(app); + + NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION); + + // We need a storage struct (gain accesso to the filesystem API ) + Storage* storage = furi_record_open(RECORD_STORAGE); + + // storage_file_alloc gives to us a File pointer using the Storage API. + File* file = storage_file_alloc(storage); + + if(storage_common_stat(storage, IMAGE_FILE_DIRECTORY_PATH, NULL) == FSE_NOT_EXIST) { + storage_simply_mkdir(storage, IMAGE_FILE_DIRECTORY_PATH); + } + + // create file name + FuriString* file_name = furi_string_alloc(); + get_timefilename(file_name); + + // this functions open a file, using write access and creates new file if not exist. + bool result = storage_file_open(file, furi_string_get_cstr(file_name), FSAM_WRITE, FSOM_OPEN_ALWAYS); + //bool result = storage_file_open(file, EXT_PATH("DCIM/test.bmp"), FSAM_WRITE, FSOM_OPEN_ALWAYS); + furi_string_free(file_name); + + if (result){ + storage_file_write(file, bitmap_header, BITMAP_HEADER_LENGTH); + with_view_model(app->view, UartDumpModel * model, { + int8_t row_buffer[ROW_BUFFER_LENGTH]; + for (size_t i = 64; i > 0; --i) { + for (size_t j = 0; j < ROW_BUFFER_LENGTH; ++j){ + row_buffer[j] = model->pixels[((i-1)*ROW_BUFFER_LENGTH) + j]; + } + storage_file_write(file, row_buffer, ROW_BUFFER_LENGTH); + } + + }, false); + } + + // Closing the "file descriptor" + storage_file_close(file); + + // Freeing up memory + storage_file_free(file); + + notification_message(notifications, result ? &sequence_success : &sequence_error); +} + +static bool camera_view_input_callback(InputEvent* event, void* context) { + if (event->type == InputTypePress){ + uint8_t data[1]; + if (event->key == InputKeyUp){ + data[0] = 'C'; + } + else if (event->key == InputKeyDown){ + data[0] = 'c'; + } + else if (event->key == InputKeyRight){ + data[0] = '>'; + } + else if (event->key == InputKeyLeft){ + data[0] = '<'; + } + else if (event->key == InputKeyOk){ + save_image(context); + } + furi_hal_uart_tx(FuriHalUartIdUSART1, data, 1); + } + + return false; +} + +static uint32_t camera_exit(void* context) { + UNUSED(context); + return VIEW_NONE; +} + +static void camera_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) { + furi_assert(context); + UartEchoApp* app = context; + + if(ev == UartIrqEventRXNE) { + furi_stream_buffer_send(app->rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(app->worker_thread), WorkerEventRx); + } +} + +static void process_ringbuffer(UartDumpModel* model, uint8_t byte) { + //// 1. Phase: filling the ringbuffer + if (model->ringbuffer_index == 0 && byte != 'Y'){ // First char has to be 'Y' in the buffer. + return; + } + + if (model->ringbuffer_index == 1 && byte != ':'){ // Second char has to be ':' in the buffer or reset. + model->ringbuffer_index = 0; + process_ringbuffer(model, byte); + return; + } + + model->row_ringbuffer[model->ringbuffer_index] = byte; // Assign current byte to the ringbuffer; + ++model->ringbuffer_index; // Increment the ringbuffer index + + if (model->ringbuffer_index < RING_BUFFER_LENGTH){ // Let's wait 'till the buffer fills. + return; + } + + //// 2. Phase: flushing the ringbuffer to the framebuffer + model->ringbuffer_index = 0; // Let's reset the ringbuffer + model->initialized = true; // We've successfully established the connection + size_t row_start_index = model->row_ringbuffer[2] * ROW_BUFFER_LENGTH; // Third char will determine the row number + + if (row_start_index > LAST_ROW_INDEX){ // Failsafe + row_start_index = 0; + } + + 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 + } +} + +static int32_t camera_worker(void* context) { + furi_assert(context); + UartEchoApp* app = context; + + while(1) { + uint32_t events = + furi_thread_flags_wait(WORKER_EVENTS_MASK, FuriFlagWaitAny, FuriWaitForever); + furi_check((events & FuriFlagError) == 0); + + if(events & WorkerEventStop) break; + if(events & WorkerEventRx) { + size_t length = 0; + do { + size_t intended_data_size = 64; + uint8_t data[intended_data_size]; + length = furi_stream_buffer_receive(app->rx_stream, data, intended_data_size, 0); + + if(length > 0) { + //furi_hal_uart_tx(FuriHalUartIdUSART1, data, length); + with_view_model( + app->view, + UartDumpModel * model, { + for(size_t i = 0; i < length; i++) { + process_ringbuffer(model, data[i]); + } + }, + false); + } + } while(length > 0); + + notification_message(app->notification, &sequence_notification); + with_view_model(app->view, UartDumpModel * model, { UNUSED(model); }, true); + } + } + + return 0; +} + +static UartEchoApp* camera_app_alloc() { + UartEchoApp* app = malloc(sizeof(UartEchoApp)); + + app->rx_stream = furi_stream_buffer_alloc(2048, 1); + + // Gui + app->gui = furi_record_open(RECORD_GUI); + app->notification = furi_record_open(RECORD_NOTIFICATION); + + // View dispatcher + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + // Views + app->view = view_alloc(); + view_set_context(app->view, app); + view_set_draw_callback(app->view, camera_view_draw_callback); + view_set_input_callback(app->view, camera_view_input_callback); + view_allocate_model(app->view, ViewModelTypeLocking, sizeof(UartDumpModel)); + + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < FRAME_BUFFER_LENGTH; i++) { + model->pixels[i] = 0; + } + }, + true); + + view_set_previous_callback(app->view, camera_exit); + view_dispatcher_add_view(app->view_dispatcher, 0, app->view); + view_dispatcher_switch_to_view(app->view_dispatcher, 0); + + app->worker_thread = furi_thread_alloc_ex("UsbUartWorker", 2048, camera_worker, app); + furi_thread_start(app->worker_thread); + + // Enable uart listener + furi_hal_console_disable(); + furi_hal_uart_set_br(FuriHalUartIdUSART1, 230400); + furi_hal_uart_set_irq_cb(FuriHalUartIdUSART1, camera_on_irq_cb, app); + + return app; +} + +static void camera_app_free(UartEchoApp* app) { + furi_assert(app); + + furi_hal_console_enable(); // this will also clear IRQ callback so thread is no longer referenced + + furi_thread_flags_set(furi_thread_get_id(app->worker_thread), WorkerEventStop); + furi_thread_join(app->worker_thread); + furi_thread_free(app->worker_thread); + + // Free views + view_dispatcher_remove_view(app->view_dispatcher, 0); + + view_free(app->view); + view_dispatcher_free(app->view_dispatcher); + + // Close gui record + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_NOTIFICATION); + app->gui = NULL; + + furi_stream_buffer_free(app->rx_stream); + + // Free rest + free(app); +} + +int32_t camera_app(void* p) { + UNUSED(p); + UartEchoApp* app = camera_app_alloc(); + view_dispatcher_run(app->view_dispatcher); + camera_app_free(app); + return 0; +} diff --git a/fap_source/camera/camera.h b/fap_source/camera/camera.h new file mode 100644 index 0000000..12747c6 --- /dev/null +++ b/fap_source/camera/camera.h @@ -0,0 +1,81 @@ +// TODO +// (DONE) Fix performance when not being charged +// (DONE) Add UART command parsing to Esp32 +// (DONE) Prepare application and icon on github +// (DONE) Write snapshots to SD card +// 5. Set a constant refresh rate to the Flipper's display +// 6. Emulate grayscale +// 7. Photo browser app + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define THREAD_ALLOC 2048 + +#define FRAME_WIDTH 128 +#define FRAME_HEIGTH 64 +#define FRAME_BIT_DEPTH 1 +#define FRAME_BUFFER_LENGTH (FRAME_WIDTH * FRAME_HEIGTH * FRAME_BIT_DEPTH / 8) // 128*64*1 / 8 = 1024 +#define ROW_BUFFER_LENGTH (FRAME_WIDTH / 8) // 128/8 = 16 +#define LAST_ROW_INDEX (FRAME_BUFFER_LENGTH - ROW_BUFFER_LENGTH) // 1024 - 16 = 1008 +#define RING_BUFFER_LENGTH (ROW_BUFFER_LENGTH + 3) // ROW_BUFFER_LENGTH + Header => 16 + 3 = 19 +#define BITMAP_HEADER_LENGTH 62 +#define IMAGE_FILE_DIRECTORY_PATH EXT_PATH("DCIM") + +static const unsigned char bitmap_header[BITMAP_HEADER_LENGTH] = { + 0x42, 0x4D, 0x3E, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0xFF, 0x00 +}; + +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,}; +const uint8_t* const _I_DolphinCommon_56x48[] = {_I_DolphinCommon_56x48_0}; +const Icon I_DolphinCommon_56x48 = {.width=56,.height=48,.frame_count=1,.frame_rate=0,.frames=_I_DolphinCommon_56x48}; + +typedef struct UartDumpModel UartDumpModel; + +typedef struct { + Gui* gui; + NotificationApp* notification; + ViewDispatcher* view_dispatcher; + View* view; + FuriThread* worker_thread; + FuriStreamBuffer* rx_stream; +} UartEchoApp; + +struct UartDumpModel { + uint8_t pixels[FRAME_BUFFER_LENGTH]; + + bool initialized; + + uint8_t row_ringbuffer[RING_BUFFER_LENGTH]; + uint8_t ringbuffer_index; +}; + +typedef enum { + WorkerEventReserved = (1 << 0), // Reserved for StreamBuffer internal event + WorkerEventStop = (1 << 1), + WorkerEventRx = (1 << 2), +} WorkerEventFlags; + +#define WORKER_EVENTS_MASK (WorkerEventStop | WorkerEventRx) + +const NotificationSequence sequence_notification = { + &message_display_backlight_on, + &message_delay_10, + NULL, +}; \ No newline at end of file diff --git a/fap_source/camera/icon.png b/fap_source/camera/icon.png new file mode 100644 index 0000000..75bbd02 Binary files /dev/null and b/fap_source/camera/icon.png differ