// scenes/plugins/protopirate_emulate_plugin.c #include "protopirate_emulate_plugin.h" #include "../../protopirate_app_i.h" #ifdef ENABLE_EMULATE_FEATURE #include "../../protocols/protocols_common.h" #include "../../protocols/protocol_items.h" #include "../../protocols/fiat_v1.h" #include "../../protocols/ford_v1.h" #include "../../protocols/kia_v0.h" #include "../../protocols/kia_v3_v4.h" #include "../../protocols/kia_v7.h" #include "../../protocols/psa.h" #include "../../protocols/renault_v0.h" #include "../../protocols/renault_v1.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define TAG "ProtoPirateEmulatePlugin" #define MIN_TX_TIME 666U #define MIN_TX_TIME_KIA_V3_V4 1600U #define TX_PRESET_PATCH_MAX_SIZE 128U #define EMU_PRESET_KEY_PROTOCOL "Protocol" #define EMU_PRESET_KEY_FREQUENCY "Frequency" #define EMU_PRESET_KEY_PRESET "Preset" #define EMU_PRESET_KEY_SERIAL "Serial" #define EMU_PRESET_KEY_BTN "Btn" #define EMU_PRESET_KEY_CNT "Cnt" #define EMU_PRESET_KEY_TYPE "Type" #define EMU_PRESET_KEY_HITAG2_KEY "Hitag2 Key" #define EMU_PRESET_KEY_HITAG2_EPOCH "Hitag2 Epoch" #define EMU_CUSTOM_PRESET_KEY "Custom_preset_data" #define EMU_HITAG2_KEY_TEXT_LEN 12U #define EMU_HITAG2_RECOVERED_YES 1U typedef struct { uint32_t original_counter; uint32_t current_counter; uint32_t serial; uint8_t original_button; FuriString* protocol_name; const char* preset; FuriString* preset_from_file; uint32_t freq; FlipperFormat* flipper_format; SubGhzTransmitter* transmitter; bool is_transmitting; bool flag_stop_called; bool replay_only; Storage* storage; char hitag2_key_text[EMU_HITAG2_KEY_TEXT_LEN + 1U]; } EmulateContext; typedef struct { uint8_t* data; size_t size; bool should_free; } EmulateResolvedPreset; static EmulateContext* emulate_context = NULL; static const ProtoPirateEmulateHostApi* g_host_api = NULL; static bool emulate_hitag2_key_nonzero(const uint8_t key[6]) { for(size_t i = 0; i < 6U; i++) { if(key[i]) { return true; } } return false; } static bool emulate_has_hitag2_key(FlipperFormat* flipper_format) { if(!flipper_format) return false; uint8_t key[6] = {0}; flipper_format_rewind(flipper_format); return flipper_format_read_hex(flipper_format, EMU_PRESET_KEY_HITAG2_KEY, key, sizeof(key)); } static bool emulate_has_nonzero_hitag2_key(FlipperFormat* flipper_format) { if(!flipper_format) return false; uint8_t key[6] = {0}; flipper_format_rewind(flipper_format); if(!flipper_format_read_hex(flipper_format, EMU_PRESET_KEY_HITAG2_KEY, key, sizeof(key))) { return false; } return emulate_hitag2_key_nonzero(key); } static bool emulate_parse_hitag2_key_text(const char* text, uint8_t key[6]) { if(!text || !key) { return false; } uint8_t hex_count = 0U; uint8_t high_nibble = 0U; for(size_t i = 0U; text[i] != '\0'; i++) { const char c = text[i]; if(c == ' ') { continue; } uint8_t nibble = 0U; if(c >= '0' && c <= '9') { nibble = (uint8_t)(c - '0'); } else if(c >= 'A' && c <= 'F') { nibble = (uint8_t)(c - 'A' + 10); } else if(c >= 'a' && c <= 'f') { nibble = (uint8_t)(c - 'a' + 10); } else { return false; } if(hex_count >= 12U) { return false; } if((hex_count & 1U) == 0U) { high_nibble = nibble; } else { key[hex_count >> 1U] = (uint8_t)((high_nibble << 4U) | nibble); } hex_count++; } return hex_count == 12U; } static bool emulate_write_hitag2_key(FlipperFormat* flipper_format, const uint8_t key[6]) { if(!flipper_format || !key) { return false; } flipper_format_rewind(flipper_format); if(!flipper_format_insert_or_update_hex(flipper_format, EMU_PRESET_KEY_HITAG2_KEY, key, 6U)) { return false; } uint32_t epoch = 0U; flipper_format_rewind(flipper_format); if(!flipper_format_read_uint32(flipper_format, EMU_PRESET_KEY_HITAG2_EPOCH, &epoch, 1U)) { flipper_format_rewind(flipper_format); flipper_format_insert_or_update_uint32( flipper_format, EMU_PRESET_KEY_HITAG2_EPOCH, &epoch, 1U); } flipper_format_rewind(flipper_format); return true; } static bool emulate_hitag2_recovered_yes(FlipperFormat* flipper_format) { if(!flipper_format) { return false; } uint8_t recovered_hex = 0; flipper_format_rewind(flipper_format); if(flipper_format_read_hex(flipper_format, "Recovered", &recovered_hex, 1)) { return recovered_hex == EMU_HITAG2_RECOVERED_YES; } uint32_t recovered_u32 = 0; flipper_format_rewind(flipper_format); if(flipper_format_read_uint32(flipper_format, "Recovered", &recovered_u32, 1)) { return (uint8_t)recovered_u32 == EMU_HITAG2_RECOVERED_YES; } return false; } static void emulate_request_nav_pop(ProtoPirateApp* app) { app->emulate_nav_pending = EMULATE_NAV_POP; } static void emulate_request_nav_after_exit(ProtoPirateApp* app) { if(scene_manager_has_previous_scene(app->scene_manager, ProtoPirateSceneStart)) { app->emulate_nav_pending = EMULATE_NAV_POP; } else { app->emulate_nav_pending = EMULATE_NAV_STOP_APP; } } static bool emulate_prompt_hitag2_key(ProtoPirateApp* app, EmulateContext* ctx); static void emulate_hitag2_key_input_callback(void* context) { ProtoPirateApp* app = context; EmulateContext* ctx = emulate_context; uint8_t key[6] = {0}; if(!app || !ctx || !ctx->flipper_format || !emulate_parse_hitag2_key_text(ctx->hitag2_key_text, key)) { if(app && app->notifications) { notification_message(app->notifications, &sequence_error); } if(app && ctx) { (void)emulate_prompt_hitag2_key(app, ctx); } return; } if(!emulate_write_hitag2_key(ctx->flipper_format, key)) { notification_message(app->notifications, &sequence_error); emulate_request_nav_pop(app); return; } view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewAbout); if(app->view_about) { view_commit_model(app->view_about, true); } } static bool emulate_prompt_hitag2_key(ProtoPirateApp* app, EmulateContext* ctx) { furi_check(app); furi_check(ctx); if(!g_host_api || !g_host_api->ensure_text_input || !g_host_api->ensure_text_input(app)) { return false; } memset(ctx->hitag2_key_text, 0, sizeof(ctx->hitag2_key_text)); text_input_reset(app->text_input); text_input_set_header_text(app->text_input, "HITAG2 key (12 hex):"); text_input_set_result_callback( app->text_input, emulate_hitag2_key_input_callback, app, ctx->hitag2_key_text, sizeof(ctx->hitag2_key_text), true); view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewTextInput); return true; } static bool emulate_needs_hitag2_prompt(EmulateContext* ctx) { if(!ctx || !ctx->flipper_format) { return false; } if(furi_string_equal(ctx->protocol_name, FIAT_V1_PROTOCOL_NAME)) { return !emulate_has_hitag2_key(ctx->flipper_format); } if(furi_string_equal(ctx->protocol_name, RENAULT_PROTOCOL_V1_NAME)) { if(emulate_hitag2_recovered_yes(ctx->flipper_format)) { return false; } return !emulate_has_nonzero_hitag2_key(ctx->flipper_format); } return false; } static bool emu_preset_name_is_custom_marker(const char* preset_name) { return preset_name && (!strcmp(preset_name, "Custom") || !strcmp(preset_name, "CUSTOM") || !strcmp(preset_name, "FuriHalSubGhzPresetCustom") || strstr(preset_name, "PresetCustom")); } static const char* emu_get_short_preset_name(const char* preset_name) { if(!preset_name || preset_name[0] == '\0') return "AM650"; if(strstr(preset_name, "Ook650") || strstr(preset_name, "OOK650")) return "AM650"; if(strstr(preset_name, "Ook270") || strstr(preset_name, "OOK270")) return "AM270"; if(strstr(preset_name, "2FSKDev238") || strstr(preset_name, "Dev238")) return "FM238"; if(strstr(preset_name, "2FSKDev12K") || strstr(preset_name, "Dev12K")) return "FM12K"; if(strstr(preset_name, "2FSKDev476") || strstr(preset_name, "Dev476")) return "FM476"; if(emu_preset_name_is_custom_marker(preset_name)) return "Custom"; if(!strcmp(preset_name, "AM650")) return "AM650"; if(!strcmp(preset_name, "AM270")) return "AM270"; if(!strcmp(preset_name, "FM238")) return "FM238"; if(!strcmp(preset_name, "FM12K")) return "FM12K"; if(!strcmp(preset_name, "FM476")) return "FM476"; return preset_name; } // ----------------------------------------------------------------------------- // Notification sequence used while transmitting. Duplicated locally so the // plugin does not link against the host's `static const` copy. // ----------------------------------------------------------------------------- static const NotificationSequence emu_sequence_tx = { &message_note_c5, &message_vibro_on, &message_red_255, &message_blue_255, &message_blink_start_10, &message_delay_25, &message_vibro_off, &message_delay_25, &message_sound_off, NULL, }; #define TX_PRESET_VALUES_AM 8 // Gets 1 added, so is 1 less than actual value. #define TX_PRESET_VALUES_COUNT 17 static const uint8_t tx_power_value[TX_PRESET_VALUES_COUNT] = { // FM Power Values for 1st PA Table Byte. 0, 0xC0, // 10dBm 0xC8, // 7dBm 0x84, // 5dBm 0x60, // 0dBm 0x34, // -10dBm 0x1D, // -15dBm 0x0E, // -20dBm 0x12, // -30dBm // AM Power Values for 2nd PA Table Byte. 0xC0, // 12dBm 0xCD, // 7dBm 0x86, // 5dBm 0x50, // 0dBm 0x26, // -10dBm 0x1D, // -15dBm 0x17, // -20dBm 0x03 // -30dBm }; static bool emulate_radio_ready(ProtoPirateApp* app) { furi_check(app); return app->radio_initialized && app->txrx && app->txrx->radio_device && app->txrx->environment; } static uint32_t emulate_min_tx_time(const EmulateContext* ctx) { if(!ctx || !ctx->protocol_name) return MIN_TX_TIME; const char* proto = furi_string_get_cstr(ctx->protocol_name); proto = protopirate_protocol_catalog_canonical_name(proto); if(proto && strcmp(proto, KIA_PROTOCOL_V3_V4_NAME) == 0) { return MIN_TX_TIME_KIA_V3_V4; } return MIN_TX_TIME; } static void emulate_context_reset_transmitter(void) { EmulateContext* ctx = emulate_context; if(ctx && ctx->transmitter) { subghz_transmitter_free(ctx->transmitter); ctx->transmitter = NULL; } } static void emulate_resolved_preset_release(EmulateResolvedPreset* preset) { if(!preset) return; if(preset->should_free && preset->data) free(preset->data); preset->data = NULL; preset->size = 0U; preset->should_free = false; } static bool emulate_resolved_preset_assign_named( ProtoPirateApp* app, const char* preset_name, EmulateResolvedPreset* preset) { furi_check(app); furi_check(preset); int preset_index = subghz_setting_get_inx_preset_by_name(app->setting, preset_name); if(preset_index < 0) return false; uint8_t* preset_data = subghz_setting_get_preset_data(app->setting, (size_t)preset_index); size_t preset_size = subghz_setting_get_preset_data_size(app->setting, (size_t)preset_index); if(!preset_data || !preset_size) return false; preset->data = preset_data; preset->size = preset_size; preset->should_free = false; return true; } static bool emulate_resolved_preset_try_load_custom(EmulateContext* ctx, EmulateResolvedPreset* preset) { furi_check(ctx); furi_check(preset); if(!ctx->flipper_format) return false; uint32_t value_count = 0; flipper_format_rewind(ctx->flipper_format); if(!flipper_format_get_value_count(ctx->flipper_format, EMU_CUSTOM_PRESET_KEY, &value_count) || value_count == 0U || value_count >= 1024U) { return false; } uint8_t* preset_data = malloc(value_count); if(!preset_data) return false; flipper_format_rewind(ctx->flipper_format); if(!flipper_format_read_hex( ctx->flipper_format, EMU_CUSTOM_PRESET_KEY, preset_data, value_count)) { free(preset_data); return false; } preset->data = preset_data; preset->size = value_count; preset->should_free = true; return true; } static bool emulate_context_resolve_tx_preset( ProtoPirateApp* app, EmulateContext* ctx, EmulateResolvedPreset* preset) { furi_check(app); furi_check(ctx); furi_check(preset); memset(preset, 0, sizeof(*preset)); const char* requested_preset = ctx->preset ? ctx->preset : "AM650"; const char* saved_preset = ctx->preset_from_file ? furi_string_get_cstr(ctx->preset_from_file) : NULL; if(emu_preset_name_is_custom_marker(requested_preset)) { if(emulate_resolved_preset_try_load_custom(ctx, preset)) { ctx->preset = "Custom"; return true; } FURI_LOG_W(TAG, "Custom preset data missing"); if(saved_preset && saved_preset[0] && !emu_preset_name_is_custom_marker(saved_preset)) { requested_preset = saved_preset; } else { requested_preset = "AM650"; } } if(emulate_resolved_preset_assign_named(app, requested_preset, preset)) { ctx->preset = requested_preset; return true; } if(saved_preset && saved_preset[0] && strcmp(saved_preset, requested_preset) != 0 && !emu_preset_name_is_custom_marker(saved_preset) && emulate_resolved_preset_assign_named(app, saved_preset, preset)) { ctx->preset = saved_preset; return true; } if(strcmp(requested_preset, "AM650") != 0) { FURI_LOG_W(TAG, "Preset %s not found, trying AM650", requested_preset); if(emulate_resolved_preset_assign_named(app, "AM650", preset)) { ctx->preset = "AM650"; return true; } } FURI_LOG_W(TAG, "AM650 not found, trying FM476"); if(emulate_resolved_preset_assign_named(app, "FM476", preset)) { ctx->preset = "FM476"; return true; } return false; } static void emu_stop_tx(ProtoPirateApp* app) { FURI_LOG_I(TAG, "Stopping transmission"); if(app->txrx->radio_device) { subghz_devices_stop_async_tx(app->txrx->radio_device); } else { FURI_LOG_W(TAG, "stop_tx requested without radio device"); } if(emulate_context && emulate_context->transmitter) { subghz_transmitter_stop(emulate_context->transmitter); } furi_delay_ms(10); if(app->txrx->radio_device) { subghz_devices_idle(app->txrx->radio_device); } app->txrx->txrx_state = ProtoPirateTxRxStateIDLE; app->start_tx_time = 0; emulate_context_reset_transmitter(); FURI_LOG_I(TAG, "Transmission stopped, state set to IDLE"); notification_message(app->notifications, &sequence_blink_stop); } static void emulate_context_free(void) { if(emulate_context == NULL) return; if(emulate_context->transmitter) { subghz_transmitter_free(emulate_context->transmitter); emulate_context->transmitter = NULL; } if(emulate_context->flipper_format) { flipper_format_free(emulate_context->flipper_format); emulate_context->flipper_format = NULL; } if(emulate_context->protocol_name) { furi_string_free(emulate_context->protocol_name); emulate_context->protocol_name = NULL; } if(emulate_context->preset_from_file) { furi_string_free(emulate_context->preset_from_file); emulate_context->preset_from_file = NULL; } if(emulate_context->storage) { furi_record_close(RECORD_STORAGE); emulate_context->storage = NULL; } free(emulate_context); emulate_context = NULL; } static bool emulate_context_try_init_transmitter(ProtoPirateApp* app, EmulateContext* ctx) { if(ctx->transmitter) return true; if(!ctx->flipper_format || !ctx->protocol_name) return false; const char* proto_name = furi_string_get_cstr(ctx->protocol_name); const char* registry_name = protopirate_protocol_catalog_canonical_name(proto_name); if(!registry_name || !protopirate_protocol_catalog_can_tx(proto_name)) { FURI_LOG_E(TAG, "Protocol %s has no TX catalog entry", proto_name ? proto_name : "?"); return false; } if(strcmp(proto_name, registry_name) != 0) { FURI_LOG_I(TAG, "Protocol name %s mapped to %s for registry", proto_name, registry_name); } EmulateResolvedPreset resolved_preset; if(!emulate_context_resolve_tx_preset(app, ctx, &resolved_preset)) { FURI_LOG_E(TAG, "Failed to resolve preset data for emulate registry"); return false; } bool registry_ready = g_host_api && g_host_api->apply_protocol_registry_for_context && g_host_api->apply_protocol_registry_for_context( app, ctx->preset, ctx->freq, resolved_preset.data, resolved_preset.size, registry_name); emulate_resolved_preset_release(&resolved_preset); if(!registry_ready) { FURI_LOG_E(TAG, "Failed to apply protocol registry for emulate preset"); return false; } const SubGhzProtocol* protocol = NULL; const SubGhzProtocolRegistry* active_registry = app->txrx->protocol_registry; if(!active_registry) { FURI_LOG_E(TAG, "Active protocol registry unavailable"); return false; } for(size_t i = 0; i < active_registry->size; i++) { if(strcmp(active_registry->items[i]->name, registry_name) == 0) { protocol = active_registry->items[i]; FURI_LOG_I(TAG, "Found protocol %s in registry at index %zu", registry_name, i); break; } } if(!protocol || !protocol->encoder || !protocol->encoder->alloc) { FURI_LOG_E(TAG, "Protocol %s has no encoder or not in registry", registry_name); return false; } ctx->transmitter = subghz_transmitter_alloc_init(app->txrx->environment, registry_name); if(!ctx->transmitter) { FURI_LOG_E(TAG, "Failed to allocate transmitter for %s", registry_name); return false; } flipper_format_rewind(ctx->flipper_format); SubGhzProtocolStatus status = subghz_transmitter_deserialize(ctx->transmitter, ctx->flipper_format); if(status != SubGhzProtocolStatusOk) { FURI_LOG_E(TAG, "Failed to deserialize transmitter, status: %d", status); subghz_transmitter_free(ctx->transmitter); ctx->transmitter = NULL; return false; } FURI_LOG_I(TAG, "Transmitter ready (lazy init)"); return true; } static uint8_t emu_button_for_protocol( const char* protocol, InputKey key, uint8_t original, FlipperFormat* ff) { if(!protocol) { return original; } protocol = protopirate_protocol_catalog_canonical_name(protocol); if(strcmp(protocol, KIA_PROTOCOL_V7_NAME) == 0) { switch(key) { case InputKeyUp: return 0x01; // Lock case InputKeyOk: return 0x02; // Unlock case InputKeyDown: return 0x03; // Trunk case InputKeyRight: return 0x08; // Panic default: return original; } } if(strcmp(protocol, KIA_PROTOCOL_V3_V4_NAME) == 0) { switch(key) { case InputKeyUp: return 0x01; case InputKeyOk: return 0x02; case InputKeyDown: return 0x03; case InputKeyLeft: return 0x04; case InputKeyRight: return 0x08; default: return original; } } if(strstr(protocol, "Kia")) { uint32_t kia_v0_type = 1; if(ff) { flipper_format_rewind(ff); flipper_format_read_uint32(ff, EMU_PRESET_KEY_TYPE, &kia_v0_type, 1); } if(kia_v0_type == 2) { switch(key) { case InputKeyUp: return 0x3; // Lock case InputKeyOk: return 0x4; // Unlock case InputKeyDown: return 0x2; // Boot case InputKeyLeft: return 0x1; // Panic default: return original; } } if(kia_v0_type == 3) { switch(key) { case InputKeyUp: return 1; case InputKeyOk: return 2; case InputKeyDown: return 3; case InputKeyLeft: return 4; case InputKeyRight: return 5; default: return original; } } switch(key) { case InputKeyUp: return 0x1; // Lock case InputKeyOk: return 0x2; // Unlock case InputKeyDown: return 0x3; // Boot case InputKeyLeft: return 0x4; // Panic case InputKeyRight: return 0x8; // Horn/Lights? default: return original; } } else if(strstr(protocol, "VAG")) { if(original == 0x10 || original == 0x20 || original == 0x40) { switch(key) { case InputKeyUp: return 0x20; // Lock case InputKeyOk: return 0x10; // Unlock case InputKeyDown: return 0x40; // Boot default: return original; } } switch(key) { case InputKeyUp: return 0x2; // Lock case InputKeyOk: return 0x1; // Unlock case InputKeyDown: return 0x4; // Boot case InputKeyLeft: return 0x8; // Panic case InputKeyRight: return 0x3; // Un+Lk combo default: return original; } } else if(strstr(protocol, RENAULT_PROTOCOL_V0_NAME)) { uint32_t renault_type = 0U; if(ff) { flipper_format_rewind(ff); flipper_format_read_uint32(ff, EMU_PRESET_KEY_TYPE, &renault_type, 1); } const bool type_13 = renault_type == 0x13U; const bool type_high = (renault_type == 0x1AU) || (renault_type == 0x3BU); switch(key) { case InputKeyUp: return type_13 ? 0x06 : (type_high ? 0x44 : 0x04); case InputKeyOk: return type_13 ? 0x0A : (type_high ? 0x48 : 0x08); default: return original; } } else if(strstr(protocol, "Honda V1")) { switch(key) { case InputKeyUp: return 0x08; // Lock case InputKeyOk: return 0x00; // Unlock case InputKeyDown: return 0x09; // Trunk case InputKeyLeft: return 0x0A; // Panic default: return original; } } else if(strstr(protocol, "Honda Static")) { switch(key) { case InputKeyUp: return 0x1; // Lock case InputKeyOk: return 0x2; // Unlock case InputKeyDown: return 0x4; // Trunk case InputKeyRight: return 0x5; // Remote Start case InputKeyLeft: return 0x8; // Panic default: return original; } } else if(strstr(protocol, "Mazda")) { switch(key) { case InputKeyUp: return 0x01; // Lock case InputKeyOk: return 0x02; // Unlock case InputKeyDown: return 0x04; // Trunk case InputKeyRight: return 0x08; // Remote default: return original; } } else if(strstr(protocol, "Honda V2")) { switch(key) { case InputKeyUp: return 0x02; // Lock case InputKeyOk: return 0x04; // Unlock default: return original; } } else if(strcmp(protocol, PSA_PROTOCOL_NAME) == 0 || strstr(protocol, "PSA")) { switch(key) { case InputKeyUp: return 0x1; // Lock case InputKeyOk: return 0x2; // Unlock case InputKeyDown: return 0x4; // Trunk case InputKeyLeft: return 0x8; // Panic default: return original; } } else if(strstr(protocol, "Ford")) { if(strstr(protocol, FORD_PROTOCOL_V1_NAME)) { switch(key) { case InputKeyUp: return 0x1; // Lock case InputKeyOk: return 0x2; // Unlock case InputKeyDown: return 0x4; // Trunk case InputKeyLeft: return 0x8; // Panic default: return original; } } switch(key) { case InputKeyLeft: return 0x1; // Panic case InputKeyUp: return 0x2; // Lock case InputKeyOk: return 0x4; // Unlock case InputKeyDown: return 0x8; // Boot case InputKeyRight: return 0x10; default: return original; } } else if(strstr(protocol, "Subaru")) { switch(key) { case InputKeyUp: return 0x1; // Lock? case InputKeyOk: return 0x2; // Unlock? case InputKeyDown: return 0x3; // Boot? case InputKeyLeft: return 0x4; // Panic? case InputKeyRight: return 0x8; default: return original; } } else if(strstr(protocol, "Chrysler")) { switch(key) { case InputKeyUp: return 0x1; // Lock case InputKeyOk: return 0x2; // Unlock default: return original; } } else if(strstr(protocol, FIAT_V1_PROTOCOL_NAME)) { switch(key) { case InputKeyUp: return 0x4; // Lock case InputKeyOk: return 0x8; // Unlock case InputKeyDown: return 0x2; // Trunk case InputKeyLeft: return 0x1; // Close default: return original; } } else if(strstr(protocol, RENAULT_PROTOCOL_V1_NAME)) { switch(key) { case InputKeyUp: return 0x1; // Lock case InputKeyOk: return 0x2; // Unlock case InputKeyDown: return 0x2; // Unlock case InputKeyLeft: return 0x4; // Trunk case InputKeyRight: return 0x8; // Panic default: return original; } } else if(strstr(protocol, RENAULT_PROTOCOL_V0_NAME)) { switch(key) { case InputKeyOk: return 0x06; // Lock case InputKeyUp: return 0x0A; // Unlock case InputKeyDown: return 0x05; // Trunk default: return original; } } else if(strstr(protocol, "Fiat")) { return original; } else if(strstr(protocol, "Scher")) { return original; } else if(strstr(protocol, "Star Line")) { return original; } return original; } static bool emulate_update_data(EmulateContext* ctx, uint8_t button) { if(!ctx || !ctx->flipper_format) return false; flipper_format_rewind(ctx->flipper_format); uint32_t btn_value = button; flipper_format_insert_or_update_uint32(ctx->flipper_format, EMU_PRESET_KEY_BTN, &btn_value, 1); FURI_LOG_I(TAG, "Updated flipper format - Btn: 0x%02X", button); flipper_format_insert_or_update_uint32( ctx->flipper_format, EMU_PRESET_KEY_CNT, &ctx->current_counter, 1); FURI_LOG_I(TAG, "Updated flipper format - Cnt: 0x%03lX", (unsigned long)ctx->current_counter); return true; } static void emulate_draw_callback(Canvas* canvas, void* model) { UNUSED(model); if(!emulate_context) return; EmulateContext* ctx = emulate_context; static uint8_t animation_frame = 0; animation_frame = (animation_frame + 1) % 8; canvas_clear(canvas); canvas_draw_box(canvas, 0, 0, 128, 11); canvas_invert_color(canvas); canvas_set_font(canvas, FontSecondary); const char* proto_name = furi_string_get_cstr(ctx->protocol_name); canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, proto_name); canvas_invert_color(canvas); canvas_set_font(canvas, FontSecondary); char info_str[32]; if(ctx->serial <= 0xFFFFFFUL) { snprintf( info_str, sizeof(info_str), "SN:%06lX", (unsigned long)(ctx->serial & 0xFFFFFFUL)); } else { snprintf(info_str, sizeof(info_str), "SN:%08lX", (unsigned long)ctx->serial); } canvas_draw_str(canvas, 2, 20, info_str); snprintf( info_str, sizeof(info_str), "F:%lu.%02lu", ctx->freq / 1000000, (ctx->freq % 1000000) / 10000); canvas_draw_str(canvas, 2, 30, info_str); if(ctx->current_counter > 0xFFFF) { snprintf( info_str, sizeof(info_str), "CNT:...%03lX", (unsigned long)ctx->current_counter & 0xFFF); } else { snprintf(info_str, sizeof(info_str), "CNT:%04lX", (unsigned long)ctx->current_counter); } canvas_draw_str(canvas, 68, 20, info_str); if(ctx->current_counter > ctx->original_counter) { snprintf( info_str, sizeof(info_str), "+%ld", (long)(ctx->current_counter - ctx->original_counter)); canvas_draw_str(canvas, 112, 20, info_str); } snprintf(info_str, sizeof(info_str), "%s", ctx->preset); canvas_draw_str(canvas, 95, 30, info_str); canvas_set_font(canvas, FontSecondary); const char* unlock_text = ctx->replay_only ? "REPLAY" : "UNLOCK"; uint16_t width_button = canvas_string_width(canvas, unlock_text) + 8; uint16_t height_button = canvas_current_font_height(canvas); canvas_draw_rbox( canvas, 64 - (width_button / 2), 45 - (height_button / 2), width_button, height_button, 3); canvas_invert_color(canvas); canvas_draw_str_aligned(canvas, 64, 49, AlignCenter, AlignBottom, unlock_text); canvas_invert_color(canvas); char* panic_text = "PANIC"; width_button = canvas_string_width(canvas, panic_text) + 8; canvas_draw_rbox( canvas, 64 - (width_button / 2), 33 - (height_button / 2), width_button, height_button, 3); canvas_invert_color(canvas); canvas_draw_str_aligned(canvas, 64, 37, AlignCenter, AlignBottom, "LOCK"); canvas_invert_color(canvas); canvas_draw_rbox(canvas, 0, 46 - (height_button / 2), width_button, height_button, 3); canvas_invert_color(canvas); canvas_draw_str_aligned(canvas, (width_button / 2), 50, AlignCenter, AlignBottom, panic_text); canvas_invert_color(canvas); canvas_draw_rbox( canvas, 127 - width_button, 46 - (height_button / 2), width_button, height_button, 3); canvas_invert_color(canvas); canvas_draw_str_aligned(canvas, 127 - (width_button / 2), 50, AlignCenter, AlignBottom, "XXX"); canvas_invert_color(canvas); canvas_draw_rbox( canvas, 64 - (width_button / 2), 57 - (height_button / 2), width_button, height_button, 3); canvas_invert_color(canvas); canvas_draw_str_aligned(canvas, 64, 61, AlignCenter, AlignBottom, "BOOT"); canvas_invert_color(canvas); if(ctx->is_transmitting) { canvas_draw_rbox(canvas, 24, 18, 80, 18, 3); canvas_invert_color(canvas); int wave = animation_frame % 3; canvas_draw_str(canvas, 28 + wave * 2, 25, ")))"); canvas_set_font(canvas, FontPrimary); canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignCenter, "TX"); canvas_invert_color(canvas); } } static bool emulate_input_callback(InputEvent* event, void* context) { ProtoPirateApp* app = context; EmulateContext* ctx = emulate_context; if(!ctx) return false; if(event->type == InputTypePress) { if(event->key == InputKeyBack) { view_dispatcher_send_custom_event( app->view_dispatcher, ProtoPirateCustomEventEmulateExit); return true; } if(!ctx->replay_only) { uint8_t button = emu_button_for_protocol( furi_string_get_cstr(ctx->protocol_name), event->key, ctx->original_button, ctx->flipper_format); if(furi_string_equal(ctx->protocol_name, RENAULT_PROTOCOL_V0_NAME)) { ctx->current_counter = (ctx->current_counter + 1U) & 0xFFU; } else { ctx->current_counter++; } emulate_update_data(ctx, button); } ctx->is_transmitting = true; view_dispatcher_send_custom_event( app->view_dispatcher, ProtoPirateCustomEventEmulateTransmit); return true; } else if(event->type == InputTypeRelease) { if(ctx && ctx->is_transmitting) { view_dispatcher_send_custom_event( app->view_dispatcher, ProtoPirateCustomEventEmulateStop); return true; } return false; } return false; } static bool get_tx_preset_byte(const uint8_t* preset_data, size_t preset_size, uint8_t* preset_offset) { if(!preset_data || !preset_offset) return false; size_t offset = 0; size_t scan_limit = preset_size < TX_PRESET_PATCH_MAX_SIZE ? preset_size : TX_PRESET_PATCH_MAX_SIZE; while((offset < scan_limit) && preset_data[offset]) { offset += 2; } if(offset >= scan_limit) return false; size_t tx_offset = offset + 2U; if((tx_offset + 1U) >= scan_limit || (tx_offset + 1U) >= preset_size) return false; *preset_offset = (uint8_t)tx_offset; return true; } static void plugin_on_enter(void* context) { ProtoPirateApp* app = context; furi_check(g_host_api); if(!g_host_api->ensure_view_about || !g_host_api->ensure_view_about(app)) { notification_message(app->notifications, &sequence_error); emulate_request_nav_pop(app); return; } if(emulate_context != NULL) { FURI_LOG_W(TAG, "Previous emulate context not freed, cleaning up"); emulate_context_free(); } if(app->txrx && app->txrx->history && g_host_api->history_release_scratch) { g_host_api->history_release_scratch(app); } if(g_host_api->rx_stack_suspend_for_tx) { g_host_api->rx_stack_suspend_for_tx(app); } if(!emulate_radio_ready(app)) { if(!g_host_api->radio_init || !g_host_api->radio_init(app)) { FURI_LOG_E(TAG, "Failed to initialize radio for emulate scene"); notification_message(app->notifications, &sequence_error); emulate_request_nav_pop(app); return; } } if(!emulate_radio_ready(app)) { FURI_LOG_E(TAG, "Radio still incomplete after emulate init"); notification_message(app->notifications, &sequence_error); emulate_request_nav_pop(app); return; } emulate_context = malloc(sizeof(EmulateContext)); if(!emulate_context) { FURI_LOG_E(TAG, "Failed to allocate emulate context"); emulate_request_nav_pop(app); return; } memset(emulate_context, 0, sizeof(EmulateContext)); EmulateContext* ctx = emulate_context; ctx->protocol_name = furi_string_alloc(); if(!ctx->protocol_name) { FURI_LOG_E(TAG, "Failed to allocate protocol name string"); emulate_context_free(); emulate_request_nav_pop(app); return; } if(!app->loaded_file_path) { FURI_LOG_E(TAG, "No file path set"); emulate_context_free(); notification_message(app->notifications, &sequence_error); emulate_request_nav_pop(app); return; } ctx->storage = furi_record_open(RECORD_STORAGE); if(!ctx->storage) { FURI_LOG_E(TAG, "Failed to open storage"); emulate_context_free(); notification_message(app->notifications, &sequence_error); emulate_request_nav_pop(app); return; } ctx->flipper_format = flipper_format_file_alloc(ctx->storage); if(!ctx->flipper_format) { FURI_LOG_E(TAG, "Failed to allocate FlipperFormat"); emulate_context_free(); notification_message(app->notifications, &sequence_error); emulate_request_nav_pop(app); return; } if(!flipper_format_file_open_existing( ctx->flipper_format, furi_string_get_cstr(app->loaded_file_path))) { FURI_LOG_E(TAG, "Failed to open file: %s", furi_string_get_cstr(app->loaded_file_path)); emulate_context_free(); notification_message(app->notifications, &sequence_error); emulate_request_nav_pop(app); return; } uint32_t frequency = 433920000; FuriString* preset_str = furi_string_alloc(); flipper_format_rewind(ctx->flipper_format); if(!flipper_format_read_uint32(ctx->flipper_format, EMU_PRESET_KEY_FREQUENCY, &frequency, 1)) { FURI_LOG_W(TAG, "Failed to read frequency, using default 433.92MHz"); } flipper_format_rewind(ctx->flipper_format); if(!flipper_format_read_string(ctx->flipper_format, EMU_PRESET_KEY_PRESET, preset_str)) { FURI_LOG_W(TAG, "Failed to read preset, using AM650"); furi_string_set(preset_str, "AM650"); } ctx->preset_from_file = furi_string_alloc(); furi_string_set(ctx->preset_from_file, preset_str); ctx->preset = emu_get_short_preset_name(furi_string_get_cstr(ctx->preset_from_file)); FURI_LOG_I( TAG, "Using frequency %lu Hz, preset %s (from %s)", (unsigned long)frequency, ctx->preset, furi_string_get_cstr(ctx->preset_from_file)); ctx->freq = frequency; furi_string_free(preset_str); flipper_format_rewind(ctx->flipper_format); if(!flipper_format_read_string( ctx->flipper_format, EMU_PRESET_KEY_PROTOCOL, ctx->protocol_name)) { FURI_LOG_E(TAG, "Failed to read protocol name"); furi_string_set(ctx->protocol_name, "Unknown"); } ctx->replay_only = false; // Standalone Suzuki/Honda/Mitsubishi V0 captures: merged into Kia V0 if(furi_string_equal(ctx->protocol_name, "Suzuki") || furi_string_equal(ctx->protocol_name, "Suzuki V0") || furi_string_equal(ctx->protocol_name, "Honda V0") || furi_string_equal(ctx->protocol_name, "Mitsu V0") || furi_string_equal(ctx->protocol_name, "Mitsu v0") || furi_string_equal(ctx->protocol_name, "Mitsubishi V0") || furi_string_equal(ctx->protocol_name, "Mitsubishi v0")) { uint32_t kia_v0_type = furi_string_equal(ctx->protocol_name, "Honda V0") ? 3U : (furi_string_equal(ctx->protocol_name, "Mitsu V0") || furi_string_equal(ctx->protocol_name, "Mitsu v0") || furi_string_equal(ctx->protocol_name, "Mitsubishi V0") || furi_string_equal(ctx->protocol_name, "Mitsubishi v0")) ? 4U : 2U; furi_string_set(ctx->protocol_name, KIA_PROTOCOL_V0_NAME); flipper_format_rewind(ctx->flipper_format); flipper_format_insert_or_update_string_cstr( ctx->flipper_format, EMU_PRESET_KEY_PROTOCOL, KIA_PROTOCOL_V0_NAME); flipper_format_insert_or_update_uint32( ctx->flipper_format, EMU_PRESET_KEY_TYPE, &kia_v0_type, 1); } if(furi_string_equal(ctx->protocol_name, "Land Rover V0")) { furi_string_set(ctx->protocol_name, "Honda V2"); flipper_format_rewind(ctx->flipper_format); flipper_format_insert_or_update_string_cstr( ctx->flipper_format, EMU_PRESET_KEY_PROTOCOL, "Honda V2"); } const char* canonical_protocol = protopirate_protocol_catalog_canonical_name(furi_string_get_cstr(ctx->protocol_name)); if(canonical_protocol && strcmp(furi_string_get_cstr(ctx->protocol_name), canonical_protocol) != 0) { furi_string_set(ctx->protocol_name, canonical_protocol); flipper_format_rewind(ctx->flipper_format); flipper_format_insert_or_update_string_cstr( ctx->flipper_format, EMU_PRESET_KEY_PROTOCOL, canonical_protocol); } flipper_format_rewind(ctx->flipper_format); if(!flipper_format_read_uint32(ctx->flipper_format, EMU_PRESET_KEY_SERIAL, &ctx->serial, 1)) { FURI_LOG_W(TAG, "Failed to read serial"); ctx->serial = 0; } flipper_format_rewind(ctx->flipper_format); uint32_t btn_temp = 0; if(flipper_format_read_uint32(ctx->flipper_format, EMU_PRESET_KEY_BTN, &btn_temp, 1)) { ctx->original_button = (uint8_t)btn_temp; } flipper_format_rewind(ctx->flipper_format); if(flipper_format_read_uint32( ctx->flipper_format, EMU_PRESET_KEY_CNT, &ctx->original_counter, 1)) { ctx->current_counter = ctx->original_counter; } if(furi_string_equal(ctx->protocol_name, FIAT_V1_PROTOCOL_NAME)) { uint8_t raw[13] = {0}; bool have_raw = false; flipper_format_rewind(ctx->flipper_format); if(flipper_format_read_hex(ctx->flipper_format, "Raw", raw, sizeof(raw)) && raw[0] == 0x00U && raw[1] == 0x01U) { have_raw = true; } if(ctx->serial == 0U) { if(have_raw) { ctx->serial = ((uint32_t)raw[2] << 24U) | ((uint32_t)raw[3] << 16U) | ((uint32_t)raw[4] << 8U) | raw[5]; } else { uint8_t key_bytes[8] = {0}; flipper_format_rewind(ctx->flipper_format); if(flipper_format_read_hex( ctx->flipper_format, "Key", key_bytes, sizeof(key_bytes))) { ctx->serial = ((uint32_t)key_bytes[0] << 24U) | ((uint32_t)key_bytes[1] << 16U) | ((uint32_t)key_bytes[2] << 8U) | key_bytes[3]; } } if(ctx->serial != 0U) { flipper_format_rewind(ctx->flipper_format); flipper_format_insert_or_update_uint32( ctx->flipper_format, EMU_PRESET_KEY_SERIAL, &ctx->serial, 1); } } if(have_raw && (ctx->current_counter > 0x3FFU || ctx->original_counter > 0x3FFU)) { const uint32_t ctrl = ((uint32_t)(raw[6] & 0x0FU) << 6U) | ((uint32_t)raw[7] >> 2U); ctx->original_counter = ctrl; ctx->current_counter = ctrl; flipper_format_rewind(ctx->flipper_format); flipper_format_insert_or_update_uint32( ctx->flipper_format, EMU_PRESET_KEY_CNT, &ctrl, 1); if(ctx->original_button == 0U) { ctx->original_button = (uint8_t)(raw[6] >> 4U); uint32_t btn = ctx->original_button; flipper_format_rewind(ctx->flipper_format); flipper_format_insert_or_update_uint32( ctx->flipper_format, EMU_PRESET_KEY_BTN, &btn, 1); } } } view_set_draw_callback(app->view_about, emulate_draw_callback); view_set_input_callback(app->view_about, emulate_input_callback); view_set_context(app->view_about, app); view_set_previous_callback(app->view_about, NULL); if(emulate_needs_hitag2_prompt(ctx)) { if(!emulate_prompt_hitag2_key(app, ctx)) { FURI_LOG_E(TAG, "Failed to show HITAG2 key input"); notification_message(app->notifications, &sequence_error); emulate_context_free(); emulate_request_nav_pop(app); } return; } view_dispatcher_switch_to_view(app->view_dispatcher, ProtoPirateViewAbout); } static bool plugin_on_event(void* context, SceneManagerEvent event) { #define INVALID_PRESET "Cannot set TX power on this preset." ProtoPirateApp* app = context; bool consumed = false; EmulateContext* ctx = emulate_context; if(event.type == SceneManagerEventTypeCustom) { switch(event.event) { case ProtoPirateCustomEventEmulateTransmit: if(ctx && ctx->flipper_format) { if(app->txrx->txrx_state == ProtoPirateTxRxStateTx) { FURI_LOG_W(TAG, "Previous transmission still active, stopping it"); if(app->txrx->radio_device) { subghz_devices_stop_async_tx(app->txrx->radio_device); } subghz_transmitter_stop(ctx->transmitter); furi_delay_ms(10); if(app->txrx->radio_device) { subghz_devices_idle(app->txrx->radio_device); } app->txrx->txrx_state = ProtoPirateTxRxStateIDLE; } emulate_context_reset_transmitter(); if(!emulate_context_try_init_transmitter(app, ctx)) { FURI_LOG_E(TAG, "No transmitter available"); ctx->is_transmitting = false; notification_message(app->notifications, &sequence_error); consumed = true; break; } EmulateResolvedPreset resolved_preset; if(!emulate_context_resolve_tx_preset(app, ctx, &resolved_preset)) { FURI_LOG_E(TAG, "No preset data available - cannot transmit"); ctx->is_transmitting = false; notification_message(app->notifications, &sequence_error); consumed = true; break; } uint8_t* preset_data = resolved_preset.data; uint8_t preset_patch_buffer[TX_PRESET_PATCH_MAX_SIZE]; if(preset_data) { if(!emulate_radio_ready(app)) { FURI_LOG_W(TAG, "Radio went cold before TX, reinitializing"); if(!g_host_api->radio_init || !g_host_api->radio_init(app)) { emulate_resolved_preset_release(&resolved_preset); emulate_context_reset_transmitter(); ctx->is_transmitting = false; notification_message(app->notifications, &sequence_error); consumed = true; break; } } if(app->tx_power) { bool patchable = true; if(!resolved_preset.should_free) { patchable = resolved_preset.size <= sizeof(preset_patch_buffer); if(patchable) { memcpy(preset_patch_buffer, preset_data, resolved_preset.size); preset_data = preset_patch_buffer; } } uint8_t preset_offset = 0; if(!patchable || !get_tx_preset_byte( preset_data, resolved_preset.size, &preset_offset)) { FURI_LOG_I(TAG, INVALID_PRESET); } else { uint8_t fm_byte = preset_data[preset_offset]; uint8_t am_byte = preset_data[preset_offset + 1]; if(fm_byte && am_byte) { FURI_LOG_I(TAG, INVALID_PRESET); } else if(fm_byte) { FURI_LOG_I(TAG, "FM PA table found."); preset_data[preset_offset] = tx_power_value[app->tx_power]; } else if(am_byte) { FURI_LOG_I(TAG, "AM PA table found."); preset_data[preset_offset + 1] = tx_power_value[TX_PRESET_VALUES_AM + app->tx_power]; } else { FURI_LOG_I(TAG, INVALID_PRESET); } } } subghz_devices_reset(app->txrx->radio_device); subghz_devices_idle(app->txrx->radio_device); subghz_devices_load_preset( app->txrx->radio_device, FuriHalSubGhzPresetCustom, preset_data); subghz_devices_set_frequency(app->txrx->radio_device, ctx->freq); subghz_devices_set_tx(app->txrx->radio_device); app->start_tx_time = furi_get_tick(); if(subghz_devices_start_async_tx( app->txrx->radio_device, subghz_transmitter_yield, ctx->transmitter)) { app->txrx->txrx_state = ProtoPirateTxRxStateTx; notification_message(app->notifications, &emu_sequence_tx); notification_message(app->notifications, &sequence_blink_magenta_10); FURI_LOG_I( TAG, "Started transmission: freq=%lu file_Preset=\"%s\" short=\"%s\"", (unsigned long)ctx->freq, ctx->preset_from_file ? furi_string_get_cstr(ctx->preset_from_file) : "?", ctx->preset); } else { FURI_LOG_E(TAG, "Failed to start async TX"); emulate_context_reset_transmitter(); ctx->is_transmitting = false; subghz_devices_idle(app->txrx->radio_device); notification_message(app->notifications, &sequence_error); } } else { FURI_LOG_E(TAG, "No preset data available - cannot transmit"); ctx->is_transmitting = false; notification_message(app->notifications, &sequence_error); } emulate_resolved_preset_release(&resolved_preset); } consumed = true; break; case ProtoPirateCustomEventEmulateStop: FURI_LOG_I(TAG, "Stop event received, txrx_state=%d", app->txrx->txrx_state); if(app->txrx->txrx_state == ProtoPirateTxRxStateTx && ctx) { if((furi_get_tick() - app->start_tx_time) > emulate_min_tx_time(ctx)) { emu_stop_tx(app); ctx->is_transmitting = false; } else { ctx->flag_stop_called = true; } } consumed = true; break; case ProtoPirateCustomEventEmulateExit: if(app->txrx->txrx_state == ProtoPirateTxRxStateTx) { emu_stop_tx(app); if(ctx) { ctx->is_transmitting = false; ctx->flag_stop_called = false; } } emulate_request_nav_after_exit(app); consumed = true; break; } } else if(event.type == SceneManagerEventTypeTick) { view_commit_model(app->view_about, true); if(ctx && ctx->is_transmitting) { if(app->txrx->txrx_state == ProtoPirateTxRxStateTx) { if((app->start_tx_time && ((furi_get_tick() - app->start_tx_time) > emulate_min_tx_time(ctx))) && ctx->flag_stop_called) { emu_stop_tx(app); ctx->is_transmitting = false; ctx->flag_stop_called = false; } else { notification_message(app->notifications, &sequence_blink_magenta_10); } } } consumed = true; } return consumed; } static void plugin_on_exit(void* context) { ProtoPirateApp* app = context; if(!app) { emulate_context_free(); return; } // Stop any active transmission before tearing down callbacks. if(app->txrx && app->txrx->txrx_state == ProtoPirateTxRxStateTx) { FURI_LOG_I(TAG, "Stopping transmission on exit"); if(app->txrx->radio_device) { subghz_devices_stop_async_tx(app->txrx->radio_device); } else { FURI_LOG_W(TAG, "Emulate exit saw TX state without radio device"); } if(emulate_context && emulate_context->transmitter) { subghz_transmitter_stop(emulate_context->transmitter); } furi_delay_ms(10); if(app->txrx->radio_device) { subghz_devices_idle(app->txrx->radio_device); } app->txrx->txrx_state = ProtoPirateTxRxStateIDLE; } else if(app->txrx && app->txrx->txrx_state != ProtoPirateTxRxStateIDLE) { if(g_host_api && g_host_api->idle) g_host_api->idle(app); } emulate_context_free(); if(g_host_api && g_host_api->storage_delete_temp) g_host_api->storage_delete_temp(); if(app->radio_initialized && app->txrx && app->txrx->environment && app->txrx->preset && app->txrx->preset->data && app->txrx->preset->name && g_host_api && g_host_api->apply_protocol_registry_for_context) { const char* preset_name = furi_string_get_cstr(app->txrx->preset->name); if(preset_name) { if(!g_host_api->apply_protocol_registry_for_context( app, preset_name, app->txrx->preset->frequency, app->txrx->preset->data, app->txrx->preset->data_size, NULL)) { FURI_LOG_W(TAG, "Failed to restore session protocol registry on emulate exit"); } } } if(app->view_about) { view_set_draw_callback(app->view_about, NULL); view_set_input_callback(app->view_about, NULL); view_set_context(app->view_about, NULL); } if(app->notifications) { notification_message_block(app->notifications, &sequence_blink_stop); } } static void plugin_context_release(void* context) { UNUSED(context); emulate_context_free(); } static void plugin_set_host_api(const ProtoPirateEmulateHostApi* host_api) { g_host_api = host_api; } static const ProtoPirateEmulatePlugin protopirate_emulate_plugin = { .plugin_name = "ProtoPirate Emulate", .set_host_api = plugin_set_host_api, .on_enter = plugin_on_enter, .on_event = plugin_on_event, .on_exit = plugin_on_exit, .context_release = plugin_context_release, }; static const FlipperAppPluginDescriptor protopirate_emulate_plugin_descriptor = { .appid = PROTOPIRATE_EMULATE_PLUGIN_APP_ID, .ep_api_version = PROTOPIRATE_EMULATE_PLUGIN_API_VERSION, .entry_point = &protopirate_emulate_plugin, }; const FlipperAppPluginDescriptor* protopirate_emulate_plugin_ep(void) { return &protopirate_emulate_plugin_descriptor; } #endif // ENABLE_EMULATE_FEATURE