diff --git a/applications/external/esp32cam_camera/application.fam b/applications/external/esp32cam_camera/application.fam new file mode 100644 index 000000000..ef05b36ed --- /dev/null +++ b/applications/external/esp32cam_camera/application.fam @@ -0,0 +1,15 @@ +App( + appid="MAYHEM_Camera", + name="[MAYHEM] 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, use left/right for orientation/mode, up/down for brightness and center for saving a screenshot. [Unplug the USB cable to test with Mayhem]", + fap_author="Z4urce", + fap_weburl="https://github.com/Z4urce/flipper-camera" +) diff --git a/applications/external/esp32cam_camera/camera.c b/applications/external/esp32cam_camera/camera.c new file mode 100644 index 000000000..932fee1da --- /dev/null +++ b/applications/external/esp32cam_camera/camera.c @@ -0,0 +1,308 @@ +#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){ + + /*if(!model->marauderInitialized) + { + // Init marauder into stream mode + uint8_t data[] = "\nstream\n"; + furi_hal_uart_tx(FuriHalUartIdUSART1, data, sizeof(data)); + }*/ + + canvas_draw_icon(canvas, 74, 16, &I_DolphinCommon_56x48); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 8, 12, "Waiting ESP32-CAM..."); + canvas_draw_str(canvas, 20, 24, "VCC - 3V3/5V"); + 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); + + furi_hal_power_disable_external_3_3v(); + furi_hal_power_disable_otg(); + furi_delay_ms(200); + furi_hal_power_enable_external_3_3v(); + furi_hal_power_enable_otg(); + for(int i=0;i<2;i++) + { + furi_delay_ms(500); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[1]){'c'}, 1); + } + furi_delay_ms(1); + 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); + + furi_hal_power_disable_otg(); + + return 0; +} diff --git a/applications/external/esp32cam_camera/camera.h b/applications/external/esp32cam_camera/camera.h new file mode 100644 index 000000000..f4fef232e --- /dev/null +++ b/applications/external/esp32cam_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; + //bool marauderInitialized; + 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/applications/external/esp32cam_camera/icon.png b/applications/external/esp32cam_camera/icon.png new file mode 100644 index 000000000..75bbd0257 Binary files /dev/null and b/applications/external/esp32cam_camera/icon.png differ diff --git a/applications/external/esp32cam_marauder_companion/application.fam b/applications/external/esp32cam_marauder_companion/application.fam new file mode 100644 index 000000000..8450a5f78 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/application.fam @@ -0,0 +1,13 @@ +App( + appid="MAYHEM_Marauder", + name="[MAYHEM] Marauder", + apptype=FlipperAppType.EXTERNAL, + entry_point="wifi_marauder_app", + cdefines=["APP_WIFI_MARAUDER"], + requires=["gui"], + stack_size=4 * 1024, + order=2, + fap_icon="wifi_10px.png", + fap_category="GPIO", + fap_description="ESP32-CAM version of Marauder. Includes all functionality from the original plus some options to trigger the camera and flashlight. [Unplug the USB cable to test with Mayhem]" +) diff --git a/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene.c b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene.c new file mode 100644 index 000000000..b992cb10a --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene.c @@ -0,0 +1,30 @@ +#include "wifi_marauder_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const wifi_marauder_scene_on_enter_handlers[])(void*) = { +#include "wifi_marauder_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const wifi_marauder_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "wifi_marauder_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const wifi_marauder_scene_on_exit_handlers[])(void* context) = { +#include "wifi_marauder_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers wifi_marauder_scene_handlers = { + .on_enter_handlers = wifi_marauder_scene_on_enter_handlers, + .on_event_handlers = wifi_marauder_scene_on_event_handlers, + .on_exit_handlers = wifi_marauder_scene_on_exit_handlers, + .scene_num = WifiMarauderSceneNum, +}; diff --git a/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene.h b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene.h new file mode 100644 index 000000000..739e30d07 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) WifiMarauderScene##id, +typedef enum { +#include "wifi_marauder_scene_config.h" + WifiMarauderSceneNum, +} WifiMarauderScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers wifi_marauder_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "wifi_marauder_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "wifi_marauder_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "wifi_marauder_scene_config.h" +#undef ADD_SCENE diff --git a/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_config.h b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_config.h new file mode 100644 index 000000000..715897d17 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_config.h @@ -0,0 +1,5 @@ +ADD_SCENE(wifi_marauder, start, Start) +ADD_SCENE(wifi_marauder, console_output, ConsoleOutput) +ADD_SCENE(wifi_marauder, text_input, TextInput) +ADD_SCENE(wifi_marauder, settings_init, SettingsInit) +ADD_SCENE(wifi_marauder, log_viewer, LogViewer) diff --git a/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_console_output.c b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_console_output.c new file mode 100644 index 000000000..409bbb4ab --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_console_output.c @@ -0,0 +1,133 @@ +#include "../wifi_marauder_app_i.h" + +void wifi_marauder_console_output_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) { + furi_assert(context); + WifiMarauderApp* app = context; + + if(app->is_writing_log) { + app->has_saved_logs_this_session = true; + storage_file_write(app->log_file, buf, len); + } + + // If text box store gets too big, then truncate it + app->text_box_store_strlen += len; + if(app->text_box_store_strlen >= WIFI_MARAUDER_TEXT_BOX_STORE_SIZE - 1) { + furi_string_right(app->text_box_store, app->text_box_store_strlen / 2); + app->text_box_store_strlen = furi_string_size(app->text_box_store) + len; + } + + // Null-terminate buf and append to text box store + buf[len] = '\0'; + furi_string_cat_printf(app->text_box_store, "%s", buf); + view_dispatcher_send_custom_event(app->view_dispatcher, WifiMarauderEventRefreshConsoleOutput); +} + +void wifi_marauder_console_output_handle_rx_packets_cb(uint8_t* buf, size_t len, void* context) { + furi_assert(context); + WifiMarauderApp* app = context; + + if(app->is_writing_pcap) { + storage_file_write(app->capture_file, buf, len); + } +} + +void wifi_marauder_scene_console_output_on_enter(void* context) { + WifiMarauderApp* app = context; + + TextBox* text_box = app->text_box; + text_box_reset(app->text_box); + text_box_set_font(text_box, TextBoxFontText); + if(app->focus_console_start) { + text_box_set_focus(text_box, TextBoxFocusStart); + } else { + text_box_set_focus(text_box, TextBoxFocusEnd); + } + if(app->is_command) { + furi_string_reset(app->text_box_store); + app->text_box_store_strlen = 0; + if(0 == strncmp("help", app->selected_tx_string, strlen("help"))) { + const char* help_msg = "Marauder companion " WIFI_MARAUDER_APP_VERSION "\n"; + furi_string_cat_str(app->text_box_store, help_msg); + app->text_box_store_strlen += strlen(help_msg); + } + + if(app->show_stopscan_tip) { + const char* help_msg = "Press BACK to send stopscan\n"; + furi_string_cat_str(app->text_box_store, help_msg); + app->text_box_store_strlen += strlen(help_msg); + } + } + + // Set starting text - for "View Log from end", this will just be what was already in the text box store + text_box_set_text(app->text_box, furi_string_get_cstr(app->text_box_store)); + + scene_manager_set_scene_state(app->scene_manager, WifiMarauderSceneConsoleOutput, 0); + view_dispatcher_switch_to_view(app->view_dispatcher, WifiMarauderAppViewConsoleOutput); + + // Register callback to receive data + wifi_marauder_uart_set_handle_rx_data_cb( + app->uart, + wifi_marauder_console_output_handle_rx_data_cb); // setup callback for general log rx thread + wifi_marauder_uart_set_handle_rx_data_cb( + app->lp_uart, + wifi_marauder_console_output_handle_rx_packets_cb); // setup callback for packets rx thread + + // Get ready to send command + if(app->is_command && app->selected_tx_string) { + // Create files *before* sending command + // (it takes time to iterate through the directory) + if(app->ok_to_save_logs) { + app->is_writing_log = true; + wifi_marauder_create_log_file(app); + } + + // If it is a sniff function, open the pcap file for recording + if(app->ok_to_save_pcaps && strncmp("sniff", app->selected_tx_string, strlen("sniff")) == 0) { + app->is_writing_pcap = true; + wifi_marauder_create_pcap_file(app); + } + + // Send command with newline '\n' + wifi_marauder_uart_tx( + (uint8_t*)(app->selected_tx_string), strlen(app->selected_tx_string)); + wifi_marauder_uart_tx((uint8_t*)("\n"), 1); + } +} + +bool wifi_marauder_scene_console_output_on_event(void* context, SceneManagerEvent event) { + WifiMarauderApp* app = context; + + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + text_box_set_text(app->text_box, furi_string_get_cstr(app->text_box_store)); + consumed = true; + } else if(event.type == SceneManagerEventTypeTick) { + consumed = true; + } + + return consumed; +} + +void wifi_marauder_scene_console_output_on_exit(void* context) { + WifiMarauderApp* app = context; + + // Unregister rx callback + wifi_marauder_uart_set_handle_rx_data_cb(app->uart, NULL); + wifi_marauder_uart_set_handle_rx_data_cb(app->lp_uart, NULL); + + // Automatically stop the scan when exiting view + if(app->is_command) { + wifi_marauder_uart_tx((uint8_t*)("stopscan\n"), strlen("stopscan\n")); + } + + app->is_writing_pcap = false; + if(app->capture_file && storage_file_is_open(app->capture_file)) { + storage_file_close(app->capture_file); + } + + app->is_writing_log = false; + if(app->log_file && storage_file_is_open(app->log_file)) { + storage_file_close(app->log_file); + } +} \ No newline at end of file diff --git a/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_log_viewer.c b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_log_viewer.c new file mode 100644 index 000000000..7e6c23113 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_log_viewer.c @@ -0,0 +1,180 @@ +#include "../wifi_marauder_app_i.h" + +void wifi_marauder_scene_log_viewer_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + WifiMarauderApp* app = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(app->view_dispatcher, result); + } +} + +static void _read_log_page_into_text_store(WifiMarauderApp* app) { + char temp[64 + 1]; + storage_file_seek( + app->log_file, WIFI_MARAUDER_TEXT_BOX_STORE_SIZE * (app->open_log_file_page - 1), true); + furi_string_reset(app->text_box_store); + for(uint16_t i = 0; i < (WIFI_MARAUDER_TEXT_BOX_STORE_SIZE / (sizeof(temp) - 1)); i++) { + uint16_t num_bytes = storage_file_read(app->log_file, temp, sizeof(temp) - 1); + if(num_bytes == 0) { + break; + } + temp[num_bytes] = '\0'; + furi_string_cat_str(app->text_box_store, temp); + } +} + +void wifi_marauder_scene_log_viewer_setup_widget(WifiMarauderApp* app, bool called_from_browse) { + Widget* widget = app->widget; + bool is_open = storage_file_is_open(app->log_file); + bool should_open_log = (app->has_saved_logs_this_session || called_from_browse); + if(is_open) { + _read_log_page_into_text_store(app); + } else if( + should_open_log && + storage_file_open(app->log_file, app->log_file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + uint64_t filesize = storage_file_size(app->log_file); + app->open_log_file_num_pages = filesize / WIFI_MARAUDER_TEXT_BOX_STORE_SIZE; + int extra_page = (filesize % WIFI_MARAUDER_TEXT_BOX_STORE_SIZE != 0) ? 1 : 0; + app->open_log_file_num_pages = (filesize / WIFI_MARAUDER_TEXT_BOX_STORE_SIZE) + extra_page; + app->open_log_file_page = 1; + _read_log_page_into_text_store(app); + } else { + app->open_log_file_page = 0; + app->open_log_file_num_pages = 0; + } + + widget_reset(widget); + + if(furi_string_empty(app->text_box_store)) { + char help_msg[256]; + snprintf( + help_msg, + sizeof(help_msg), + "The log is empty! :(\nTry sending a command?\n\nSaving pcaps to flipper sdcard: %s\nSaving logs to flipper sdcard: %s", + app->ok_to_save_pcaps ? "ON" : "OFF", + app->ok_to_save_logs ? "ON" : "OFF"); + furi_string_set_str(app->text_box_store, help_msg); + } + + widget_add_text_scroll_element( + widget, 0, 0, 128, 53, furi_string_get_cstr(app->text_box_store)); + + if(1 < app->open_log_file_page && app->open_log_file_page < app->open_log_file_num_pages) { + // hide "Browse" text for middle pages + widget_add_button_element( + widget, GuiButtonTypeCenter, "", wifi_marauder_scene_log_viewer_widget_callback, app); + } else { + // only show "Browse" text on first and last page + widget_add_button_element( + widget, + GuiButtonTypeCenter, + "Browse", + wifi_marauder_scene_log_viewer_widget_callback, + app); + } + + char pagecounter[100]; + snprintf( + pagecounter, + sizeof(pagecounter), + "%d/%d", + app->open_log_file_page, + app->open_log_file_num_pages); + if(app->open_log_file_page > 1) { + if(app->open_log_file_page == app->open_log_file_num_pages) { + // only show left side page-count on last page + widget_add_button_element( + widget, + GuiButtonTypeLeft, + pagecounter, + wifi_marauder_scene_log_viewer_widget_callback, + app); + } else { + widget_add_button_element( + widget, GuiButtonTypeLeft, "", wifi_marauder_scene_log_viewer_widget_callback, app); + } + } + if(app->open_log_file_page < app->open_log_file_num_pages) { + widget_add_button_element( + widget, + GuiButtonTypeRight, + pagecounter, + wifi_marauder_scene_log_viewer_widget_callback, + app); + } +} + +void wifi_marauder_scene_log_viewer_on_enter(void* context) { + WifiMarauderApp* app = context; + + app->open_log_file_page = 0; + app->open_log_file_num_pages = 0; + bool saved_logs_exist = false; + if (!app->has_saved_logs_this_session && furi_string_empty(app->text_box_store)) { + // no commands sent yet this session, find last saved log + if (storage_dir_open(app->log_file, MARAUDER_APP_FOLDER_LOGS)) { + char name[70]; + char lastname[70]; + while (storage_dir_read(app->log_file, NULL, name, sizeof(name))) { + // keep reading directory until last file is reached + strlcpy(lastname, name, sizeof(lastname)); + saved_logs_exist = true; + } + if (saved_logs_exist) { + snprintf(app->log_file_path, sizeof(app->log_file_path), "%s/%s", MARAUDER_APP_FOLDER_LOGS, lastname); + } + } + storage_dir_close(app->log_file); + } + + wifi_marauder_scene_log_viewer_setup_widget(app, saved_logs_exist); + + view_dispatcher_switch_to_view(app->view_dispatcher, WifiMarauderAppViewWidget); +} + +bool wifi_marauder_scene_log_viewer_on_event(void* context, SceneManagerEvent event) { + WifiMarauderApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == GuiButtonTypeCenter) { + // Browse + FuriString* predefined_filepath = furi_string_alloc_set_str(MARAUDER_APP_FOLDER_LOGS); + FuriString* selected_filepath = furi_string_alloc(); + if(dialog_file_browser_show( + app->dialogs, selected_filepath, predefined_filepath, NULL)) { + strncpy( + app->log_file_path, + furi_string_get_cstr(selected_filepath), + sizeof(app->log_file_path)); + if(storage_file_is_open(app->log_file)) { + storage_file_close(app->log_file); + } + wifi_marauder_scene_log_viewer_setup_widget(app, true); + } + furi_string_free(selected_filepath); + furi_string_free(predefined_filepath); + consumed = true; + } else if(event.event == GuiButtonTypeRight) { + // Advance page + ++app->open_log_file_page; + wifi_marauder_scene_log_viewer_setup_widget(app, false); + } else if(event.event == GuiButtonTypeLeft) { + // Previous page + --app->open_log_file_page; + wifi_marauder_scene_log_viewer_setup_widget(app, false); + } + } + + return consumed; +} + +void wifi_marauder_scene_log_viewer_on_exit(void* context) { + WifiMarauderApp* app = context; + widget_reset(app->widget); + if(storage_file_is_open(app->log_file)) { + storage_file_close(app->log_file); + } +} diff --git a/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_settings_init.c b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_settings_init.c new file mode 100644 index 000000000..04d099d12 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_settings_init.c @@ -0,0 +1,130 @@ +#include "../wifi_marauder_app_i.h" + +const char* Y = "Y"; +const char* N = "N"; + +#define PROMPT_PCAPS 0 +#define PROMPT_LOGS 1 + +void wifi_marauder_scene_settings_init_widget_callback( + GuiButtonType result, + InputType type, + void* context) { + WifiMarauderApp* app = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(app->view_dispatcher, result); + } +} + +void wifi_marauder_scene_settings_init_setup_widget(WifiMarauderApp* app) { + Widget* widget = app->widget; + + widget_reset(widget); + + widget_add_button_element( + widget, GuiButtonTypeLeft, "No", wifi_marauder_scene_settings_init_widget_callback, app); + widget_add_button_element( + widget, GuiButtonTypeRight, "Yes", wifi_marauder_scene_settings_init_widget_callback, app); + + if(app->which_prompt == PROMPT_PCAPS) { + widget_add_string_element(widget, 0, 0, AlignLeft, AlignTop, FontPrimary, "Save pcaps?"); + widget_add_text_scroll_element( + widget, + 0, + 12, + 128, + 38, + "With compatible marauder\nfirmware, you can choose to\nsave captures (pcaps) to the\nflipper sd card here:\n" MARAUDER_APP_FOLDER_USER_PCAPS + "\n\nYou can change this setting in the app at any time. Would\nyou like to enable this feature now?"); + } else { + widget_add_string_element(widget, 0, 0, AlignLeft, AlignTop, FontPrimary, "Save logs?"); + widget_add_text_scroll_element( + widget, + 0, + 12, + 128, + 38, + "This app supports saving text\nlogs of console output to the\nflipper sd card here:\n" MARAUDER_APP_FOLDER_USER_LOGS + "\n\nYou can change this setting in the app at any time. Would\nyou like to enable this feature now?"); + } +} + +void wifi_marauder_scene_settings_init_on_enter(void* context) { + WifiMarauderApp* app = context; + + app->which_prompt = PROMPT_PCAPS; + wifi_marauder_scene_settings_init_setup_widget(app); + + view_dispatcher_switch_to_view(app->view_dispatcher, WifiMarauderAppViewWidget); +} + +bool wifi_marauder_scene_settings_init_on_event(void* context, SceneManagerEvent event) { + WifiMarauderApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + // get which button press: "Yes" or "No" + if(event.event == GuiButtonTypeRight) { + // Yes + if(app->which_prompt == PROMPT_PCAPS) { + app->ok_to_save_pcaps = true; + } else { + app->ok_to_save_logs = true; + } + } else if(event.event == GuiButtonTypeLeft) { + // No + if(app->which_prompt == PROMPT_PCAPS) { + app->ok_to_save_pcaps = false; + } else { + app->ok_to_save_logs = false; + } + } + + // save setting to file, load next widget or scene + if(app->which_prompt == PROMPT_PCAPS) { + if(storage_file_open( + app->save_pcap_setting_file, + SAVE_PCAP_SETTING_FILEPATH, + FSAM_WRITE, + FSOM_CREATE_ALWAYS)) { + const char* ok = app->ok_to_save_pcaps ? Y : N; + storage_file_write(app->save_pcap_setting_file, ok, sizeof(ok)); + } else { + dialog_message_show_storage_error(app->dialogs, "Cannot save settings"); + } + storage_file_close(app->save_pcap_setting_file); + // same scene, different-looking widget + app->which_prompt = PROMPT_LOGS; + wifi_marauder_scene_settings_init_setup_widget(app); + } else { + if(storage_file_open( + app->save_logs_setting_file, + SAVE_LOGS_SETTING_FILEPATH, + FSAM_WRITE, + FSOM_CREATE_ALWAYS)) { + const char* ok = app->ok_to_save_logs ? Y : N; + storage_file_write(app->save_logs_setting_file, ok, sizeof(ok)); + } else { + dialog_message_show_storage_error(app->dialogs, "Cannot save settings"); + } + storage_file_close(app->save_logs_setting_file); + // go back to start scene (main menu) + app->need_to_prompt_settings_init = false; + scene_manager_previous_scene(app->scene_manager); + } + consumed = true; + } + + return consumed; +} + +void wifi_marauder_scene_settings_init_on_exit(void* context) { + WifiMarauderApp* app = context; + widget_reset(app->widget); + if(storage_file_is_open(app->save_pcap_setting_file)) { + storage_file_close(app->save_pcap_setting_file); + } + if(storage_file_is_open(app->save_logs_setting_file)) { + storage_file_close(app->save_logs_setting_file); + } +} diff --git a/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_start.c b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_start.c new file mode 100644 index 000000000..193471958 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_start.c @@ -0,0 +1,269 @@ +//** Includes sniffbt and sniffskim for compatible ESP32-WROOM hardware. +//wifi_marauder_app_i.h also changed **// +#include "../wifi_marauder_app_i.h" + +// For each command, define whether additional arguments are needed +// (enabling text input to fill them out), and whether the console +// text box should focus at the start of the output or the end +typedef enum { NO_ARGS = 0, INPUT_ARGS, TOGGLE_ARGS } InputArgs; + +typedef enum { FOCUS_CONSOLE_END = 0, FOCUS_CONSOLE_START, FOCUS_CONSOLE_TOGGLE } FocusConsole; + +#define SHOW_STOPSCAN_TIP (true) +#define NO_TIP (false) + +#define MAX_OPTIONS (9) +typedef struct { + const char* item_string; + const char* options_menu[MAX_OPTIONS]; + int num_options_menu; + const char* actual_commands[MAX_OPTIONS]; + InputArgs needs_keyboard; + FocusConsole focus_console; + bool show_stopscan_tip; +} WifiMarauderItem; + +// NUM_MENU_ITEMS defined in wifi_marauder_app_i.h - if you add an entry here, increment it! +const WifiMarauderItem items[NUM_MENU_ITEMS] = { + {"View Log from", {"start", "end"}, 2, {"", ""}, NO_ARGS, FOCUS_CONSOLE_TOGGLE, NO_TIP}, + {"Scan", + {"ap", "station"}, + 2, + {"scanap", "scansta"}, + NO_ARGS, + FOCUS_CONSOLE_END, + SHOW_STOPSCAN_TIP}, + {"SSID", + {"add rand", "add name", "remove"}, + 3, + {"ssid -a -g", "ssid -a -n", "ssid -r"}, + INPUT_ARGS, + FOCUS_CONSOLE_START, + NO_TIP}, + {"List", + {"ap", "ssid", "station"}, + 3, + {"list -a", "list -s", "list -c"}, + NO_ARGS, + FOCUS_CONSOLE_START, + NO_TIP}, + {"Select", + {"ap", "ssid", "station"}, + 3, + {"select -a", "select -s", "select -c"}, + INPUT_ARGS, + FOCUS_CONSOLE_END, + NO_TIP}, + {"Clear List", + {"ap", "ssid", "station"}, + 3, + {"clearlist -a", "clearlist -s", "clearlist -c"}, + NO_ARGS, + FOCUS_CONSOLE_END, + NO_TIP}, + {"Attack", + {"deauth", "probe", "rickroll"}, + 3, + {"attack -t deauth", "attack -t probe", "attack -t rickroll"}, + NO_ARGS, + FOCUS_CONSOLE_END, + SHOW_STOPSCAN_TIP}, + {"Targeted Deauth", + {"station", "manual"}, + 2, + {"attack -t deauth -c", "attack -t deauth -s"}, + TOGGLE_ARGS, + FOCUS_CONSOLE_END, + SHOW_STOPSCAN_TIP}, + {"Beacon Spam", + {"ap list", "ssid list", "random"}, + 3, + {"attack -t beacon -a", "attack -t beacon -l", "attack -t beacon -r"}, + NO_ARGS, + FOCUS_CONSOLE_END, + SHOW_STOPSCAN_TIP}, + {"Sniff", + {"beacon", "deauth", "esp", "pmkid", "probe", "pwn", "raw", "bt", "skim"}, + 9, + {"sniffbeacon", + "sniffdeauth", + "sniffesp", + "sniffpmkid", + "sniffprobe", + "sniffpwn", + "sniffraw", + "sniffbt", + "sniffskim"}, + NO_ARGS, + FOCUS_CONSOLE_END, + SHOW_STOPSCAN_TIP}, + {"Sniff PMKID on channel", + {""}, + 1, + {"sniffpmkid -c"}, + INPUT_ARGS, + FOCUS_CONSOLE_END, + SHOW_STOPSCAN_TIP}, + {"Channel", + {"get", "set"}, + 2, + {"channel", "channel -s"}, + TOGGLE_ARGS, + FOCUS_CONSOLE_END, + NO_TIP}, + {"Camera", + {"photo", "flashlight"}, + 2, + {"photo", "flashlight"}, + NO_ARGS, + FOCUS_CONSOLE_END, + NO_TIP}, + {"Settings", + {"display", "restore", "ForcePMKID", "ForceProbe", "SavePCAP", "EnableLED", "other"}, + 7, + {"settings", + "settings -r", + "settings -s ForcePMKID enable", + "settings -s ForceProbe enable", + "settings -s SavePCAP enable", + "settings -s EnableLED enable", + "settings -s"}, + TOGGLE_ARGS, + FOCUS_CONSOLE_START, + NO_TIP}, + {"Update", {"ota", "sd"}, 2, {"update -w", "update -s"}, NO_ARGS, FOCUS_CONSOLE_END, NO_TIP}, + {"Reboot", {""}, 1, {"reboot"}, NO_ARGS, FOCUS_CONSOLE_END, NO_TIP}, + {"Help", {""}, 1, {"help"}, NO_ARGS, FOCUS_CONSOLE_START, SHOW_STOPSCAN_TIP}, + {"Save to flipper sdcard", // keep as last entry or change logic in callback below + {""}, + 1, + {""}, + NO_ARGS, + FOCUS_CONSOLE_START, + NO_TIP}, +}; + +static void wifi_marauder_scene_start_var_list_enter_callback(void* context, uint32_t index) { + furi_assert(context); + WifiMarauderApp* app = context; + + furi_assert(index < NUM_MENU_ITEMS); + const WifiMarauderItem* item = &items[index]; + + if(index == NUM_MENU_ITEMS - 1) { + // "Save to flipper sdcard" special case - start SettingsInit widget + view_dispatcher_send_custom_event( + app->view_dispatcher, WifiMarauderEventStartSettingsInit); + return; + } + + const int selected_option_index = app->selected_option_index[index]; + furi_assert(selected_option_index < item->num_options_menu); + app->selected_tx_string = item->actual_commands[selected_option_index]; + app->is_command = (1 <= index); + app->is_custom_tx_string = false; + app->selected_menu_index = index; + app->focus_console_start = (item->focus_console == FOCUS_CONSOLE_TOGGLE) ? + (selected_option_index == 0) : + item->focus_console; + app->show_stopscan_tip = item->show_stopscan_tip; + + if(!app->is_command && selected_option_index == 0) { + // View Log from start + view_dispatcher_send_custom_event(app->view_dispatcher, WifiMarauderEventStartLogViewer); + return; + } + + bool needs_keyboard = (item->needs_keyboard == TOGGLE_ARGS) ? (selected_option_index != 0) : + item->needs_keyboard; + if(needs_keyboard) { + view_dispatcher_send_custom_event(app->view_dispatcher, WifiMarauderEventStartKeyboard); + } else { + view_dispatcher_send_custom_event(app->view_dispatcher, WifiMarauderEventStartConsole); + } +} + +static void wifi_marauder_scene_start_var_list_change_callback(VariableItem* item) { + furi_assert(item); + + WifiMarauderApp* app = variable_item_get_context(item); + furi_assert(app); + + const WifiMarauderItem* menu_item = &items[app->selected_menu_index]; + uint8_t item_index = variable_item_get_current_value_index(item); + furi_assert(item_index < menu_item->num_options_menu); + variable_item_set_current_value_text(item, menu_item->options_menu[item_index]); + app->selected_option_index[app->selected_menu_index] = item_index; +} + +void wifi_marauder_scene_start_on_enter(void* context) { + WifiMarauderApp* app = context; + VariableItemList* var_item_list = app->var_item_list; + + variable_item_list_set_enter_callback( + var_item_list, wifi_marauder_scene_start_var_list_enter_callback, app); + + VariableItem* item; + for(int i = 0; i < NUM_MENU_ITEMS; ++i) { + item = variable_item_list_add( + var_item_list, + items[i].item_string, + items[i].num_options_menu, + wifi_marauder_scene_start_var_list_change_callback, + app); + variable_item_set_current_value_index(item, app->selected_option_index[i]); + variable_item_set_current_value_text( + item, items[i].options_menu[app->selected_option_index[i]]); + } + + variable_item_list_set_selected_item( + var_item_list, scene_manager_get_scene_state(app->scene_manager, WifiMarauderSceneStart)); + + view_dispatcher_switch_to_view(app->view_dispatcher, WifiMarauderAppViewVarItemList); + + // Wait, if the user hasn't initialized sdcard settings, let's prompt them once (then come back here) + if(app->need_to_prompt_settings_init) { + scene_manager_next_scene(app->scene_manager, WifiMarauderSceneSettingsInit); + } +} + +bool wifi_marauder_scene_start_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + WifiMarauderApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == WifiMarauderEventStartKeyboard) { + scene_manager_set_scene_state( + app->scene_manager, WifiMarauderSceneStart, app->selected_menu_index); + scene_manager_next_scene(app->scene_manager, WifiMarauderSceneTextInput); + } else if(event.event == WifiMarauderEventStartConsole) { + scene_manager_set_scene_state( + app->scene_manager, WifiMarauderSceneStart, app->selected_menu_index); + scene_manager_next_scene(app->scene_manager, WifiMarauderSceneConsoleOutput); + } else if(event.event == WifiMarauderEventStartSettingsInit) { + scene_manager_set_scene_state( + app->scene_manager, WifiMarauderSceneStart, app->selected_menu_index); + scene_manager_next_scene(app->scene_manager, WifiMarauderSceneSettingsInit); + } else if(event.event == WifiMarauderEventStartLogViewer) { + scene_manager_set_scene_state( + app->scene_manager, WifiMarauderSceneStart, app->selected_menu_index); + scene_manager_next_scene(app->scene_manager, WifiMarauderSceneLogViewer); + } + consumed = true; + } else if(event.type == SceneManagerEventTypeTick) { + app->selected_menu_index = variable_item_list_get_selected_item_index(app->var_item_list); + consumed = true; + } else if(event.type == SceneManagerEventTypeBack) { + scene_manager_stop(app->scene_manager); + view_dispatcher_stop(app->view_dispatcher); + consumed = true; + } + + return consumed; +} + +void wifi_marauder_scene_start_on_exit(void* context) { + WifiMarauderApp* app = context; + variable_item_list_reset(app->var_item_list); +} diff --git a/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_text_input.c b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_text_input.c new file mode 100644 index 000000000..b721e868d --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/scenes/wifi_marauder_scene_text_input.c @@ -0,0 +1,154 @@ +#include "../wifi_marauder_app_i.h" + +void wifi_marauder_scene_text_input_callback(void* context) { + WifiMarauderApp* app = context; + + switch(app->special_case_input_step) { + case 0: // most commands + view_dispatcher_send_custom_event(app->view_dispatcher, WifiMarauderEventStartConsole); + break; + case 1: // special case for deauth: save source MAC + view_dispatcher_send_custom_event(app->view_dispatcher, WifiMarauderEventSaveSourceMac); + break; + case 2: // special case for deauth: save destination MAC + view_dispatcher_send_custom_event( + app->view_dispatcher, WifiMarauderEventSaveDestinationMac); + break; + default: + break; + } +} + +void wifi_marauder_scene_text_input_on_enter(void* context) { + WifiMarauderApp* app = context; + + if(0 == + strncmp("attack -t deauth -s", app->selected_tx_string, strlen("attack -t deauth -s"))) { + // Special case for manual deauth input + app->special_case_input_step = 1; + bzero(app->text_input_store, WIFI_MARAUDER_TEXT_INPUT_STORE_SIZE); + } else if(false == app->is_custom_tx_string) { + // Most commands + app->special_case_input_step = 0; + + // Fill text input with selected string so that user can add to it + size_t length = strlen(app->selected_tx_string); + furi_assert(length < WIFI_MARAUDER_TEXT_INPUT_STORE_SIZE); + bzero(app->text_input_store, WIFI_MARAUDER_TEXT_INPUT_STORE_SIZE); + strncpy(app->text_input_store, app->selected_tx_string, length); + + // Add space - because flipper keyboard currently doesn't have a space + app->text_input_store[length] = ' '; + app->text_input_store[length + 1] = '\0'; + app->is_custom_tx_string = true; + } + + // Setup view + TextInput* text_input = app->text_input; + // Add help message to header + if(app->special_case_input_step == 1) { + text_input_set_header_text(text_input, "Enter source MAC"); + } else if(0 == strncmp("ssid -a -g", app->selected_tx_string, strlen("ssid -a -g"))) { + text_input_set_header_text(text_input, "Enter # SSIDs to generate"); + } else if(0 == strncmp("ssid -a -n", app->selected_tx_string, strlen("ssid -a -n"))) { + text_input_set_header_text(text_input, "Enter SSID name to add"); + } else if(0 == strncmp("ssid -r", app->selected_tx_string, strlen("ssid -r"))) { + text_input_set_header_text(text_input, "Remove target from SSID list"); + } else if(0 == strncmp("select -a", app->selected_tx_string, strlen("select -a"))) { + text_input_set_header_text(text_input, "Add target from AP list"); + } else if(0 == strncmp("select -s", app->selected_tx_string, strlen("select -s"))) { + text_input_set_header_text(text_input, "Add target from SSID list"); + } else { + text_input_set_header_text(text_input, "Add command arguments"); + } + text_input_set_result_callback( + text_input, + wifi_marauder_scene_text_input_callback, + app, + app->text_input_store, + WIFI_MARAUDER_TEXT_INPUT_STORE_SIZE, + false); + + view_dispatcher_switch_to_view(app->view_dispatcher, WifiMarauderAppViewTextInput); +} + +bool wifi_marauder_scene_text_input_on_event(void* context, SceneManagerEvent event) { + WifiMarauderApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == WifiMarauderEventStartConsole) { + // Point to custom string to send + app->selected_tx_string = app->text_input_store; + scene_manager_next_scene(app->scene_manager, WifiMarauderSceneConsoleOutput); + consumed = true; + } else if(event.event == WifiMarauderEventSaveSourceMac) { + if(12 != strlen(app->text_input_store)) { + text_input_set_header_text(app->text_input, "MAC must be 12 hex chars!"); + } else { + snprintf( + app->special_case_input_src_addr, + sizeof(app->special_case_input_src_addr), + "%c%c:%c%c:%c%c:%c%c:%c%c:%c%c", + app->text_input_store[0], + app->text_input_store[1], + app->text_input_store[2], + app->text_input_store[3], + app->text_input_store[4], + app->text_input_store[5], + app->text_input_store[6], + app->text_input_store[7], + app->text_input_store[8], + app->text_input_store[9], + app->text_input_store[10], + app->text_input_store[11]); + + // Advance scene to input destination MAC, clear text input + app->special_case_input_step = 2; + bzero(app->text_input_store, WIFI_MARAUDER_TEXT_INPUT_STORE_SIZE); + text_input_set_header_text(app->text_input, "Enter destination MAC"); + } + consumed = true; + } else if(event.event == WifiMarauderEventSaveDestinationMac) { + if(12 != strlen(app->text_input_store)) { + text_input_set_header_text(app->text_input, "MAC must be 12 hex chars!"); + } else { + snprintf( + app->special_case_input_dst_addr, + sizeof(app->special_case_input_dst_addr), + "%c%c:%c%c:%c%c:%c%c:%c%c:%c%c", + app->text_input_store[0], + app->text_input_store[1], + app->text_input_store[2], + app->text_input_store[3], + app->text_input_store[4], + app->text_input_store[5], + app->text_input_store[6], + app->text_input_store[7], + app->text_input_store[8], + app->text_input_store[9], + app->text_input_store[10], + app->text_input_store[11]); + + // Construct command with source and destination MACs + snprintf( + app->text_input_store, + WIFI_MARAUDER_TEXT_INPUT_STORE_SIZE, + "attack -t deauth -s %18s -d %18s", + app->special_case_input_src_addr, + app->special_case_input_dst_addr); + app->selected_tx_string = app->text_input_store; + scene_manager_next_scene(app->scene_manager, WifiMarauderSceneConsoleOutput); + } + consumed = true; + } + } + + return consumed; +} + +void wifi_marauder_scene_text_input_on_exit(void* context) { + WifiMarauderApp* app = context; + + text_input_reset(app->text_input); +} diff --git a/applications/external/esp32cam_marauder_companion/wifi_10px.png b/applications/external/esp32cam_marauder_companion/wifi_10px.png new file mode 100644 index 000000000..c13534660 Binary files /dev/null and b/applications/external/esp32cam_marauder_companion/wifi_10px.png differ diff --git a/applications/external/esp32cam_marauder_companion/wifi_marauder_app.c b/applications/external/esp32cam_marauder_companion/wifi_marauder_app.c new file mode 100644 index 000000000..39d13b0f5 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/wifi_marauder_app.c @@ -0,0 +1,188 @@ +#include "wifi_marauder_app_i.h" + +#include +#include + +static bool wifi_marauder_app_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + WifiMarauderApp* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool wifi_marauder_app_back_event_callback(void* context) { + furi_assert(context); + WifiMarauderApp* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static void wifi_marauder_app_tick_event_callback(void* context) { + furi_assert(context); + WifiMarauderApp* app = context; + scene_manager_handle_tick_event(app->scene_manager); +} + +WifiMarauderApp* wifi_marauder_app_alloc() { + WifiMarauderApp* app = malloc(sizeof(WifiMarauderApp)); + + app->gui = furi_record_open(RECORD_GUI); + app->dialogs = furi_record_open(RECORD_DIALOGS); + app->storage = furi_record_open(RECORD_STORAGE); + app->capture_file = storage_file_alloc(app->storage); + app->log_file = storage_file_alloc(app->storage); + app->save_pcap_setting_file = storage_file_alloc(app->storage); + app->save_logs_setting_file = storage_file_alloc(app->storage); + + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&wifi_marauder_scene_handlers, app); + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, wifi_marauder_app_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, wifi_marauder_app_back_event_callback); + view_dispatcher_set_tick_event_callback( + app->view_dispatcher, wifi_marauder_app_tick_event_callback, 100); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + app->var_item_list = variable_item_list_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + WifiMarauderAppViewVarItemList, + variable_item_list_get_view(app->var_item_list)); + + for(int i = 0; i < NUM_MENU_ITEMS; ++i) { + app->selected_option_index[i] = 0; + } + + app->special_case_input_step = 0; + + app->text_box = text_box_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, WifiMarauderAppViewConsoleOutput, text_box_get_view(app->text_box)); + app->text_box_store = furi_string_alloc(); + furi_string_reserve(app->text_box_store, WIFI_MARAUDER_TEXT_BOX_STORE_SIZE); + + app->text_input = text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, WifiMarauderAppViewTextInput, text_input_get_view(app->text_input)); + + app->widget = widget_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, WifiMarauderAppViewWidget, widget_get_view(app->widget)); + + app->has_saved_logs_this_session = false; + + // if user hasn't confirmed whether to save pcaps and logs to sdcard, then prompt when scene starts + app->need_to_prompt_settings_init = + (!storage_file_exists(app->storage, SAVE_PCAP_SETTING_FILEPATH) || + !storage_file_exists(app->storage, SAVE_LOGS_SETTING_FILEPATH)); + + scene_manager_next_scene(app->scene_manager, WifiMarauderSceneStart); + + return app; +} + +void wifi_marauder_make_app_folder(WifiMarauderApp* app) { + furi_assert(app); + + if(!storage_simply_mkdir(app->storage, MARAUDER_APP_FOLDER)) { + dialog_message_show_storage_error(app->dialogs, "Cannot create\napp folder"); + } + + if(!storage_simply_mkdir(app->storage, MARAUDER_APP_FOLDER_PCAPS)) { + dialog_message_show_storage_error(app->dialogs, "Cannot create\npcaps folder"); + } + + if(!storage_simply_mkdir(app->storage, MARAUDER_APP_FOLDER_LOGS)) { + dialog_message_show_storage_error(app->dialogs, "Cannot create\npcaps folder"); + } +} + +void wifi_marauder_load_settings(WifiMarauderApp* app) { + if(storage_file_open( + app->save_pcap_setting_file, + SAVE_PCAP_SETTING_FILEPATH, + FSAM_READ, + FSOM_OPEN_EXISTING)) { + char ok[1]; + storage_file_read(app->save_pcap_setting_file, ok, sizeof(ok)); + app->ok_to_save_pcaps = ok[0] == 'Y'; + } + storage_file_close(app->save_pcap_setting_file); + + if(storage_file_open( + app->save_logs_setting_file, + SAVE_LOGS_SETTING_FILEPATH, + FSAM_READ, + FSOM_OPEN_EXISTING)) { + char ok[1]; + storage_file_read(app->save_logs_setting_file, ok, sizeof(ok)); + app->ok_to_save_logs = ok[0] == 'Y'; + } + storage_file_close(app->save_logs_setting_file); +} + +void wifi_marauder_app_free(WifiMarauderApp* app) { + furi_assert(app); + + // Views + view_dispatcher_remove_view(app->view_dispatcher, WifiMarauderAppViewVarItemList); + view_dispatcher_remove_view(app->view_dispatcher, WifiMarauderAppViewConsoleOutput); + view_dispatcher_remove_view(app->view_dispatcher, WifiMarauderAppViewTextInput); + view_dispatcher_remove_view(app->view_dispatcher, WifiMarauderAppViewWidget); + widget_free(app->widget); + text_box_free(app->text_box); + furi_string_free(app->text_box_store); + text_input_free(app->text_input); + storage_file_free(app->capture_file); + storage_file_free(app->log_file); + storage_file_free(app->save_pcap_setting_file); + storage_file_free(app->save_logs_setting_file); + + // View dispatcher + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + wifi_marauder_uart_free(app->uart); + wifi_marauder_uart_free(app->lp_uart); + + // Close records + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_DIALOGS); + + free(app); +} + +int32_t wifi_marauder_app(void* p) { + UNUSED(p); + furi_hal_power_disable_external_3_3v(); + furi_hal_power_disable_otg(); + furi_delay_ms(200); + furi_hal_power_enable_external_3_3v(); + furi_hal_power_enable_otg(); + for(int i=0;i<2;i++) + { + furi_delay_ms(500); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[1]){'w'}, 1); + } + furi_delay_ms(1); + + WifiMarauderApp* wifi_marauder_app = wifi_marauder_app_alloc(); + + wifi_marauder_make_app_folder(wifi_marauder_app); + wifi_marauder_load_settings(wifi_marauder_app); + + wifi_marauder_app->uart = wifi_marauder_usart_init(wifi_marauder_app); + wifi_marauder_app->lp_uart = wifi_marauder_lp_uart_init(wifi_marauder_app); + + view_dispatcher_run(wifi_marauder_app->view_dispatcher); + + wifi_marauder_app_free(wifi_marauder_app); + + furi_hal_power_disable_otg(); + + return 0; +} diff --git a/applications/external/esp32cam_marauder_companion/wifi_marauder_app.h b/applications/external/esp32cam_marauder_companion/wifi_marauder_app.h new file mode 100644 index 000000000..89a06f904 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/wifi_marauder_app.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define WIFI_MARAUDER_APP_VERSION "v0.3.3" + +typedef struct WifiMarauderApp WifiMarauderApp; + +#ifdef __cplusplus +} +#endif diff --git a/applications/external/esp32cam_marauder_companion/wifi_marauder_app_i.h b/applications/external/esp32cam_marauder_companion/wifi_marauder_app_i.h new file mode 100644 index 000000000..62b858cea --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/wifi_marauder_app_i.h @@ -0,0 +1,108 @@ +//** Includes sniffbt and sniffskim for compatible ESP32-WROOM hardware. +// wifi_marauder_scene_start.c also changed **// +#pragma once + +#include "wifi_marauder_app.h" +#include "scenes/wifi_marauder_scene.h" +#include "wifi_marauder_custom_event.h" +#include "wifi_marauder_uart.h" +#include "wifi_marauder_pcap.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define NUM_MENU_ITEMS (18) + +#define WIFI_MARAUDER_TEXT_BOX_STORE_SIZE (4096) +#define WIFI_MARAUDER_TEXT_INPUT_STORE_SIZE (512) + +#define MARAUDER_APP_FOLDER_USER "apps_data/marauder" +#define MARAUDER_APP_FOLDER ANY_PATH(MARAUDER_APP_FOLDER_USER) +#define MARAUDER_APP_FOLDER_PCAPS MARAUDER_APP_FOLDER "/pcaps" +#define MARAUDER_APP_FOLDER_LOGS MARAUDER_APP_FOLDER "/logs" +#define MARAUDER_APP_FOLDER_USER_PCAPS MARAUDER_APP_FOLDER_USER "/pcaps" +#define MARAUDER_APP_FOLDER_USER_LOGS MARAUDER_APP_FOLDER_USER "/logs" +#define SAVE_PCAP_SETTING_FILEPATH MARAUDER_APP_FOLDER "/save_pcaps_here.setting" +#define SAVE_LOGS_SETTING_FILEPATH MARAUDER_APP_FOLDER "/save_logs_here.setting" + +struct WifiMarauderApp { + Gui* gui; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + + char text_input_store[WIFI_MARAUDER_TEXT_INPUT_STORE_SIZE + 1]; + FuriString* text_box_store; + size_t text_box_store_strlen; + TextBox* text_box; + TextInput* text_input; + Storage* storage; + File* capture_file; + File* log_file; + char log_file_path[100]; + File* save_pcap_setting_file; + File* save_logs_setting_file; + bool need_to_prompt_settings_init; + int which_prompt; + bool ok_to_save_pcaps; + bool ok_to_save_logs; + bool has_saved_logs_this_session; + DialogsApp* dialogs; + + VariableItemList* var_item_list; + Widget* widget; + int open_log_file_page; + int open_log_file_num_pages; + + WifiMarauderUart* uart; + WifiMarauderUart* lp_uart; + int selected_menu_index; + int selected_option_index[NUM_MENU_ITEMS]; + const char* selected_tx_string; + bool is_command; + bool is_custom_tx_string; + bool focus_console_start; + bool show_stopscan_tip; + bool is_writing_pcap; + bool is_writing_log; + + // For input source and destination MAC in targeted deauth attack + int special_case_input_step; + char special_case_input_src_addr[20]; + char special_case_input_dst_addr[20]; +}; + +// Supported commands: +// https://github.com/justcallmekoko/ESP32Marauder/wiki/cli +// Scan +// -> If list is empty, then start a new scanap. (Tap any button to stop.) +// -> If there's a list, provide option to rescan and dump list of targets to select. +// -> Press BACK to go back to top-level. +// Attack +// -> Beacon +// -> Deauth +// -> Probe +// -> Rickroll +// Sniff +// -> Beacon +// -> Deauth +// -> ESP +// -> PMKID +// -> Pwnagotchi +// Channel +// Update +// Reboot + +typedef enum { + WifiMarauderAppViewVarItemList, + WifiMarauderAppViewConsoleOutput, + WifiMarauderAppViewTextInput, + WifiMarauderAppViewWidget, +} WifiMarauderAppView; diff --git a/applications/external/esp32cam_marauder_companion/wifi_marauder_custom_event.h b/applications/external/esp32cam_marauder_companion/wifi_marauder_custom_event.h new file mode 100644 index 000000000..79f96b107 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/wifi_marauder_custom_event.h @@ -0,0 +1,11 @@ +#pragma once + +typedef enum { + WifiMarauderEventRefreshConsoleOutput = 0, + WifiMarauderEventStartConsole, + WifiMarauderEventStartKeyboard, + WifiMarauderEventSaveSourceMac, + WifiMarauderEventSaveDestinationMac, + WifiMarauderEventStartSettingsInit, + WifiMarauderEventStartLogViewer +} WifiMarauderCustomEvent; diff --git a/applications/external/esp32cam_marauder_companion/wifi_marauder_pcap.c b/applications/external/esp32cam_marauder_companion/wifi_marauder_pcap.c new file mode 100644 index 000000000..73e3d98ea --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/wifi_marauder_pcap.c @@ -0,0 +1,64 @@ +#include "wifi_marauder_app_i.h" +#include "wifi_marauder_pcap.h" + +void wifi_marauder_get_prefix_from_sniff_cmd(char* dest, const char* command) { + int start, end, delta; + start = strlen("sniff"); + end = strcspn(command, " "); + delta = end - start; + strncpy(dest, command + start, end - start); + dest[delta] = '\0'; +} + +void wifi_marauder_get_prefix_from_cmd(char* dest, const char* command) { + int end; + end = strcspn(command, " "); + strncpy(dest, command, end); + dest[end] = '\0'; +} + +void wifi_marauder_create_pcap_file(WifiMarauderApp* app) { + char prefix[10]; + char capture_file_path[100]; + wifi_marauder_get_prefix_from_sniff_cmd(prefix, app->selected_tx_string); + + int i = 0; + do { + snprintf( + capture_file_path, + sizeof(capture_file_path), + "%s/%s_%d.pcap", + MARAUDER_APP_FOLDER_PCAPS, + prefix, + i); + i++; + } while(storage_file_exists(app->storage, capture_file_path)); + + if(!storage_file_open(app->capture_file, capture_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + dialog_message_show_storage_error(app->dialogs, "Cannot open pcap file"); + } +} + +void wifi_marauder_create_log_file(WifiMarauderApp* app) { + char prefix[10]; + char log_file_path[100]; + wifi_marauder_get_prefix_from_cmd(prefix, app->selected_tx_string); + + int i = 0; + do { + snprintf( + log_file_path, + sizeof(log_file_path), + "%s/%s_%d.log", + MARAUDER_APP_FOLDER_LOGS, + prefix, + i); + i++; + } while(storage_file_exists(app->storage, log_file_path)); + + if(!storage_file_open(app->log_file, log_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + dialog_message_show_storage_error(app->dialogs, "Cannot open log file"); + } else { + strcpy(app->log_file_path, log_file_path); + } +} \ No newline at end of file diff --git a/applications/external/esp32cam_marauder_companion/wifi_marauder_pcap.h b/applications/external/esp32cam_marauder_companion/wifi_marauder_pcap.h new file mode 100644 index 000000000..94f6282f3 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/wifi_marauder_pcap.h @@ -0,0 +1,20 @@ +#pragma once + +#include "furi_hal.h" + +/** + * Creates a PCAP file to store incoming packets. + * The file name will have a prefix according to the type of scan being performed by the application (Eg: raw_0.pcap) + * + * @param app Application context + */ +void wifi_marauder_create_pcap_file(WifiMarauderApp* app); + +/** + * Creates a log file to store text from console output. + * The file name will have a prefix according to the command being performed by the application (Eg: scanap_0.log) + * + * @param app Application context + */ +// same as wifi_marauder_create_pcap_file, but for log files (to save console text output) +void wifi_marauder_create_log_file(WifiMarauderApp* app); diff --git a/applications/external/esp32cam_marauder_companion/wifi_marauder_uart.c b/applications/external/esp32cam_marauder_companion/wifi_marauder_uart.c new file mode 100644 index 000000000..37304f06a --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/wifi_marauder_uart.c @@ -0,0 +1,112 @@ +#include "wifi_marauder_app_i.h" +#include "wifi_marauder_uart.h" + +#define UART_CH (FuriHalUartIdUSART1) +#define LP_UART_CH (FuriHalUartIdLPUART1) +#define BAUDRATE (230400) + +struct WifiMarauderUart { + WifiMarauderApp* app; + FuriHalUartId channel; + FuriThread* rx_thread; + FuriStreamBuffer* rx_stream; + uint8_t rx_buf[RX_BUF_SIZE + 1]; + void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context); +}; + +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +void wifi_marauder_uart_set_handle_rx_data_cb( + WifiMarauderUart* uart, + void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context)) { + furi_assert(uart); + uart->handle_rx_data_cb = handle_rx_data_cb; +} + +#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone) + +void wifi_marauder_uart_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) { + WifiMarauderUart* uart = (WifiMarauderUart*)context; + + if(ev == UartIrqEventRXNE) { + furi_stream_buffer_send(uart->rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtRxDone); + } +} + +static int32_t uart_worker(void* context) { + WifiMarauderUart* uart = (void*)context; + + while(1) { + uint32_t events = + furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever); + furi_check((events & FuriFlagError) == 0); + if(events & WorkerEvtStop) break; + if(events & WorkerEvtRxDone) { + size_t len = furi_stream_buffer_receive(uart->rx_stream, uart->rx_buf, RX_BUF_SIZE, 0); + if(len > 0) { + if(uart->handle_rx_data_cb) uart->handle_rx_data_cb(uart->rx_buf, len, uart->app); + } + } + } + + furi_stream_buffer_free(uart->rx_stream); + + return 0; +} + +void wifi_marauder_uart_tx(uint8_t* data, size_t len) { + furi_hal_uart_tx(UART_CH, data, len); +} + +void wifi_marauder_lp_uart_tx(uint8_t* data, size_t len) { + furi_hal_uart_tx(LP_UART_CH, data, len); +} + +WifiMarauderUart* + wifi_marauder_uart_init(WifiMarauderApp* app, FuriHalUartId channel, const char* thread_name) { + WifiMarauderUart* uart = malloc(sizeof(WifiMarauderUart)); + + uart->app = app; + uart->channel = channel; + uart->rx_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); + uart->rx_thread = furi_thread_alloc(); + furi_thread_set_name(uart->rx_thread, thread_name); + furi_thread_set_stack_size(uart->rx_thread, 1024); + furi_thread_set_context(uart->rx_thread, uart); + furi_thread_set_callback(uart->rx_thread, uart_worker); + furi_thread_start(uart->rx_thread); + if(channel == FuriHalUartIdUSART1) { + furi_hal_console_disable(); + } else if(channel == FuriHalUartIdLPUART1) { + furi_hal_uart_init(channel, BAUDRATE); + } + furi_hal_uart_set_br(channel, BAUDRATE); + furi_hal_uart_set_irq_cb(channel, wifi_marauder_uart_on_irq_cb, uart); + + return uart; +} + +WifiMarauderUart* wifi_marauder_usart_init(WifiMarauderApp* app) { + return wifi_marauder_uart_init(app, UART_CH, "WifiMarauderUartRxThread"); +} + +WifiMarauderUart* wifi_marauder_lp_uart_init(WifiMarauderApp* app) { + return wifi_marauder_uart_init(app, LP_UART_CH, "WifiMarauderLPUartRxThread"); +} + +void wifi_marauder_uart_free(WifiMarauderUart* uart) { + furi_assert(uart); + + furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtStop); + furi_thread_join(uart->rx_thread); + furi_thread_free(uart->rx_thread); + + furi_hal_uart_set_irq_cb(uart->channel, NULL, NULL); + furi_hal_console_enable(); + + free(uart); +} \ No newline at end of file diff --git a/applications/external/esp32cam_marauder_companion/wifi_marauder_uart.h b/applications/external/esp32cam_marauder_companion/wifi_marauder_uart.h new file mode 100644 index 000000000..e352cfec5 --- /dev/null +++ b/applications/external/esp32cam_marauder_companion/wifi_marauder_uart.h @@ -0,0 +1,16 @@ +#pragma once + +#include "furi_hal.h" + +#define RX_BUF_SIZE (2048) + +typedef struct WifiMarauderUart WifiMarauderUart; + +void wifi_marauder_uart_set_handle_rx_data_cb( + WifiMarauderUart* uart, + void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context)); +void wifi_marauder_uart_tx(uint8_t* data, size_t len); +void wifi_marauder_lp_uart_tx(uint8_t* data, size_t len); +WifiMarauderUart* wifi_marauder_usart_init(WifiMarauderApp* app); +WifiMarauderUart* wifi_marauder_lp_uart_init(WifiMarauderApp* app); +void wifi_marauder_uart_free(WifiMarauderUart* uart); diff --git a/applications/external/esp32cam_morseflasher/LICENSE b/applications/external/esp32cam_morseflasher/LICENSE new file mode 100644 index 000000000..c4613e7ea --- /dev/null +++ b/applications/external/esp32cam_morseflasher/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 Malik cool4uma + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/applications/external/esp32cam_morseflasher/application.fam b/applications/external/esp32cam_morseflasher/application.fam new file mode 100644 index 000000000..c15207015 --- /dev/null +++ b/applications/external/esp32cam_morseflasher/application.fam @@ -0,0 +1,14 @@ +App( + appid="MAYHEM_MorseFlash", + name="[MAYHEM] Morse Flasher", + apptype=FlipperAppType.EXTERNAL, + entry_point="uart_terminal_app", + cdefines=["APP_UART_TERMINAL"], + requires=["gui"], + stack_size=1 * 1024, + order=90, + fap_icon_assets="assets", + fap_icon="icon.png", + fap_category="GPIO", + fap_description="ESP32-CAM app to stream a message in morse using the powerful flashlight. [Unplug the USB cable to test with Mayhem]", +) diff --git a/applications/external/esp32cam_morseflasher/assets/KeyBackspaceSelected_16x9.png b/applications/external/esp32cam_morseflasher/assets/KeyBackspaceSelected_16x9.png new file mode 100644 index 000000000..7cc0759a8 Binary files /dev/null and b/applications/external/esp32cam_morseflasher/assets/KeyBackspaceSelected_16x9.png differ diff --git a/applications/external/esp32cam_morseflasher/assets/KeyBackspace_16x9.png b/applications/external/esp32cam_morseflasher/assets/KeyBackspace_16x9.png new file mode 100644 index 000000000..9946232d9 Binary files /dev/null and b/applications/external/esp32cam_morseflasher/assets/KeyBackspace_16x9.png differ diff --git a/applications/external/esp32cam_morseflasher/assets/KeySaveSelected_24x11.png b/applications/external/esp32cam_morseflasher/assets/KeySaveSelected_24x11.png new file mode 100644 index 000000000..eeb3569d3 Binary files /dev/null and b/applications/external/esp32cam_morseflasher/assets/KeySaveSelected_24x11.png differ diff --git a/applications/external/esp32cam_morseflasher/assets/KeySave_24x11.png b/applications/external/esp32cam_morseflasher/assets/KeySave_24x11.png new file mode 100644 index 000000000..e7dba987a Binary files /dev/null and b/applications/external/esp32cam_morseflasher/assets/KeySave_24x11.png differ diff --git a/applications/external/esp32cam_morseflasher/assets/WarningDolphin_45x42.png b/applications/external/esp32cam_morseflasher/assets/WarningDolphin_45x42.png new file mode 100644 index 000000000..d766ffbb4 Binary files /dev/null and b/applications/external/esp32cam_morseflasher/assets/WarningDolphin_45x42.png differ diff --git a/applications/external/esp32cam_morseflasher/icon.png b/applications/external/esp32cam_morseflasher/icon.png new file mode 100644 index 000000000..5cd18293c Binary files /dev/null and b/applications/external/esp32cam_morseflasher/icon.png differ diff --git a/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene.c b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene.c new file mode 100644 index 000000000..451c5d98b --- /dev/null +++ b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene.c @@ -0,0 +1,30 @@ +#include "uart_terminal_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const uart_terminal_scene_on_enter_handlers[])(void*) = { +#include "uart_terminal_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const uart_terminal_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "uart_terminal_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const uart_terminal_scene_on_exit_handlers[])(void* context) = { +#include "uart_terminal_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers uart_terminal_scene_handlers = { + .on_enter_handlers = uart_terminal_scene_on_enter_handlers, + .on_event_handlers = uart_terminal_scene_on_event_handlers, + .on_exit_handlers = uart_terminal_scene_on_exit_handlers, + .scene_num = UART_TerminalSceneNum, +}; diff --git a/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene.h b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene.h new file mode 100644 index 000000000..c6f4ed4b4 --- /dev/null +++ b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) UART_TerminalScene##id, +typedef enum { +#include "uart_terminal_scene_config.h" + UART_TerminalSceneNum, +} UART_TerminalScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers uart_terminal_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "uart_terminal_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "uart_terminal_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "uart_terminal_scene_config.h" +#undef ADD_SCENE diff --git a/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_config.h b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_config.h new file mode 100644 index 000000000..febdce167 --- /dev/null +++ b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_config.h @@ -0,0 +1,3 @@ +ADD_SCENE(uart_terminal, start, Start) +ADD_SCENE(uart_terminal, console_output, ConsoleOutput) +ADD_SCENE(uart_terminal, text_input, UART_TextInput) diff --git a/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_console_output.c b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_console_output.c new file mode 100644 index 000000000..df3d20228 --- /dev/null +++ b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_console_output.c @@ -0,0 +1,96 @@ +#include "../uart_terminal_app_i.h" + +void uart_terminal_console_output_handle_rx_data_cb(uint8_t* buf, size_t len, void* context) { + furi_assert(context); + UART_TerminalApp* app = context; + + // If text box store gets too big, then truncate it + app->text_box_store_strlen += len; + if(app->text_box_store_strlen >= UART_TERMINAL_TEXT_BOX_STORE_SIZE - 1) { + furi_string_right(app->text_box_store, app->text_box_store_strlen / 2); + app->text_box_store_strlen = furi_string_size(app->text_box_store) + len; + } + + // Null-terminate buf and append to text box store + buf[len] = '\0'; + furi_string_cat_printf(app->text_box_store, "%s", buf); + + view_dispatcher_send_custom_event( + app->view_dispatcher, UART_TerminalEventRefreshConsoleOutput); +} + +void uart_terminal_scene_console_output_on_enter(void* context) { + UART_TerminalApp* app = context; + + TextBox* text_box = app->text_box; + text_box_reset(app->text_box); + text_box_set_font(text_box, TextBoxFontText); + if(app->focus_console_start) { + text_box_set_focus(text_box, TextBoxFocusStart); + } else { + text_box_set_focus(text_box, TextBoxFocusEnd); + } + + if(app->is_command) { + furi_string_reset(app->text_box_store); + app->text_box_store_strlen = 0; + + // app->show_stopscan_tip in the if is just a hack to get the help displayed since there is no commands in this app + if(app->show_stopscan_tip || 0 == strncmp("help", app->selected_tx_string, strlen("help"))) { + const char* help_msg = + "Morse Flasher for\nMayhem Fin\n\nBased on UART terminal by\ncool4uma, which is a\nmodified WiFi Marauder\ncompanion by 0xchocolate\n\n"; + furi_string_cat_str(app->text_box_store, help_msg); + app->text_box_store_strlen += strlen(help_msg); + } + + if(app->show_stopscan_tip) { + const char* help_msg = "Press BACK to return\n"; + furi_string_cat_str(app->text_box_store, help_msg); + app->text_box_store_strlen += strlen(help_msg); + } + } + + // Set starting text - for "View Log", this will just be what was already in the text box store + text_box_set_text(app->text_box, furi_string_get_cstr(app->text_box_store)); + + scene_manager_set_scene_state(app->scene_manager, UART_TerminalSceneConsoleOutput, 0); + view_dispatcher_switch_to_view(app->view_dispatcher, UART_TerminalAppViewConsoleOutput); + + // Register callback to receive data + uart_terminal_uart_set_handle_rx_data_cb( + app->uart, uart_terminal_console_output_handle_rx_data_cb); // setup callback for rx thread + + // Send command with newline '\n' + /*if(!app->is_command && app->selected_tx_string)*/ { + uart_terminal_uart_tx( + (uint8_t*)(app->selected_tx_string), strlen(app->selected_tx_string)); + uart_terminal_uart_tx((uint8_t*)("\n"), 1); + } +} + +bool uart_terminal_scene_console_output_on_event(void* context, SceneManagerEvent event) { + UART_TerminalApp* app = context; + + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + text_box_set_text(app->text_box, furi_string_get_cstr(app->text_box_store)); + consumed = true; + } else if(event.type == SceneManagerEventTypeTick) { + consumed = true; + } + + return consumed; +} + +void uart_terminal_scene_console_output_on_exit(void* context) { + UART_TerminalApp* app = context; + + // Unregister rx callback + uart_terminal_uart_set_handle_rx_data_cb(app->uart, NULL); + + // Automatically logut when exiting view + //if(app->is_command) { + // uart_terminal_uart_tx((uint8_t*)("exit\n"), strlen("exit\n")); + //} +} diff --git a/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_start.c b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_start.c new file mode 100644 index 000000000..8d638283b --- /dev/null +++ b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_start.c @@ -0,0 +1,130 @@ +#include "../uart_terminal_app_i.h" + +// For each command, define whether additional arguments are needed +// (enabling text input to fill them out), and whether the console +// text box should focus at the start of the output or the end +typedef enum { NO_ARGS = 0, INPUT_ARGS, TOGGLE_ARGS } InputArgs; + +typedef enum { FOCUS_CONSOLE_END = 0, FOCUS_CONSOLE_START, FOCUS_CONSOLE_TOGGLE } FocusConsole; + +#define SHOW_STOPSCAN_TIP (true) +#define NO_TIP (false) + +#define MAX_OPTIONS (9) +typedef struct { + const char* item_string; + const char* options_menu[MAX_OPTIONS]; + int num_options_menu; + const char* actual_commands[MAX_OPTIONS]; + InputArgs needs_keyboard; + FocusConsole focus_console; + bool show_stopscan_tip; +} UART_TerminalItem; + +// NUM_MENU_ITEMS defined in uart_terminal_app_i.h - if you add an entry here, increment it! +const UART_TerminalItem items[NUM_MENU_ITEMS] = { + {"New custom message", {""}, 1, {""}, INPUT_ARGS, FOCUS_CONSOLE_END, NO_TIP}, + {"Quick message", + {"SOS", "CQD", "VVV", "Eureka", "E.T ph...", "what h...", "Mayhem", "Flipper"}, + 8, + {"sos", "cqd", "vvv", "eureka", "e.t. phone home", "what hath god wrought!", "let the mayhem begin", "flipper zero in da housa"}, + NO_ARGS, + FOCUS_CONSOLE_END, + NO_TIP}, + {"Help", {""}, 1, {""}, NO_ARGS, FOCUS_CONSOLE_START, SHOW_STOPSCAN_TIP}, +}; + +static void uart_terminal_scene_start_var_list_enter_callback(void* context, uint32_t index) { + furi_assert(context); + UART_TerminalApp* app = context; + + furi_assert(index < NUM_MENU_ITEMS); + const UART_TerminalItem* item = &items[index]; + + const int selected_option_index = app->selected_option_index[index]; + furi_assert(selected_option_index < item->num_options_menu); + app->selected_tx_string = item->actual_commands[selected_option_index]; + app->is_command = (1 <= index); + app->is_custom_tx_string = false; + app->selected_menu_index = index; + app->focus_console_start = (item->focus_console == FOCUS_CONSOLE_TOGGLE) ? + (selected_option_index == 0) : + item->focus_console; + app->show_stopscan_tip = item->show_stopscan_tip; + + bool needs_keyboard = (item->needs_keyboard == TOGGLE_ARGS) ? (selected_option_index != 0) : + item->needs_keyboard; + if(needs_keyboard) { + view_dispatcher_send_custom_event(app->view_dispatcher, UART_TerminalEventStartKeyboard); + } else { + view_dispatcher_send_custom_event(app->view_dispatcher, UART_TerminalEventStartConsole); + } +} + +static void uart_terminal_scene_start_var_list_change_callback(VariableItem* item) { + furi_assert(item); + + UART_TerminalApp* app = variable_item_get_context(item); + furi_assert(app); + + const UART_TerminalItem* menu_item = &items[app->selected_menu_index]; + uint8_t item_index = variable_item_get_current_value_index(item); + furi_assert(item_index < menu_item->num_options_menu); + variable_item_set_current_value_text(item, menu_item->options_menu[item_index]); + app->selected_option_index[app->selected_menu_index] = item_index; +} + +void uart_terminal_scene_start_on_enter(void* context) { + UART_TerminalApp* app = context; + VariableItemList* var_item_list = app->var_item_list; + + variable_item_list_set_enter_callback( + var_item_list, uart_terminal_scene_start_var_list_enter_callback, app); + + VariableItem* item; + for(int i = 0; i < NUM_MENU_ITEMS; ++i) { + item = variable_item_list_add( + var_item_list, + items[i].item_string, + items[i].num_options_menu, + uart_terminal_scene_start_var_list_change_callback, + app); + variable_item_set_current_value_index(item, app->selected_option_index[i]); + variable_item_set_current_value_text( + item, items[i].options_menu[app->selected_option_index[i]]); + } + + variable_item_list_set_selected_item( + var_item_list, scene_manager_get_scene_state(app->scene_manager, UART_TerminalSceneStart)); + + view_dispatcher_switch_to_view(app->view_dispatcher, UART_TerminalAppViewVarItemList); +} + +bool uart_terminal_scene_start_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UART_TerminalApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == UART_TerminalEventStartKeyboard) { + scene_manager_set_scene_state( + app->scene_manager, UART_TerminalSceneStart, app->selected_menu_index); + scene_manager_next_scene(app->scene_manager, UART_TerminalAppViewTextInput); + } else if(event.event == UART_TerminalEventStartConsole) { + scene_manager_set_scene_state( + app->scene_manager, UART_TerminalSceneStart, app->selected_menu_index); + scene_manager_next_scene(app->scene_manager, UART_TerminalAppViewConsoleOutput); + } + consumed = true; + } else if(event.type == SceneManagerEventTypeTick) { + app->selected_menu_index = variable_item_list_get_selected_item_index(app->var_item_list); + consumed = true; + } + + return consumed; +} + +void uart_terminal_scene_start_on_exit(void* context) { + UART_TerminalApp* app = context; + variable_item_list_reset(app->var_item_list); +} diff --git a/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_text_input.c b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_text_input.c new file mode 100644 index 000000000..b3b469063 --- /dev/null +++ b/applications/external/esp32cam_morseflasher/scenes/uart_terminal_scene_text_input.c @@ -0,0 +1,60 @@ +#include "../uart_terminal_app_i.h" + +void uart_terminal_scene_text_input_callback(void* context) { + UART_TerminalApp* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, UART_TerminalEventStartConsole); +} + +void uart_terminal_scene_text_input_on_enter(void* context) { + UART_TerminalApp* app = context; + + if(false == app->is_custom_tx_string) { + // Fill text input with selected string so that user can add to it + size_t length = strlen(app->selected_tx_string); + furi_assert(length < UART_TERMINAL_TEXT_INPUT_STORE_SIZE); + bzero(app->text_input_store, UART_TERMINAL_TEXT_INPUT_STORE_SIZE); + strncpy(app->text_input_store, app->selected_tx_string, length); + + // Add space - because flipper keyboard currently doesn't have a space + //app->text_input_store[length] = ' '; + app->text_input_store[length + 1] = '\0'; + app->is_custom_tx_string = true; + } + + // Setup view + UART_TextInput* text_input = app->text_input; + // Add help message to header + uart_text_input_set_header_text(text_input, "Send new morse message"); + uart_text_input_set_result_callback( + text_input, + uart_terminal_scene_text_input_callback, + app, + app->text_input_store, + UART_TERMINAL_TEXT_INPUT_STORE_SIZE, + false); + + view_dispatcher_switch_to_view(app->view_dispatcher, UART_TerminalAppViewTextInput); +} + +bool uart_terminal_scene_text_input_on_event(void* context, SceneManagerEvent event) { + UART_TerminalApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == UART_TerminalEventStartConsole) { + // Point to custom string to send + app->selected_tx_string = app->text_input_store; + scene_manager_next_scene(app->scene_manager, UART_TerminalAppViewConsoleOutput); + consumed = true; + } + } + + return consumed; +} + +void uart_terminal_scene_text_input_on_exit(void* context) { + UART_TerminalApp* app = context; + + uart_text_input_reset(app->text_input); +} diff --git a/applications/external/esp32cam_morseflasher/uart_terminal_app.c b/applications/external/esp32cam_morseflasher/uart_terminal_app.c new file mode 100644 index 000000000..29158c89c --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_terminal_app.c @@ -0,0 +1,113 @@ +#include "uart_terminal_app_i.h" + +#include +#include + +static bool uart_terminal_app_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + UART_TerminalApp* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool uart_terminal_app_back_event_callback(void* context) { + furi_assert(context); + UART_TerminalApp* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static void uart_terminal_app_tick_event_callback(void* context) { + furi_assert(context); + UART_TerminalApp* app = context; + scene_manager_handle_tick_event(app->scene_manager); +} + +UART_TerminalApp* uart_terminal_app_alloc() { + UART_TerminalApp* app = malloc(sizeof(UART_TerminalApp)); + + app->gui = furi_record_open(RECORD_GUI); + + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&uart_terminal_scene_handlers, app); + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, uart_terminal_app_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, uart_terminal_app_back_event_callback); + view_dispatcher_set_tick_event_callback( + app->view_dispatcher, uart_terminal_app_tick_event_callback, 100); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + app->var_item_list = variable_item_list_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + UART_TerminalAppViewVarItemList, + variable_item_list_get_view(app->var_item_list)); + + for(int i = 0; i < NUM_MENU_ITEMS; ++i) { + app->selected_option_index[i] = 0; + } + + app->text_box = text_box_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, UART_TerminalAppViewConsoleOutput, text_box_get_view(app->text_box)); + app->text_box_store = furi_string_alloc(); + furi_string_reserve(app->text_box_store, UART_TERMINAL_TEXT_BOX_STORE_SIZE); + + app->text_input = uart_text_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, UART_TerminalAppViewTextInput, uart_text_input_get_view(app->text_input)); + + scene_manager_next_scene(app->scene_manager, UART_TerminalSceneStart); + + return app; +} + +void uart_terminal_app_free(UART_TerminalApp* app) { + furi_assert(app); + + // Views + view_dispatcher_remove_view(app->view_dispatcher, UART_TerminalAppViewVarItemList); + view_dispatcher_remove_view(app->view_dispatcher, UART_TerminalAppViewConsoleOutput); + view_dispatcher_remove_view(app->view_dispatcher, UART_TerminalAppViewTextInput); + text_box_free(app->text_box); + furi_string_free(app->text_box_store); + uart_text_input_free(app->text_input); + + // View dispatcher + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + uart_terminal_uart_free(app->uart); + + // Close records + furi_record_close(RECORD_GUI); + + free(app); +} + +int32_t uart_terminal_app(void* p) { + UNUSED(p); + furi_hal_power_disable_external_3_3v(); + furi_hal_power_disable_otg(); + furi_delay_ms(200); + furi_hal_power_enable_external_3_3v(); + furi_hal_power_enable_otg(); + for(int i=0;i<2;i++) + { + furi_delay_ms(500); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[1]){'.'}, 1); + } + furi_delay_ms(1); + UART_TerminalApp* uart_terminal_app = uart_terminal_app_alloc(); + + uart_terminal_app->uart = uart_terminal_uart_init(uart_terminal_app); + + view_dispatcher_run(uart_terminal_app->view_dispatcher); + + uart_terminal_app_free(uart_terminal_app); + furi_hal_power_disable_otg(); + return 0; +} diff --git a/applications/external/esp32cam_morseflasher/uart_terminal_app.h b/applications/external/esp32cam_morseflasher/uart_terminal_app.h new file mode 100644 index 000000000..c859d828b --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_terminal_app.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct UART_TerminalApp UART_TerminalApp; + +#ifdef __cplusplus +} +#endif diff --git a/applications/external/esp32cam_morseflasher/uart_terminal_app_i.h b/applications/external/esp32cam_morseflasher/uart_terminal_app_i.h new file mode 100644 index 000000000..80f78c8ab --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_terminal_app_i.h @@ -0,0 +1,47 @@ +#pragma once + +#include "uart_terminal_app.h" +#include "scenes/uart_terminal_scene.h" +#include "uart_terminal_custom_event.h" +#include "uart_terminal_uart.h" + +#include +#include +#include +#include +#include "uart_text_input.h" +#include + +#define NUM_MENU_ITEMS (3) + +#define UART_TERMINAL_TEXT_BOX_STORE_SIZE (4096) +#define UART_TERMINAL_TEXT_INPUT_STORE_SIZE (512) + +struct UART_TerminalApp { + Gui* gui; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + + char text_input_store[UART_TERMINAL_TEXT_INPUT_STORE_SIZE + 1]; + FuriString* text_box_store; + size_t text_box_store_strlen; + TextBox* text_box; + UART_TextInput* text_input; + + VariableItemList* var_item_list; + + UART_TerminalUart* uart; + int selected_menu_index; + int selected_option_index[NUM_MENU_ITEMS]; + const char* selected_tx_string; + bool is_command; + bool is_custom_tx_string; + bool focus_console_start; + bool show_stopscan_tip; +}; + +typedef enum { + UART_TerminalAppViewVarItemList, + UART_TerminalAppViewConsoleOutput, + UART_TerminalAppViewTextInput, +} UART_TerminalAppView; diff --git a/applications/external/esp32cam_morseflasher/uart_terminal_custom_event.h b/applications/external/esp32cam_morseflasher/uart_terminal_custom_event.h new file mode 100644 index 000000000..d57d822d1 --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_terminal_custom_event.h @@ -0,0 +1,7 @@ +#pragma once + +typedef enum { + UART_TerminalEventRefreshConsoleOutput = 0, + UART_TerminalEventStartConsole, + UART_TerminalEventStartKeyboard, +} UART_TerminalCustomEvent; diff --git a/applications/external/esp32cam_morseflasher/uart_terminal_uart.c b/applications/external/esp32cam_morseflasher/uart_terminal_uart.c new file mode 100644 index 000000000..c8dc5c0a5 --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_terminal_uart.c @@ -0,0 +1,101 @@ +#include "uart_terminal_app_i.h" +#include "uart_terminal_uart.h" + +#define UART_CH (FuriHalUartIdUSART1) +#define BAUDRATE (230400) + +struct UART_TerminalUart { + UART_TerminalApp* app; + FuriThread* rx_thread; + FuriStreamBuffer* rx_stream; + uint8_t rx_buf[RX_BUF_SIZE + 1]; + void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context); +}; + +typedef enum { + WorkerEvtStop = (1 << 0), + WorkerEvtRxDone = (1 << 1), +} WorkerEvtFlags; + +void uart_terminal_uart_set_handle_rx_data_cb( + UART_TerminalUart* uart, + void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context)) { + furi_assert(uart); + uart->handle_rx_data_cb = handle_rx_data_cb; +} + +#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone) + +void uart_terminal_uart_on_irq_cb(UartIrqEvent ev, uint8_t data, void* context) { + UART_TerminalUart* uart = (UART_TerminalUart*)context; + + if(ev == UartIrqEventRXNE) { + furi_stream_buffer_send(uart->rx_stream, &data, 1, 0); + furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtRxDone); + } +} + +static int32_t uart_worker(void* context) { + UART_TerminalUart* uart = (void*)context; + + while(1) { + uint32_t events = + furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever); + furi_check((events & FuriFlagError) == 0); + if(events & WorkerEvtStop) break; + if(events & WorkerEvtRxDone) { + size_t len = furi_stream_buffer_receive(uart->rx_stream, uart->rx_buf, RX_BUF_SIZE, 0); + if(len > 0) { + if(uart->handle_rx_data_cb) uart->handle_rx_data_cb(uart->rx_buf, len, uart->app); + } + } + } + + furi_stream_buffer_free(uart->rx_stream); + + return 0; +} + +void uart_terminal_uart_tx(uint8_t* data, size_t len) { + furi_hal_uart_tx(UART_CH, data, len); +} + +UART_TerminalUart* uart_terminal_uart_init(UART_TerminalApp* app) { + UART_TerminalUart* uart = malloc(sizeof(UART_TerminalUart)); + + /*furi_hal_console_disable(); + if(app->BAUDRATE == 0) { + app->BAUDRATE = 230400; + } + furi_hal_uart_set_br(UART_CH, app->BAUDRATE); + furi_hal_uart_set_irq_cb(UART_CH, uart_terminal_uart_on_irq_cb, uart);*/ + + uart->app = app; + uart->rx_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1); + uart->rx_thread = furi_thread_alloc(); + furi_thread_set_name(uart->rx_thread, "UART_TerminalUartRxThread"); + furi_thread_set_stack_size(uart->rx_thread, 1024); + furi_thread_set_context(uart->rx_thread, uart); + furi_thread_set_callback(uart->rx_thread, uart_worker); + + furi_thread_start(uart->rx_thread); + + furi_hal_console_disable(); + furi_hal_uart_set_br(UART_CH, BAUDRATE); + furi_hal_uart_set_irq_cb(UART_CH, uart_terminal_uart_on_irq_cb, uart); + + return uart; +} + +void uart_terminal_uart_free(UART_TerminalUart* uart) { + furi_assert(uart); + + furi_thread_flags_set(furi_thread_get_id(uart->rx_thread), WorkerEvtStop); + furi_thread_join(uart->rx_thread); + furi_thread_free(uart->rx_thread); + + furi_hal_uart_set_irq_cb(UART_CH, NULL, NULL); + furi_hal_console_enable(); + + free(uart); +} \ No newline at end of file diff --git a/applications/external/esp32cam_morseflasher/uart_terminal_uart.h b/applications/external/esp32cam_morseflasher/uart_terminal_uart.h new file mode 100644 index 000000000..ca95c92fb --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_terminal_uart.h @@ -0,0 +1,14 @@ +#pragma once + +#include "furi_hal.h" + +#define RX_BUF_SIZE (320) + +typedef struct UART_TerminalUart UART_TerminalUart; + +void uart_terminal_uart_set_handle_rx_data_cb( + UART_TerminalUart* uart, + void (*handle_rx_data_cb)(uint8_t* buf, size_t len, void* context)); +void uart_terminal_uart_tx(uint8_t* data, size_t len); +UART_TerminalUart* uart_terminal_uart_init(UART_TerminalApp* app); +void uart_terminal_uart_free(UART_TerminalUart* uart); diff --git a/applications/external/esp32cam_morseflasher/uart_text_input.c b/applications/external/esp32cam_morseflasher/uart_text_input.c new file mode 100644 index 000000000..4e2336254 --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_text_input.c @@ -0,0 +1,637 @@ +#include "uart_text_input.h" +#include +#include "MAYHEM_MorseFlash_icons.h" +#include + +struct UART_TextInput { + View* view; + FuriTimer* timer; +}; + +typedef struct { + const char text; + const uint8_t x; + const uint8_t y; +} UART_TextInputKey; + +typedef struct { + const char* header; + char* text_buffer; + size_t text_buffer_size; + bool clear_default_text; + + UART_TextInputCallback callback; + void* callback_context; + + uint8_t selected_row; + uint8_t selected_column; + + UART_TextInputValidatorCallback validator_callback; + void* validator_callback_context; + FuriString* validator_text; + bool valadator_message_visible; +} UART_TextInputModel; + +static const uint8_t keyboard_origin_x = 1; +static const uint8_t keyboard_origin_y = 29; +static const uint8_t keyboard_row_count = 4; + +#define ENTER_KEY '\r' +#define BACKSPACE_KEY '\b' + +static const UART_TextInputKey keyboard_keys_row_1[] = { + {'{', 1, 0}, + {'(', 9, 0}, + {'[', 17, 0}, + {'|', 25, 0}, + {'@', 33, 0}, + {'&', 41, 0}, + {'#', 49, 0}, + {';', 57, 0}, + {'^', 65, 0}, + {'*', 73, 0}, + {'`', 81, 0}, + {'"', 89, 0}, + {'~', 97, 0}, + {'\'', 105, 0}, + {'.', 113, 0}, + {'/', 120, 0}, +}; + +static const UART_TextInputKey keyboard_keys_row_2[] = { + {'q', 1, 10}, + {'w', 9, 10}, + {'e', 17, 10}, + {'r', 25, 10}, + {'t', 33, 10}, + {'y', 41, 10}, + {'u', 49, 10}, + {'i', 57, 10}, + {'o', 65, 10}, + {'p', 73, 10}, + {'0', 81, 10}, + {'1', 89, 10}, + {'2', 97, 10}, + {'3', 105, 10}, + {'=', 113, 10}, + {'-', 120, 10}, +}; + +static const UART_TextInputKey keyboard_keys_row_3[] = { + {'a', 1, 21}, + {'s', 9, 21}, + {'d', 18, 21}, + {'f', 25, 21}, + {'g', 33, 21}, + {'h', 41, 21}, + {'j', 49, 21}, + {'k', 57, 21}, + {'l', 65, 21}, + {BACKSPACE_KEY, 72, 13}, + {'4', 89, 21}, + {'5', 97, 21}, + {'6', 105, 21}, + {'$', 113, 21}, + {'%', 120, 21}, + +}; + +static const UART_TextInputKey keyboard_keys_row_4[] = { + {'z', 1, 33}, + {'x', 9, 33}, + {'c', 18, 33}, + {'v', 25, 33}, + {'b', 33, 33}, + {'n', 41, 33}, + {'m', 49, 33}, + {'_', 57, 33}, + {ENTER_KEY, 64, 24}, + {'7', 89, 33}, + {'8', 97, 33}, + {'9', 105, 33}, + {'!', 113, 33}, + {'+', 120, 33}, +}; + +static uint8_t get_row_size(uint8_t row_index) { + uint8_t row_size = 0; + + switch(row_index + 1) { + case 1: + row_size = sizeof(keyboard_keys_row_1) / sizeof(UART_TextInputKey); + break; + case 2: + row_size = sizeof(keyboard_keys_row_2) / sizeof(UART_TextInputKey); + break; + case 3: + row_size = sizeof(keyboard_keys_row_3) / sizeof(UART_TextInputKey); + break; + case 4: + row_size = sizeof(keyboard_keys_row_4) / sizeof(UART_TextInputKey); + break; + } + + return row_size; +} + +static const UART_TextInputKey* get_row(uint8_t row_index) { + const UART_TextInputKey* row = NULL; + + switch(row_index + 1) { + case 1: + row = keyboard_keys_row_1; + break; + case 2: + row = keyboard_keys_row_2; + break; + case 3: + row = keyboard_keys_row_3; + break; + case 4: + row = keyboard_keys_row_4; + break; + } + + return row; +} + +static char get_selected_char(UART_TextInputModel* model) { + return get_row(model->selected_row)[model->selected_column].text; +} + +static bool char_is_lowercase(char letter) { + return (letter >= 0x61 && letter <= 0x7A); +} + +static char char_to_uppercase(const char letter) { + switch(letter) { + case '_': + return 0x20; + break; + case '(': + return 0x29; + break; + case '{': + return 0x7d; + break; + case '[': + return 0x5d; + break; + case '/': + return 0x5c; + break; + case ';': + return 0x3a; + break; + case '.': + return 0x2c; + break; + case '!': + return 0x3f; + break; + case '<': + return 0x3e; + break; + } + if(isalpha(letter)) { + return (letter - 0x20); + } else { + return letter; + } +} + +static void uart_text_input_backspace_cb(UART_TextInputModel* model) { + uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer); + if(text_length > 0) { + model->text_buffer[text_length - 1] = 0; + } +} + +static void uart_text_input_view_draw_callback(Canvas* canvas, void* _model) { + UART_TextInputModel* model = _model; + uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0; + uint8_t needed_string_width = canvas_width(canvas) - 8; + uint8_t start_pos = 4; + + const char* text = model->text_buffer; + + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + + canvas_draw_str(canvas, 2, 7, model->header); + elements_slightly_rounded_frame(canvas, 1, 8, 126, 12); + + if(canvas_string_width(canvas, text) > needed_string_width) { + canvas_draw_str(canvas, start_pos, 17, "..."); + start_pos += 6; + needed_string_width -= 8; + } + + while(text != 0 && canvas_string_width(canvas, text) > needed_string_width) { + text++; + } + + if(model->clear_default_text) { + elements_slightly_rounded_box( + canvas, start_pos - 1, 14, canvas_string_width(canvas, text) + 2, 10); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 1, 18, "|"); + canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 2, 18, "|"); + } + canvas_draw_str(canvas, start_pos, 17, text); + + canvas_set_font(canvas, FontKeyboard); + + for(uint8_t row = 0; row <= keyboard_row_count; row++) { + const uint8_t column_count = get_row_size(row); + const UART_TextInputKey* keys = get_row(row); + + for(size_t column = 0; column < column_count; column++) { + if(keys[column].text == ENTER_KEY) { + canvas_set_color(canvas, ColorBlack); + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySaveSelected_24x11); + } else { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySave_24x11); + } + } else if(keys[column].text == BACKSPACE_KEY) { + canvas_set_color(canvas, ColorBlack); + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeyBackspaceSelected_16x9); + } else { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeyBackspace_16x9); + } + } else { + if(model->selected_row == row && model->selected_column == column) { + canvas_set_color(canvas, ColorBlack); + canvas_draw_box( + canvas, + keyboard_origin_x + keys[column].x - 1, + keyboard_origin_y + keys[column].y - 8, + 7, + 10); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_set_color(canvas, ColorBlack); + } + + if(model->clear_default_text || + (text_length == 0 && char_is_lowercase(keys[column].text))) { + canvas_draw_glyph( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + //char_to_uppercase(keys[column].text)); + keys[column].text); + } else { + canvas_draw_glyph( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + keys[column].text); + } + } + } + } + if(model->valadator_message_visible) { + canvas_set_font(canvas, FontSecondary); + canvas_set_color(canvas, ColorWhite); + canvas_draw_box(canvas, 8, 10, 110, 48); + canvas_set_color(canvas, ColorBlack); + canvas_draw_icon(canvas, 10, 14, &I_WarningDolphin_45x42); + canvas_draw_rframe(canvas, 8, 8, 112, 50, 3); + canvas_draw_rframe(canvas, 9, 9, 110, 48, 2); + elements_multiline_text(canvas, 62, 20, furi_string_get_cstr(model->validator_text)); + canvas_set_font(canvas, FontKeyboard); + } +} + +static void + uart_text_input_handle_up(UART_TextInput* uart_text_input, UART_TextInputModel* model) { + UNUSED(uart_text_input); + if(model->selected_row > 0) { + model->selected_row--; + if(model->selected_column > get_row_size(model->selected_row) - 6) { + model->selected_column = model->selected_column + 1; + } + } +} + +static void + uart_text_input_handle_down(UART_TextInput* uart_text_input, UART_TextInputModel* model) { + UNUSED(uart_text_input); + if(model->selected_row < keyboard_row_count - 1) { + model->selected_row++; + if(model->selected_column > get_row_size(model->selected_row) - 4) { + model->selected_column = model->selected_column - 1; + } + } +} + +static void + uart_text_input_handle_left(UART_TextInput* uart_text_input, UART_TextInputModel* model) { + UNUSED(uart_text_input); + if(model->selected_column > 0) { + model->selected_column--; + } else { + model->selected_column = get_row_size(model->selected_row) - 1; + } +} + +static void + uart_text_input_handle_right(UART_TextInput* uart_text_input, UART_TextInputModel* model) { + UNUSED(uart_text_input); + if(model->selected_column < get_row_size(model->selected_row) - 1) { + model->selected_column++; + } else { + model->selected_column = 0; + } +} + +static void uart_text_input_handle_ok( + UART_TextInput* uart_text_input, + UART_TextInputModel* model, + bool shift) { + char selected = get_selected_char(model); + uint8_t text_length = strlen(model->text_buffer); + + if(shift) { + selected = char_to_uppercase(selected); + } + + if(selected == ENTER_KEY) { + if(model->validator_callback && + (!model->validator_callback( + model->text_buffer, model->validator_text, model->validator_callback_context))) { + model->valadator_message_visible = true; + furi_timer_start(uart_text_input->timer, furi_kernel_get_tick_frequency() * 4); + } else if(model->callback != 0 && text_length > 0) { + model->callback(model->callback_context); + } + } else if(selected == BACKSPACE_KEY) { + uart_text_input_backspace_cb(model); + } else { + if(model->clear_default_text) { + text_length = 0; + } + if(text_length < (model->text_buffer_size - 1)) { + if(text_length == 0 && char_is_lowercase(selected)) { + //selected = char_to_uppercase(selected); + } + model->text_buffer[text_length] = selected; + model->text_buffer[text_length + 1] = 0; + } + } + model->clear_default_text = false; +} + +static bool uart_text_input_view_input_callback(InputEvent* event, void* context) { + UART_TextInput* uart_text_input = context; + furi_assert(uart_text_input); + + bool consumed = false; + + // Acquire model + UART_TextInputModel* model = view_get_model(uart_text_input->view); + + if((!(event->type == InputTypePress) && !(event->type == InputTypeRelease)) && + model->valadator_message_visible) { + model->valadator_message_visible = false; + consumed = true; + } else if(event->type == InputTypeShort) { + consumed = true; + switch(event->key) { + case InputKeyUp: + uart_text_input_handle_up(uart_text_input, model); + break; + case InputKeyDown: + uart_text_input_handle_down(uart_text_input, model); + break; + case InputKeyLeft: + uart_text_input_handle_left(uart_text_input, model); + break; + case InputKeyRight: + uart_text_input_handle_right(uart_text_input, model); + break; + case InputKeyOk: + uart_text_input_handle_ok(uart_text_input, model, false); + break; + default: + consumed = false; + break; + } + } else if(event->type == InputTypeLong) { + consumed = true; + switch(event->key) { + case InputKeyUp: + uart_text_input_handle_up(uart_text_input, model); + break; + case InputKeyDown: + uart_text_input_handle_down(uart_text_input, model); + break; + case InputKeyLeft: + uart_text_input_handle_left(uart_text_input, model); + break; + case InputKeyRight: + uart_text_input_handle_right(uart_text_input, model); + break; + case InputKeyOk: + uart_text_input_handle_ok(uart_text_input, model, true); + break; + case InputKeyBack: + uart_text_input_backspace_cb(model); + break; + default: + consumed = false; + break; + } + } else if(event->type == InputTypeRepeat) { + consumed = true; + switch(event->key) { + case InputKeyUp: + uart_text_input_handle_up(uart_text_input, model); + break; + case InputKeyDown: + uart_text_input_handle_down(uart_text_input, model); + break; + case InputKeyLeft: + uart_text_input_handle_left(uart_text_input, model); + break; + case InputKeyRight: + uart_text_input_handle_right(uart_text_input, model); + break; + case InputKeyBack: + uart_text_input_backspace_cb(model); + break; + default: + consumed = false; + break; + } + } + + // Commit model + view_commit_model(uart_text_input->view, consumed); + + return consumed; +} + +void uart_text_input_timer_callback(void* context) { + furi_assert(context); + UART_TextInput* uart_text_input = context; + + with_view_model( + uart_text_input->view, + UART_TextInputModel * model, + { model->valadator_message_visible = false; }, + true); +} + +UART_TextInput* uart_text_input_alloc() { + UART_TextInput* uart_text_input = malloc(sizeof(UART_TextInput)); + uart_text_input->view = view_alloc(); + view_set_context(uart_text_input->view, uart_text_input); + view_allocate_model(uart_text_input->view, ViewModelTypeLocking, sizeof(UART_TextInputModel)); + view_set_draw_callback(uart_text_input->view, uart_text_input_view_draw_callback); + view_set_input_callback(uart_text_input->view, uart_text_input_view_input_callback); + + uart_text_input->timer = + furi_timer_alloc(uart_text_input_timer_callback, FuriTimerTypeOnce, uart_text_input); + + with_view_model( + uart_text_input->view, + UART_TextInputModel * model, + { model->validator_text = furi_string_alloc(); }, + false); + + uart_text_input_reset(uart_text_input); + + return uart_text_input; +} + +void uart_text_input_free(UART_TextInput* uart_text_input) { + furi_assert(uart_text_input); + with_view_model( + uart_text_input->view, + UART_TextInputModel * model, + { furi_string_free(model->validator_text); }, + false); + + // Send stop command + furi_timer_stop(uart_text_input->timer); + // Release allocated memory + furi_timer_free(uart_text_input->timer); + + view_free(uart_text_input->view); + + free(uart_text_input); +} + +void uart_text_input_reset(UART_TextInput* uart_text_input) { + furi_assert(uart_text_input); + with_view_model( + uart_text_input->view, + UART_TextInputModel * model, + { + model->text_buffer_size = 0; + model->header = ""; + model->selected_row = 0; + model->selected_column = 0; + model->clear_default_text = false; + model->text_buffer = NULL; + model->text_buffer_size = 0; + model->callback = NULL; + model->callback_context = NULL; + model->validator_callback = NULL; + model->validator_callback_context = NULL; + furi_string_reset(model->validator_text); + model->valadator_message_visible = false; + }, + true); +} + +View* uart_text_input_get_view(UART_TextInput* uart_text_input) { + furi_assert(uart_text_input); + return uart_text_input->view; +} + +void uart_text_input_set_result_callback( + UART_TextInput* uart_text_input, + UART_TextInputCallback callback, + void* callback_context, + char* text_buffer, + size_t text_buffer_size, + bool clear_default_text) { + with_view_model( + uart_text_input->view, + UART_TextInputModel * model, + { + model->callback = callback; + model->callback_context = callback_context; + model->text_buffer = text_buffer; + model->text_buffer_size = text_buffer_size; + model->clear_default_text = clear_default_text; + if(text_buffer && text_buffer[0] != '\0') { + // Set focus on Save + model->selected_row = 2; + model->selected_column = 8; + } + }, + true); +} + +void uart_text_input_set_validator( + UART_TextInput* uart_text_input, + UART_TextInputValidatorCallback callback, + void* callback_context) { + with_view_model( + uart_text_input->view, + UART_TextInputModel * model, + { + model->validator_callback = callback; + model->validator_callback_context = callback_context; + }, + true); +} + +UART_TextInputValidatorCallback + uart_text_input_get_validator_callback(UART_TextInput* uart_text_input) { + UART_TextInputValidatorCallback validator_callback = NULL; + with_view_model( + uart_text_input->view, + UART_TextInputModel * model, + { validator_callback = model->validator_callback; }, + false); + return validator_callback; +} + +void* uart_text_input_get_validator_callback_context(UART_TextInput* uart_text_input) { + void* validator_callback_context = NULL; + with_view_model( + uart_text_input->view, + UART_TextInputModel * model, + { validator_callback_context = model->validator_callback_context; }, + false); + return validator_callback_context; +} + +void uart_text_input_set_header_text(UART_TextInput* uart_text_input, const char* text) { + with_view_model( + uart_text_input->view, UART_TextInputModel * model, { model->header = text; }, true); +} diff --git a/applications/external/esp32cam_morseflasher/uart_text_input.h b/applications/external/esp32cam_morseflasher/uart_text_input.h new file mode 100644 index 000000000..f3703ed5a --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_text_input.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include "uart_validators.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Text input anonymous structure */ +typedef struct UART_TextInput UART_TextInput; +typedef void (*UART_TextInputCallback)(void* context); +typedef bool (*UART_TextInputValidatorCallback)(const char* text, FuriString* error, void* context); + +/** Allocate and initialize text input + * + * This text input is used to enter string + * + * @return UART_TextInput instance + */ +UART_TextInput* uart_text_input_alloc(); + +/** Deinitialize and free text input + * + * @param uart_text_input UART_TextInput instance + */ +void uart_text_input_free(UART_TextInput* uart_text_input); + +/** Clean text input view Note: this function does not free memory + * + * @param uart_text_input Text input instance + */ +void uart_text_input_reset(UART_TextInput* uart_text_input); + +/** Get text input view + * + * @param uart_text_input UART_TextInput instance + * + * @return View instance that can be used for embedding + */ +View* uart_text_input_get_view(UART_TextInput* uart_text_input); + +/** Set text input result callback + * + * @param uart_text_input UART_TextInput instance + * @param callback callback fn + * @param callback_context callback context + * @param text_buffer pointer to YOUR text buffer, that we going + * to modify + * @param text_buffer_size YOUR text buffer size in bytes. Max string + * length will be text_buffer_size-1. + * @param clear_default_text clear text from text_buffer on first OK + * event + */ +void uart_text_input_set_result_callback( + UART_TextInput* uart_text_input, + UART_TextInputCallback callback, + void* callback_context, + char* text_buffer, + size_t text_buffer_size, + bool clear_default_text); + +void uart_text_input_set_validator( + UART_TextInput* uart_text_input, + UART_TextInputValidatorCallback callback, + void* callback_context); + +UART_TextInputValidatorCallback + uart_text_input_get_validator_callback(UART_TextInput* uart_text_input); + +void* uart_text_input_get_validator_callback_context(UART_TextInput* uart_text_input); + +/** Set text input header text + * + * @param uart_text_input UART_TextInput instance + * @param text text to be shown + */ +void uart_text_input_set_header_text(UART_TextInput* uart_text_input, const char* text); + +#ifdef __cplusplus +} +#endif diff --git a/applications/external/esp32cam_morseflasher/uart_validators.c b/applications/external/esp32cam_morseflasher/uart_validators.c new file mode 100644 index 000000000..c87a6cc6e --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_validators.c @@ -0,0 +1,57 @@ +#include +#include "uart_validators.h" +#include + +struct ValidatorIsFile { + char* app_path_folder; + const char* app_extension; + char* current_name; +}; + +bool validator_is_file_callback(const char* text, FuriString* error, void* context) { + furi_assert(context); + ValidatorIsFile* instance = context; + + if(instance->current_name != NULL) { + if(strcmp(instance->current_name, text) == 0) { + return true; + } + } + + bool ret = true; + FuriString* path = furi_string_alloc_printf( + "%s/%s%s", instance->app_path_folder, text, instance->app_extension); + Storage* storage = furi_record_open(RECORD_STORAGE); + if(storage_common_stat(storage, furi_string_get_cstr(path), NULL) == FSE_OK) { + ret = false; + furi_string_printf(error, "This name\nexists!\nChoose\nanother one."); + } else { + ret = true; + } + furi_string_free(path); + furi_record_close(RECORD_STORAGE); + + return ret; +} + +ValidatorIsFile* validator_is_file_alloc_init( + const char* app_path_folder, + const char* app_extension, + const char* current_name) { + ValidatorIsFile* instance = malloc(sizeof(ValidatorIsFile)); + + instance->app_path_folder = strdup(app_path_folder); + instance->app_extension = app_extension; + if(current_name != NULL) { + instance->current_name = strdup(current_name); + } + + return instance; +} + +void validator_is_file_free(ValidatorIsFile* instance) { + furi_assert(instance); + free(instance->app_path_folder); + free(instance->current_name); + free(instance); +} diff --git a/applications/external/esp32cam_morseflasher/uart_validators.h b/applications/external/esp32cam_morseflasher/uart_validators.h new file mode 100644 index 000000000..d9200b6db --- /dev/null +++ b/applications/external/esp32cam_morseflasher/uart_validators.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif +typedef struct ValidatorIsFile ValidatorIsFile; + +ValidatorIsFile* validator_is_file_alloc_init( + const char* app_path_folder, + const char* app_extension, + const char* current_name); + +void validator_is_file_free(ValidatorIsFile* instance); + +bool validator_is_file_callback(const char* text, FuriString* error, void* context); + +#ifdef __cplusplus +} +#endif diff --git a/applications/external/esp32cam_motion_detection/application.fam b/applications/external/esp32cam_motion_detection/application.fam new file mode 100644 index 000000000..e596e6b71 --- /dev/null +++ b/applications/external/esp32cam_motion_detection/application.fam @@ -0,0 +1,15 @@ +App( + appid="MAYHEM_Motion", + name="[MAYHEM] Motion detection", + apptype=FlipperAppType.EXTERNAL, + entry_point="uart_echo_app", + cdefines=["APP_QRCODE"], + requires=["gui"], + stack_size=8*1024, + order=1, + fap_icon="icon.png", + fap_category="GPIO", + fap_description="ESP32-CAM Motion detection. It generates a beep when motion is detected. Can be extended to trigger more stuff in the code. [Unplug the USB cable to test with Mayhem]", + fap_author="eried", + fap_weburl="https://flipper.ried.cl" +) diff --git a/applications/external/esp32cam_motion_detection/icon.png b/applications/external/esp32cam_motion_detection/icon.png new file mode 100644 index 000000000..3f112f1c5 Binary files /dev/null and b/applications/external/esp32cam_motion_detection/icon.png differ diff --git a/applications/external/esp32cam_motion_detection/uart_echo.c b/applications/external/esp32cam_motion_detection/uart_echo.c new file mode 100644 index 000000000..e1b5bcb2d --- /dev/null +++ b/applications/external/esp32cam_motion_detection/uart_echo.c @@ -0,0 +1,241 @@ +#include "uart_echo.h" + +static void uart_echo_view_draw_callback(Canvas* canvas, void* _model) { + UartDumpModel* model = _model; + + // Prepare canvas + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontKeyboard); + + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + canvas_draw_str( + canvas, + 0, + (i + 1) * (canvas_current_font_height(canvas) - 1), + furi_string_get_cstr(model->list[i]->text)); + + if(i == model->line) { + uint8_t width = + canvas_string_width(canvas, furi_string_get_cstr(model->list[i]->text)); + + canvas_draw_box( + canvas, + width, + (i) * (canvas_current_font_height(canvas) - 1) + 2, + 2, + canvas_current_font_height(canvas) - 2); + } + } +} + +static bool uart_echo_view_input_callback(InputEvent* event, void* context) { + UNUSED(event); + UNUSED(context); + return false; +} + +static uint32_t uart_echo_exit(void* context) { + UNUSED(context); + return VIEW_NONE; +} + +static void uart_echo_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 uart_echo_push_to_list(UartDumpModel* model, void* context, const char data) { + // Alarm sound + if(data == '!'){ + UartEchoApp* app = context; + notification_message(app->notification, &sequence_alarm); + } + + if(model->escape) { + // escape code end with letter + if((data >= 'a' && data <= 'z') || (data >= 'A' && data <= 'Z')) { + model->escape = false; + } + } else if(data == '[' && model->last_char == '\e') { + // "Esc[" is a escape code + model->escape = true; + } else if((data >= ' ' && data <= '~') || (data == '\n' || data == '\r')) { + bool new_string_needed = false; + if(furi_string_size(model->list[model->line]->text) >= COLUMNS_ON_SCREEN) { + new_string_needed = true; + } else if((data == '\n' || data == '\r')) { + // pack line breaks + if(model->last_char != '\n' && model->last_char != '\r') { + new_string_needed = true; + } + } + + if(new_string_needed) { + if((model->line + 1) < LINES_ON_SCREEN) { + model->line += 1; + } else { + ListElement* first = model->list[0]; + + for(size_t i = 1; i < LINES_ON_SCREEN; i++) { + model->list[i - 1] = model->list[i]; + } + + furi_string_reset(first->text); + model->list[model->line] = first; + } + } + + if(data != '\n' && data != '\r') { + furi_string_push_back(model->list[model->line]->text, data); + } + } + model->last_char = data; +} + +static int32_t uart_echo_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 { + uint8_t data[64]; + length = furi_stream_buffer_receive(app->rx_stream, data, 64, 0); + if(length > 0 && app->initialized) { + furi_hal_uart_tx(FuriHalUartIdUSART1, data, length); + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < length; i++) { + uart_echo_push_to_list(model, app, 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* uart_echo_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_draw_callback(app->view, uart_echo_view_draw_callback); + view_set_input_callback(app->view, uart_echo_view_input_callback); + view_allocate_model(app->view, ViewModelTypeLocking, sizeof(UartDumpModel)); + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + model->line = 0; + model->escape = false; + model->list[i] = malloc(sizeof(ListElement)); + model->list[i]->text = furi_string_alloc(); + } + }, + true); + + view_set_previous_callback(app->view, uart_echo_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", 1024, uart_echo_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, uart_echo_on_irq_cb, app); + + furi_hal_power_disable_external_3_3v(); + furi_hal_power_disable_otg(); + furi_delay_ms(200); + furi_hal_power_enable_external_3_3v(); + furi_hal_power_enable_otg(); + for(int i=0;i<2;i++) + { + furi_delay_ms(500); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[1]){'m'}, 1); + } + furi_delay_ms(1); + app->initialized = true; + return app; +} + +static void uart_echo_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); + + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + furi_string_free(model->list[i]->text); + free(model->list[i]); + } + }, + true); + 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 uart_echo_app(void* p) { + UNUSED(p); + UartEchoApp* app = uart_echo_app_alloc(); + view_dispatcher_run(app->view_dispatcher); + uart_echo_app_free(app); + furi_hal_power_disable_otg(); + return 0; +} diff --git a/applications/external/esp32cam_motion_detection/uart_echo.h b/applications/external/esp32cam_motion_detection/uart_echo.h new file mode 100644 index 000000000..70ed18af0 --- /dev/null +++ b/applications/external/esp32cam_motion_detection/uart_echo.h @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LINES_ON_SCREEN 6 +#define COLUMNS_ON_SCREEN 21 + +static const NotificationSequence sequence_alarm = { + &message_display_backlight_on, + &message_red_255, + //&message_vibro_on, + &message_note_d5, + &message_delay_100, + //&message_vibro_off, + &message_sound_off, + &message_note_b4, + &message_delay_50, + &message_sound_off, + &message_display_backlight_off, + //&message_red_0, + //&message_delay_50, + NULL, +}; + +typedef struct UartDumpModel UartDumpModel; + +typedef struct { + Gui* gui; + NotificationApp* notification; + ViewDispatcher* view_dispatcher; + View* view; + FuriThread* worker_thread; + FuriStreamBuffer* rx_stream; + bool initialized; +} UartEchoApp; + +typedef struct { + FuriString* text; +} ListElement; + +struct UartDumpModel { + ListElement* list[LINES_ON_SCREEN]; + uint8_t line; + + char last_char; + bool escape; +}; + +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_green_255, + &message_delay_10, + NULL, +};*/ diff --git a/applications/external/esp32cam_nannycam/application.fam b/applications/external/esp32cam_nannycam/application.fam new file mode 100644 index 000000000..4f12fde05 --- /dev/null +++ b/applications/external/esp32cam_nannycam/application.fam @@ -0,0 +1,15 @@ +App( + appid="MAYHEM_NannyCam", + name="[MAYHEM] Nanny Cam", + apptype=FlipperAppType.EXTERNAL, + entry_point="uart_echo_app", + cdefines=["APP_QRCODE"], + requires=["gui"], + stack_size=8*1024, + order=1, + fap_icon="icon.png", + fap_category="GPIO", + fap_description="ESP32-CAM simple app to start a remote camera. [Unplug the USB cable to test with Mayhem]", + fap_author="eried", + fap_weburl="https://flipper.ried.cl" +) diff --git a/applications/external/esp32cam_nannycam/icon.png b/applications/external/esp32cam_nannycam/icon.png new file mode 100644 index 000000000..a6439434d Binary files /dev/null and b/applications/external/esp32cam_nannycam/icon.png differ diff --git a/applications/external/esp32cam_nannycam/uart_echo.c b/applications/external/esp32cam_nannycam/uart_echo.c new file mode 100644 index 000000000..8a59d38f9 --- /dev/null +++ b/applications/external/esp32cam_nannycam/uart_echo.c @@ -0,0 +1,235 @@ +#include "uart_echo.h" + +static void uart_echo_view_draw_callback(Canvas* canvas, void* _model) { + UartDumpModel* model = _model; + + // Prepare canvas + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontKeyboard); + + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + canvas_draw_str( + canvas, + 0, + (i + 1) * (canvas_current_font_height(canvas) - 1), + furi_string_get_cstr(model->list[i]->text)); + + if(i == model->line) { + uint8_t width = + canvas_string_width(canvas, furi_string_get_cstr(model->list[i]->text)); + + canvas_draw_box( + canvas, + width, + (i) * (canvas_current_font_height(canvas) - 1) + 2, + 2, + canvas_current_font_height(canvas) - 2); + } + } +} + +static bool uart_echo_view_input_callback(InputEvent* event, void* context) { + UNUSED(event); + UNUSED(context); + return false; +} + +static uint32_t uart_echo_exit(void* context) { + UNUSED(context); + return VIEW_NONE; +} + +static void uart_echo_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 uart_echo_push_to_list(UartDumpModel* model, const char data) { + if(model->escape) { + // escape code end with letter + if((data >= 'a' && data <= 'z') || (data >= 'A' && data <= 'Z')) { + model->escape = false; + } + } else if(data == '[' && model->last_char == '\e') { + // "Esc[" is a escape code + model->escape = true; + } else if((data >= ' ' && data <= '~') || (data == '\n' || data == '\r')) { + bool new_string_needed = false; + if(furi_string_size(model->list[model->line]->text) >= COLUMNS_ON_SCREEN) { + new_string_needed = true; + } else if((data == '\n' || data == '\r')) { + // pack line breaks + if(model->last_char != '\n' && model->last_char != '\r') { + new_string_needed = true; + } + } + + if(new_string_needed) { + if((model->line + 1) < LINES_ON_SCREEN) { + model->line += 1; + } else { + ListElement* first = model->list[0]; + + for(size_t i = 1; i < LINES_ON_SCREEN; i++) { + model->list[i - 1] = model->list[i]; + } + + furi_string_reset(first->text); + model->list[model->line] = first; + } + } + + if(data != '\n' && data != '\r') { + furi_string_push_back(model->list[model->line]->text, data); + } + } + model->last_char = data; +} + +static int32_t uart_echo_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 { + uint8_t data[64]; + length = furi_stream_buffer_receive(app->rx_stream, data, 64, 0); + if(length > 0 && app->initialized) { + furi_hal_uart_tx(FuriHalUartIdUSART1, data, length); + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < length; i++) { + uart_echo_push_to_list(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* uart_echo_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_draw_callback(app->view, uart_echo_view_draw_callback); + view_set_input_callback(app->view, uart_echo_view_input_callback); + view_allocate_model(app->view, ViewModelTypeLocking, sizeof(UartDumpModel)); + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + model->line = 0; + model->escape = false; + model->list[i] = malloc(sizeof(ListElement)); + model->list[i]->text = furi_string_alloc(); + } + }, + true); + + view_set_previous_callback(app->view, uart_echo_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", 1024, uart_echo_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, uart_echo_on_irq_cb, app); + + furi_hal_power_disable_external_3_3v(); + furi_hal_power_disable_otg(); + furi_delay_ms(200); + furi_hal_power_enable_external_3_3v(); + furi_hal_power_enable_otg(); + for(int i=0;i<2;i++) + { + furi_delay_ms(500); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[1]){'n'}, 1); + } + furi_delay_ms(1); + app->initialized = true; + return app; +} + +static void uart_echo_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); + + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + furi_string_free(model->list[i]->text); + free(model->list[i]); + } + }, + true); + 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 uart_echo_app(void* p) { + UNUSED(p); + UartEchoApp* app = uart_echo_app_alloc(); + view_dispatcher_run(app->view_dispatcher); + uart_echo_app_free(app); + furi_hal_power_disable_otg(); + return 0; +} diff --git a/applications/external/esp32cam_nannycam/uart_echo.h b/applications/external/esp32cam_nannycam/uart_echo.h new file mode 100644 index 000000000..98eefd090 --- /dev/null +++ b/applications/external/esp32cam_nannycam/uart_echo.h @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LINES_ON_SCREEN 6 +#define COLUMNS_ON_SCREEN 21 + +typedef struct UartDumpModel UartDumpModel; + +typedef struct { + Gui* gui; + NotificationApp* notification; + ViewDispatcher* view_dispatcher; + View* view; + FuriThread* worker_thread; + FuriStreamBuffer* rx_stream; + bool initialized; +} UartEchoApp; + +typedef struct { + FuriString* text; +} ListElement; + +struct UartDumpModel { + ListElement* list[LINES_ON_SCREEN]; + uint8_t line; + + char last_char; + bool escape; +}; + +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_green_255, + &message_delay_10, + NULL, +}; diff --git a/applications/external/esp32cam_qrcode/application.fam b/applications/external/esp32cam_qrcode/application.fam new file mode 100644 index 000000000..116ec195e --- /dev/null +++ b/applications/external/esp32cam_qrcode/application.fam @@ -0,0 +1,15 @@ +App( + appid="MAYHEM_QRcode", + name="[MAYHEM] QR Code", + apptype=FlipperAppType.EXTERNAL, + entry_point="uart_echo_app", + cdefines=["APP_QRCODE"], + requires=["gui"], + stack_size=8*1024, + order=1, + fap_icon="icon.png", + fap_category="GPIO", + fap_description="ESP32-CAM simple app to show a payload from QR codes. Can be extended to trigger more stuff in the code. [Unplug the USB cable to test with Mayhem]", + fap_author="eried", + fap_weburl="https://flipper.ried.cl" +) diff --git a/applications/external/esp32cam_qrcode/icon.png b/applications/external/esp32cam_qrcode/icon.png new file mode 100644 index 000000000..23d614cab Binary files /dev/null and b/applications/external/esp32cam_qrcode/icon.png differ diff --git a/applications/external/esp32cam_qrcode/uart_echo.c b/applications/external/esp32cam_qrcode/uart_echo.c new file mode 100644 index 000000000..3514a4d37 --- /dev/null +++ b/applications/external/esp32cam_qrcode/uart_echo.c @@ -0,0 +1,235 @@ +#include "uart_echo.h" + +static void uart_echo_view_draw_callback(Canvas* canvas, void* _model) { + UartDumpModel* model = _model; + + // Prepare canvas + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontKeyboard); + + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + canvas_draw_str( + canvas, + 0, + (i + 1) * (canvas_current_font_height(canvas) - 1), + furi_string_get_cstr(model->list[i]->text)); + + if(i == model->line) { + uint8_t width = + canvas_string_width(canvas, furi_string_get_cstr(model->list[i]->text)); + + canvas_draw_box( + canvas, + width, + (i) * (canvas_current_font_height(canvas) - 1) + 2, + 2, + canvas_current_font_height(canvas) - 2); + } + } +} + +static bool uart_echo_view_input_callback(InputEvent* event, void* context) { + UNUSED(event); + UNUSED(context); + return false; +} + +static uint32_t uart_echo_exit(void* context) { + UNUSED(context); + return VIEW_NONE; +} + +static void uart_echo_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 uart_echo_push_to_list(UartDumpModel* model, const char data) { + if(model->escape) { + // escape code end with letter + if((data >= 'a' && data <= 'z') || (data >= 'A' && data <= 'Z')) { + model->escape = false; + } + } else if(data == '[' && model->last_char == '\e') { + // "Esc[" is a escape code + model->escape = true; + } else if((data >= ' ' && data <= '~') || (data == '\n' || data == '\r')) { + bool new_string_needed = false; + if(furi_string_size(model->list[model->line]->text) >= COLUMNS_ON_SCREEN) { + new_string_needed = true; + } else if((data == '\n' || data == '\r')) { + // pack line breaks + if(model->last_char != '\n' && model->last_char != '\r') { + new_string_needed = true; + } + } + + if(new_string_needed) { + if((model->line + 1) < LINES_ON_SCREEN) { + model->line += 1; + } else { + ListElement* first = model->list[0]; + + for(size_t i = 1; i < LINES_ON_SCREEN; i++) { + model->list[i - 1] = model->list[i]; + } + + furi_string_reset(first->text); + model->list[model->line] = first; + } + } + + if(data != '\n' && data != '\r') { + furi_string_push_back(model->list[model->line]->text, data); + } + } + model->last_char = data; +} + +static int32_t uart_echo_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 { + uint8_t data[64]; + length = furi_stream_buffer_receive(app->rx_stream, data, 64, 0); + if(length > 0 && app->initialized) { + furi_hal_uart_tx(FuriHalUartIdUSART1, data, length); + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < length; i++) { + uart_echo_push_to_list(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* uart_echo_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_draw_callback(app->view, uart_echo_view_draw_callback); + view_set_input_callback(app->view, uart_echo_view_input_callback); + view_allocate_model(app->view, ViewModelTypeLocking, sizeof(UartDumpModel)); + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + model->line = 0; + model->escape = false; + model->list[i] = malloc(sizeof(ListElement)); + model->list[i]->text = furi_string_alloc(); + } + }, + true); + + view_set_previous_callback(app->view, uart_echo_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", 1024, uart_echo_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, uart_echo_on_irq_cb, app); + + furi_hal_power_disable_external_3_3v(); + furi_hal_power_disable_otg(); + furi_delay_ms(200); + furi_hal_power_enable_external_3_3v(); + furi_hal_power_enable_otg(); + for(int i=0;i<2;i++) + { + furi_delay_ms(500); + furi_hal_uart_tx(FuriHalUartIdUSART1, (uint8_t[1]){'q'}, 1); + } + furi_delay_ms(1); + app->initialized = true; + return app; +} + +static void uart_echo_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); + + with_view_model( + app->view, + UartDumpModel * model, + { + for(size_t i = 0; i < LINES_ON_SCREEN; i++) { + furi_string_free(model->list[i]->text); + free(model->list[i]); + } + }, + true); + 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 uart_echo_app(void* p) { + UNUSED(p); + UartEchoApp* app = uart_echo_app_alloc(); + view_dispatcher_run(app->view_dispatcher); + uart_echo_app_free(app); + furi_hal_power_disable_otg(); + return 0; +} diff --git a/applications/external/esp32cam_qrcode/uart_echo.h b/applications/external/esp32cam_qrcode/uart_echo.h new file mode 100644 index 000000000..98eefd090 --- /dev/null +++ b/applications/external/esp32cam_qrcode/uart_echo.h @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LINES_ON_SCREEN 6 +#define COLUMNS_ON_SCREEN 21 + +typedef struct UartDumpModel UartDumpModel; + +typedef struct { + Gui* gui; + NotificationApp* notification; + ViewDispatcher* view_dispatcher; + View* view; + FuriThread* worker_thread; + FuriStreamBuffer* rx_stream; + bool initialized; +} UartEchoApp; + +typedef struct { + FuriString* text; +} ListElement; + +struct UartDumpModel { + ListElement* list[LINES_ON_SCREEN]; + uint8_t line; + + char last_char; + bool escape; +}; + +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_green_255, + &message_delay_10, + NULL, +};