diff --git a/subghz/apps/genie-recorder/README.md b/subghz/apps/genie-recorder/README.md new file mode 100644 index 0000000..81716e3 --- /dev/null +++ b/subghz/apps/genie-recorder/README.md @@ -0,0 +1,17 @@ +# Genie garage door recorder + +## Description +This program was written to allow the Flipper Zero to press buttons on a Genie garage door opened and record the rolling code. The goal is to capture all 65,536 signals (which hopefully repeats when it gets to the end). Our click speed is current 2000ms per click + however long it takes to get the signal. So if we assume it's 1000-1500/hr = about 3 days? + +## Running +Step 1. Deploy custom firmware (keeloq.c with te_short=200, te_long=400, te_delta=70) +Step 2. Connect a genie garage door opener to pins A7 and GND. +Step 3. Edit FREQUENCY if it is not broadcasting at 315MHz when button is held down. +Step 4. Run application +Step 5. Choose "Start" to start recording signals. +- You should see the current broadcast count (how many times the button was pressed) +- You should see the received signal count (how many times the signal was received) +- You should see the most recent Keeloq code received +Step 6. Let it run for a few weeks (the goal is to capture 65,536+ signals) +Step 7. Press the BACK button twice to exit the application. +Step 8. Copy the file "\apps_data\genie\keys.txt" from the SD card to your computer. \ No newline at end of file diff --git a/subghz/apps/genie-recorder/application.fam b/subghz/apps/genie-recorder/application.fam new file mode 100644 index 0000000..7b037f2 --- /dev/null +++ b/subghz/apps/genie-recorder/application.fam @@ -0,0 +1,13 @@ +App( + appid="genie_record", + name="Genie Door Recorder", + apptype=FlipperAppType.EXTERNAL, + entry_point="genie_record_app", + requires=["gui", "subghz"], + stack_size=2 * 1024, + fap_version=(1, 1), + fap_icon="genie.png", + fap_category="Sub-GHz", + fap_icon_assets="assets", + fap_description="Genie Door Signal w/Recorder", +) diff --git a/subghz/apps/genie-recorder/genie.c b/subghz/apps/genie-recorder/genie.c new file mode 100644 index 0000000..29556af --- /dev/null +++ b/subghz/apps/genie-recorder/genie.c @@ -0,0 +1,258 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "genie_subghz_receive.h" +#include "genie_file.h" + +#define TAG "GenieRecord" + +//#define FREQUENCY 390000000 +#define FREQUENCY 315000000 +#define CLICK_SPEED 2000 +const GpioPin* const pin_remote = &gpio_ext_pa7; + +typedef enum { + GenieSubmenuIndexStart, + GenieSubmenuIndexAbout, +} GenieSubmenuIndex; + +typedef enum { + GenieViewSubmenu, + GenieViewStart, + GenieViewAbout, +} GenieView; + +typedef struct { + ViewDispatcher* view_dispatcher; + Submenu* submenu; + View* view; + Widget* widget_about; + GenieSubGhz* genie_subghz; + FuriTimer* timer; + bool processing; + bool pressed; + uint32_t counter; + uint32_t save_counter; + FuriString* key; +} GenieApp; + +typedef struct { + GenieApp* ref; +} GenieAppRef; + +/** + * @brief Callback for navigation events + * @details This function is called when user press back button. We return VIEW_NONE to + * indicate that we want to exit the application. + * @param context The context + * @return next view id +*/ +uint32_t genie_navigation_exit_callback(void* context) { + UNUSED(context); + return VIEW_NONE; +} + +/** + * @brief Callback for navigation events + * @details This function is called when user press back button. We return VIEW_NONE to + * indicate that we want to exit the application. + * @param context The context + * @return next view id +*/ +uint32_t genie_navigation_submenu_callback(void* context) { + UNUSED(context); + return GenieViewSubmenu; +} + +void genie_submenu_callback(void* context, uint32_t index) { + GenieApp* app = (GenieApp*)context; + switch(index) { + case GenieSubmenuIndexStart: + view_dispatcher_switch_to_view(app->view_dispatcher, GenieViewStart); + break; + case GenieSubmenuIndexAbout: + view_dispatcher_switch_to_view(app->view_dispatcher, GenieViewAbout); + break; + default: + break; + } +} + +void genie_view_draw_callback(Canvas* canvas, void* model) { + GenieApp* app = ((GenieAppRef*)model)->ref; + canvas_draw_str(canvas, 5, 15, "Genie Sub-Ghz Recorder!!!"); + canvas_draw_str(canvas, 5, 30, "Make sure A7 is connected"); + canvas_draw_str(canvas, 100, 58, (app->pressed) ? "SEND" : ""); + FuriString* buffer = furi_string_alloc(); + furi_string_printf(buffer, "%ld", app->counter); + canvas_draw_str(canvas, 50, 45, furi_string_get_cstr(buffer)); + furi_string_printf(buffer, "%ld", app->save_counter); + canvas_draw_str(canvas, 96, 45, furi_string_get_cstr(buffer)); + canvas_draw_str(canvas, 5, 55, furi_string_get_cstr(app->key)); + furi_string_free(buffer); +} + +bool genie_view_input_callback(InputEvent* event, void* context) { + UNUSED(context); + UNUSED(event); + return false; +} + +uint32_t last_count() { + uint32_t count = genie_load(); + return count; +} + +void save_count(uint32_t count, FuriString* key) { + FURI_LOG_I(TAG, "%ld,%s", count, furi_string_get_cstr(key)); + genie_save(count, key); +} + +void __gui_redraw() { + // Redraw screen + Gui* gui = furi_record_open(RECORD_GUI); + gui_direct_draw_acquire(gui); + gui_direct_draw_release(gui); +} + +void press_button(GenieApp* app) { + furi_hal_gpio_write(pin_remote, false); + app->pressed = true; + __gui_redraw(); +} + +void release_button(GenieApp* app) { + furi_hal_gpio_write(pin_remote, true); + app->pressed = false; + __gui_redraw(); +} + +void genie_packet(FuriString* buffer, void* context) { + GenieApp* app = (GenieApp*)context; + app->processing = true; + release_button(app); + + FURI_LOG_I(TAG, "PACKET:\r\n%s", furi_string_get_cstr(buffer)); + if(furi_string_search_str(buffer, "KeeLoq 64bit") < furi_string_size(buffer)) { + FURI_LOG_I(TAG, "KeeLoq 64bit found"); + size_t key_index = furi_string_search_str(buffer, "Key:"); + if(key_index < furi_string_size(buffer)) { + FURI_LOG_I(TAG, "Key found"); + furi_string_set_n(app->key, buffer, key_index + 4, 16); + app->save_counter++; + save_count(app->counter, app->key); + } + } + + app->processing = false; +} + +void genie_tick(void* context) { + GenieApp* app = (GenieApp*)context; + if(!app->processing) { + if(app->pressed) { + release_button(app); + } else { + app->counter++; + press_button(app); + } + } +} + +void genie_enter_callback(void* context) { + GenieApp* app = (GenieApp*)context; + start_listening(app->genie_subghz, FREQUENCY, genie_packet, context); + furi_timer_start(app->timer, furi_ms_to_ticks(CLICK_SPEED)); +} + +void genie_exit_callback(void* context) { + GenieApp* app = (GenieApp*)context; + app->processing = false; + release_button(app); + stop_listening(app->genie_subghz); + furi_timer_stop(app->timer); +} + +GenieApp* genie_app_alloc() { + GenieApp* app = (GenieApp*)malloc(sizeof(GenieApp)); + app->genie_subghz = genie_subghz_alloc(); + app->timer = furi_timer_alloc(genie_tick, FuriTimerTypePeriodic, app); + app->counter = last_count(); + app->save_counter = app->counter; + app->key = furi_string_alloc(); + + furi_hal_gpio_init_simple(pin_remote, GpioModeOutputOpenDrain); + release_button(app); + + Gui* gui = furi_record_open(RECORD_GUI); + + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + + app->submenu = submenu_alloc(); + submenu_add_item(app->submenu, "Start", GenieSubmenuIndexStart, genie_submenu_callback, app); + submenu_add_item(app->submenu, "About", GenieSubmenuIndexAbout, genie_submenu_callback, app); + view_set_previous_callback(submenu_get_view(app->submenu), genie_navigation_exit_callback); + view_dispatcher_add_view( + app->view_dispatcher, GenieViewSubmenu, submenu_get_view(app->submenu)); + view_dispatcher_switch_to_view(app->view_dispatcher, GenieViewSubmenu); + + app->view = view_alloc(); + view_set_draw_callback(app->view, genie_view_draw_callback); + view_set_input_callback(app->view, genie_view_input_callback); + view_set_previous_callback(app->view, genie_navigation_submenu_callback); + view_set_context(app->view, app); + view_set_enter_callback(app->view, genie_enter_callback); + view_set_exit_callback(app->view, genie_exit_callback); + view_dispatcher_add_view(app->view_dispatcher, GenieViewStart, app->view); + view_allocate_model(app->view, ViewModelTypeLockFree, sizeof(GenieAppRef)); + GenieAppRef* r = (GenieAppRef*)view_get_model(app->view); + r->ref = app; + + app->widget_about = widget_alloc(); + widget_add_text_scroll_element( + app->widget_about, + 0, + 0, + 128, + 64, + "Genie garage door recorder.\n---\nConnect door to pin A7!\n\nauthor: @codeallnight\nhttps://discord.com/invite/NsjCvqwPAd\nhttps://youtube.com/@MrDerekJamison"); + view_set_previous_callback( + widget_get_view(app->widget_about), genie_navigation_submenu_callback); + view_dispatcher_add_view( + app->view_dispatcher, GenieViewAbout, widget_get_view(app->widget_about)); + + return app; +} + +void genie_app_free(GenieApp* app) { + genie_subghz_free(app->genie_subghz); + furi_timer_free(app->timer); + furi_hal_gpio_init_simple(pin_remote, GpioModeAnalog); + furi_string_free(app->key); + + view_dispatcher_remove_view(app->view_dispatcher, GenieViewAbout); + widget_free(app->widget_about); + view_dispatcher_remove_view(app->view_dispatcher, GenieViewStart); + view_free(app->view); + view_dispatcher_remove_view(app->view_dispatcher, GenieViewSubmenu); + submenu_free(app->submenu); + view_dispatcher_free(app->view_dispatcher); + furi_record_close(RECORD_GUI); + + free(app); +} + +int32_t genie_record_app(void* p) { + UNUSED(p); + GenieApp* app = genie_app_alloc(); + view_dispatcher_run(app->view_dispatcher); + genie_app_free(app); + return 0; +} \ No newline at end of file diff --git a/subghz/apps/genie-recorder/genie.png b/subghz/apps/genie-recorder/genie.png new file mode 100644 index 0000000..71eb2be Binary files /dev/null and b/subghz/apps/genie-recorder/genie.png differ diff --git a/subghz/apps/genie-recorder/genie_file.c b/subghz/apps/genie-recorder/genie_file.c new file mode 100644 index 0000000..567b91a --- /dev/null +++ b/subghz/apps/genie-recorder/genie_file.c @@ -0,0 +1,115 @@ +#include +#include + +#define GENIE_APPS_DATA_FOLDER EXT_PATH("apps_data") +#define GENIE_SAVE_FOLDER \ + GENIE_APPS_DATA_FOLDER "/" \ + "genie" +#define GENIE_SAVE_NAME "keys" +#define GENIE_SAVE_EXTENSION ".txt" + +#ifdef TAG +#undef TAG +#endif +#define TAG "GenieFile" + +static void ensure_dir_exists(Storage* storage, char* dir) { + if(!storage_dir_exists(storage, dir)) { + FURI_LOG_I(TAG, "Creating directory: %s", dir); + storage_simply_mkdir(storage, dir); + } else { + FURI_LOG_D(TAG, "Directory exists: %s", dir); + } +} + +static void ensure_save_folder_exists(Storage* storage) { + ensure_dir_exists(storage, GENIE_APPS_DATA_FOLDER); + ensure_dir_exists(storage, GENIE_SAVE_FOLDER); +} + +bool genie_save(uint32_t count, FuriString* key) { + bool success = false; + Storage* storage = furi_record_open(RECORD_STORAGE); + FuriString* file_path = furi_string_alloc(); + + FuriString* buffer = furi_string_alloc(); + furi_string_printf(buffer, "%08lX,%s\r\n", count, furi_string_get_cstr(key)); + + furi_string_printf( + file_path, "%s/%s%s", GENIE_SAVE_FOLDER, GENIE_SAVE_NAME, GENIE_SAVE_EXTENSION); + + File* file = NULL; + do { + if(!storage) { + FURI_LOG_E(TAG, "Failed to access storage"); + break; + } + + ensure_save_folder_exists(storage); + + file = storage_file_alloc(storage); + if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_WRITE, FSOM_OPEN_APPEND)) { + FURI_LOG_D(TAG, "Writing to file: %s", furi_string_get_cstr(file_path)); + + if(!storage_file_write(file, furi_string_get_cstr(buffer), furi_string_size(buffer))) { + FURI_LOG_E(TAG, "Failed to write to file"); + break; + } + + success = true; + } + + } while(false); + + if(file) { + storage_file_close(file); + } + + furi_string_free(file_path); + furi_string_free(buffer); + furi_record_close(RECORD_STORAGE); + return success; +} + +uint32_t genie_load() { + uint32_t count = 0; + Storage* storage = furi_record_open(RECORD_STORAGE); + FuriString* file_path = furi_string_alloc(); + + FuriString* buffer = furi_string_alloc(); + + furi_string_printf( + file_path, "%s/%s%s", GENIE_SAVE_FOLDER, GENIE_SAVE_NAME, GENIE_SAVE_EXTENSION); + + File* file = NULL; + do { + if(!storage) { + FURI_LOG_E(TAG, "Failed to access storage"); + break; + } + + ensure_save_folder_exists(storage); + + file = storage_file_alloc(storage); + if(storage_file_open( + file, furi_string_get_cstr(file_path), FSAM_READ, FSOM_OPEN_EXISTING)) { + FURI_LOG_D(TAG, "Reading file: %s", furi_string_get_cstr(file_path)); + + char data[8 + 1 + 16 + 2 + 1] = {0}; + + while(storage_file_read(file, data, 8 + 1 + 16 + 2)) { + sscanf(data, "%08lX", &count); + FURI_LOG_D(TAG, "Read: %s, count: %ld", data, count); + } + } + } while(false); + + if(file) { + storage_file_close(file); + } + + furi_string_free(file_path); + furi_string_free(buffer); + furi_record_close(RECORD_STORAGE); + return count; +} \ No newline at end of file diff --git a/subghz/apps/genie-recorder/genie_file.h b/subghz/apps/genie-recorder/genie_file.h new file mode 100644 index 0000000..2384497 --- /dev/null +++ b/subghz/apps/genie-recorder/genie_file.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +bool genie_save(uint32_t count, FuriString* key); +uint32_t genie_load(); \ No newline at end of file diff --git a/subghz/apps/genie-recorder/genie_subghz_receive.c b/subghz/apps/genie-recorder/genie_subghz_receive.c new file mode 100644 index 0000000..3dea39f --- /dev/null +++ b/subghz/apps/genie-recorder/genie_subghz_receive.c @@ -0,0 +1,143 @@ +#include "genie_subghz_receive.h" + +#ifdef TAG +#undef TAG +#endif +#define TAG "GenieSubGHzReceive" + +static SubGhzEnvironment* load_environment() { + SubGhzEnvironment* environment = subghz_environment_alloc(); + subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_NAME); + subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_USER_NAME); + subghz_environment_set_came_atomo_rainbow_table_file_name( + environment, SUBGHZ_CAME_ATOMO_DIR_NAME); + subghz_environment_set_alutech_at_4n_rainbow_table_file_name( + environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME); + subghz_environment_set_nice_flor_s_rainbow_table_file_name( + environment, SUBGHZ_NICE_FLOR_S_DIR_NAME); + subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry); + return environment; +} + +GenieSubGhz* genie_subghz_alloc() { + GenieSubGhz* subghz = malloc(sizeof(GenieSubGhz)); + subghz->status = SUBGHZ_RECEIVER_UNINITIALIZED; + subghz->environment = load_environment(); + subghz->stream = furi_stream_buffer_alloc(sizeof(LevelDuration) * 1024, sizeof(LevelDuration)); + furi_check(subghz->stream); + subghz->overrun = false; + return subghz; +} + +void genie_subghz_free(GenieSubGhz* subghz) { + subghz_environment_free(subghz->environment); + furi_stream_buffer_free(subghz->stream); + free(subghz); +} + +static void + rx_callback(SubGhzReceiver* receiver, SubGhzProtocolDecoderBase* decoder_base, void* cxt) { + GenieSubGhz* context = (GenieSubGhz*)cxt; + FuriString* buffer = furi_string_alloc(); + subghz_protocol_decoder_base_get_string(decoder_base, buffer); + subghz_receiver_reset(receiver); + FURI_LOG_I(TAG, "PACKET:\r\n%s", furi_string_get_cstr(buffer)); + if(context->callback) { + context->callback(buffer, context->callback_context); + } + furi_string_free(buffer); +} + +static void rx_capture_callback(bool level, uint32_t duration, void* context) { + GenieSubGhz* instance = context; + + LevelDuration level_duration = level_duration_make(level, duration); + if(instance->overrun) { + instance->overrun = false; + level_duration = level_duration_reset(); + } + size_t ret = + furi_stream_buffer_send(instance->stream, &level_duration, sizeof(LevelDuration), 0); + if(sizeof(LevelDuration) != ret) { + instance->overrun = true; + } +} + +static int32_t listen_rx(void* ctx) { + GenieSubGhz* context = (GenieSubGhz*)ctx; + context->status = SUBGHZ_RECEIVER_LISTENING; + LevelDuration level_duration; + FURI_LOG_I(TAG, "listen_rx started..."); + while(context->status == SUBGHZ_RECEIVER_LISTENING) { + int ret = furi_stream_buffer_receive( + context->stream, &level_duration, sizeof(LevelDuration), 10); + + if(ret == sizeof(LevelDuration)) { + if(level_duration_is_reset(level_duration)) { + subghz_receiver_reset(context->receiver); + } else { + bool level = level_duration_get_level(level_duration); + uint32_t duration = level_duration_get_duration(level_duration); + subghz_receiver_decode(context->receiver, level, duration); + } + } + } + FURI_LOG_I(TAG, "listen_rx exiting..."); + context->status = SUBGHZ_RECEIVER_NOTLISTENING; + return 0; +} + +void start_listening( + GenieSubGhz* context, + uint32_t frequency, + SubghzPacketCallback callback, + void* callback_context) { + context->status = SUBGHZ_RECEIVER_INITIALIZING; + + context->callback = callback; + context->callback_context = callback_context; + subghz_devices_init(); + const SubGhzDevice* device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); + if(!subghz_devices_is_frequency_valid(device, frequency)) { + FURI_LOG_E(TAG, "Frequency not in range. %lu\r\n", frequency); + subghz_devices_deinit(); + return; + } + + context->receiver = subghz_receiver_alloc_init(context->environment); + subghz_receiver_set_filter(context->receiver, SubGhzProtocolFlag_Decodable); + subghz_receiver_set_rx_callback(context->receiver, rx_callback, context); + + subghz_devices_begin(device); + subghz_devices_reset(device); + subghz_devices_load_preset(device, FuriHalSubGhzPresetOok650Async, NULL); + frequency = subghz_devices_set_frequency(device, frequency); + + furi_hal_power_suppress_charge_enter(); + + subghz_devices_start_async_rx(device, rx_capture_callback, context); + + FURI_LOG_I(TAG, "Listening at frequency: %lu\r\n", frequency); + + context->thread = furi_thread_alloc_ex("RX", 1024, listen_rx, context); + furi_thread_start(context->thread); +} + +void stop_listening(GenieSubGhz* context) { + if(context->status == SUBGHZ_RECEIVER_UNINITIALIZED) { + return; + } + + context->status = SUBGHZ_RECEIVER_UNINITIALING; + FURI_LOG_D(TAG, "Stopping listening..."); + furi_thread_join(context->thread); + furi_thread_free(context->thread); + furi_hal_power_suppress_charge_exit(); + + const SubGhzDevice* device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME); + subghz_devices_stop_async_rx(device); + + subghz_receiver_free(context->receiver); + subghz_devices_deinit(); + context->status = SUBGHZ_RECEIVER_UNINITIALIZED; +} \ No newline at end of file diff --git a/subghz/apps/genie-recorder/genie_subghz_receive.h b/subghz/apps/genie-recorder/genie_subghz_receive.h new file mode 100644 index 0000000..f880d80 --- /dev/null +++ b/subghz/apps/genie-recorder/genie_subghz_receive.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +typedef void (*SubghzPacketCallback)(FuriString* buffer, void* context); + +typedef enum { + SUBGHZ_RECEIVER_INITIALIZING, + SUBGHZ_RECEIVER_LISTENING, + SUBGHZ_RECEIVER_NOTLISTENING, + SUBGHZ_RECEIVER_UNINITIALING, + SUBGHZ_RECEIVER_UNINITIALIZED, +} SubghzReceiverState; + +typedef struct { + SubGhzEnvironment* environment; + FuriStreamBuffer* stream; + FuriThread* thread; + SubGhzReceiver* receiver; + bool overrun; + SubghzReceiverState status; + SubghzPacketCallback callback; + void* callback_context; +} GenieSubGhz; + +GenieSubGhz* genie_subghz_alloc(); +void genie_subghz_free(GenieSubGhz* subghz); + +void start_listening( + GenieSubGhz* context, + uint32_t frequency, + SubghzPacketCallback callback, + void* callback_context); +void stop_listening(GenieSubGhz* context); \ No newline at end of file