Просмотр исходного кода

Add nfc_apdu_runner from https://github.com/SpenserCai/nfc_apdu_runner

git-subtree-dir: nfc_apdu_runner
git-subtree-mainline: 06797dfbe1f313a33036297f927dc002ff4b22c2
git-subtree-split: a3ca4c1d5cd522a7bdc2f4c4614a0b18bc5ea0a2
Willy-JL 9 месяцев назад
Родитель
Сommit
b1d5116c5d

+ 7 - 0
nfc_apdu_runner/.gitignore

@@ -0,0 +1,7 @@
+dist/*
+.vscode
+.clang-format
+.clangd
+.editorconfig
+.env
+.ufbt

+ 1 - 0
nfc_apdu_runner/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/SpenserCai/nfc_apdu_runner main fap 54e0170b4ba184cb62842bf6af1c345c9f5947b4

+ 27 - 0
nfc_apdu_runner/application.fam

@@ -0,0 +1,27 @@
+'''
+Author: SpenserCai
+Date: 2025-02-28 17:52:49
+version: 
+LastEditors: SpenserCai
+LastEditTime: 2025-03-13 13:06:31
+Description: file content
+'''
+
+# For details & more options, see documentation/AppManifests.md in firmware repo
+
+App(
+    appid="nfc_apdu_runner",  # Must be unique
+    name="NFC Apdu Runner",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="nfc_apdu_runner_app",
+    stack_size=2 * 1024,
+    fap_category="NFC",
+    targets=["f7"],
+    # Optional values
+    fap_version="0.3",
+    fap_icon="images/nfc_apdu_runner_10px.png",  # 10x10 1-bit PNG
+    fap_description="Run APDU commands from script files",
+    fap_author="SpenserCai",
+    fap_weburl="https://github.com/SpenserCai/nfc_apdu_runner",
+    fap_icon_assets="images",  # Image assets to compile for this application
+)

+ 0 - 0
nfc_apdu_runner/images/.gitkeep


BIN
nfc_apdu_runner/images/Loading_24.png


BIN
nfc_apdu_runner/images/nfc_apdu_runner_10px.png


+ 13 - 0
nfc_apdu_runner/images/nfc_manual_60x50.h

@@ -0,0 +1,13 @@
+/*
+ * @Author: SpenserCai
+ * @Date: 2025-03-07 16:22:29
+ * @version: 
+ * @LastEditors: SpenserCai
+ * @LastEditTime: 2025-03-07 16:34:05
+ * @Description: file content
+ */
+#pragma once
+
+#include <gui/icon.h>
+
+extern const Icon I_NFC_manual_60x50;

BIN
nfc_apdu_runner/images/nfc_manual_60x50.png


+ 769 - 0
nfc_apdu_runner/nfc_apdu_runner.c

@@ -0,0 +1,769 @@
+/*
+ * @Author: SpenserCai
+ * @Date: 2025-02-28 17:52:49
+ * @version: 
+ * @LastEditors: SpenserCai
+ * @LastEditTime: 2025-03-16 19:59:42
+ * @Description: file content
+ */
+#include "nfc_apdu_runner.h"
+#include "scenes/nfc_apdu_runner_scene.h"
+
+// 菜单项枚举
+typedef enum {
+    NfcApduRunnerSubmenuIndexLoadFile,
+    NfcApduRunnerSubmenuIndexViewLogs,
+    NfcApduRunnerSubmenuIndexAbout,
+} NfcApduRunnerSubmenuIndex;
+
+// 前向声明
+static void nfc_apdu_runner_free(NfcApduRunner* app);
+static NfcApduRunner* nfc_apdu_runner_alloc();
+static void nfc_apdu_runner_init(NfcApduRunner* app);
+static bool nfc_apdu_runner_custom_event_callback(void* context, uint32_t event);
+static bool nfc_apdu_runner_back_event_callback(void* context);
+static void nfc_apdu_runner_tick_event_callback(void* context);
+
+// 释放APDU脚本资源
+void nfc_apdu_script_free(NfcApduScript* script) {
+    if(script == NULL) return;
+
+    for(uint32_t i = 0; i < script->command_count; i++) {
+        free(script->commands[i]);
+    }
+    free(script);
+}
+
+// 释放APDU响应资源
+void nfc_apdu_responses_free(NfcApduResponse* responses, uint32_t count) {
+    if(responses == NULL) return;
+
+    for(uint32_t i = 0; i < count; i++) {
+        free(responses[i].command);
+        free(responses[i].response);
+    }
+    free(responses);
+}
+
+// 解析卡类型字符串
+static CardType parse_card_type(const char* type_str) {
+    if(strcmp(type_str, "iso14443_3a") == 0) {
+        return CardTypeIso14443_3a;
+    } else if(strcmp(type_str, "iso14443_3b") == 0) {
+        return CardTypeIso14443_3b;
+    } else if(strcmp(type_str, "iso14443_4a") == 0) {
+        return CardTypeIso14443_4a;
+    } else if(strcmp(type_str, "iso14443_4b") == 0) {
+        return CardTypeIso14443_4b;
+    } else {
+        return CardTypeUnknown;
+    }
+}
+
+// 解析APDU脚本文件
+NfcApduScript* nfc_apdu_script_parse(Storage* storage, const char* file_path) {
+    FURI_LOG_I("APDU_DEBUG", "开始解析脚本文件: %s", file_path);
+
+    if(!storage || !file_path) {
+        FURI_LOG_E("APDU_DEBUG", "无效的存储或文件路径");
+        return NULL;
+    }
+
+    NfcApduScript* script = malloc(sizeof(NfcApduScript));
+    if(!script) {
+        FURI_LOG_E("APDU_DEBUG", "分配脚本内存失败");
+        return NULL;
+    }
+
+    memset(script, 0, sizeof(NfcApduScript));
+
+    FuriString* temp_str = furi_string_alloc();
+    if(!temp_str) {
+        FURI_LOG_E("APDU_DEBUG", "分配临时字符串内存失败");
+        free(script);
+        return NULL;
+    }
+
+    File* file = storage_file_alloc(storage);
+    if(!file) {
+        FURI_LOG_E("APDU_DEBUG", "分配文件失败");
+        furi_string_free(temp_str);
+        free(script);
+        return NULL;
+    }
+
+    bool success = false;
+    bool in_data_section = false;
+
+    do {
+        if(!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) {
+            FURI_LOG_E("APDU_DEBUG", "打开文件失败: %s", file_path);
+            break;
+        }
+
+        // 读取整个文件内容到缓冲区
+        char file_buf[1024];
+        size_t bytes_read = storage_file_read(file, file_buf, sizeof(file_buf) - 1);
+        if(bytes_read <= 0) {
+            FURI_LOG_E("APDU_DEBUG", "读取文件内容失败");
+            break;
+        }
+        file_buf[bytes_read] = '\0';
+        FURI_LOG_I("APDU_DEBUG", "读取文件内容成功, 大小: %zu 字节", bytes_read);
+
+        // 解析文件内容
+        char* line = file_buf;
+        char* next_line;
+
+        // 读取文件类型行
+        next_line = strchr(line, '\n');
+        if(!next_line) {
+            FURI_LOG_E("APDU_DEBUG", "文件格式错误,找不到换行符");
+            break;
+        }
+        *next_line = '\0';
+
+        if(strncmp(line, "Filetype: APDU Script", 21) != 0) {
+            FURI_LOG_E("APDU_DEBUG", "无效的文件类型: %s", line);
+            break;
+        }
+
+        // 读取版本行
+        line = next_line + 1;
+        next_line = strchr(line, '\n');
+        if(!next_line) {
+            FURI_LOG_E("APDU_DEBUG", "文件格式错误,找不到版本行结束");
+            break;
+        }
+        *next_line = '\0';
+
+        if(strncmp(line, "Version: 1", 10) != 0) {
+            FURI_LOG_E("APDU_DEBUG", "无效的版本: %s", line);
+            break;
+        }
+
+        // 读取卡类型行
+        line = next_line + 1;
+        next_line = strchr(line, '\n');
+        if(!next_line) {
+            FURI_LOG_E("APDU_DEBUG", "文件格式错误,找不到卡类型行结束");
+            break;
+        }
+        *next_line = '\0';
+
+        if(strncmp(line, "CardType: ", 10) != 0) {
+            FURI_LOG_E("APDU_DEBUG", "无效的卡类型行: %s", line);
+            break;
+        }
+
+        // 提取卡类型
+        const char* card_type_str = line + 10; // 跳过 "CardType: "
+        script->card_type = parse_card_type(card_type_str);
+
+        if(script->card_type == CardTypeUnknown) {
+            FURI_LOG_E("APDU_DEBUG", "不支持的卡类型: %s", card_type_str);
+            break;
+        }
+        FURI_LOG_I("APDU_DEBUG", "卡类型解析成功: %s", card_type_str);
+
+        // 读取数据行
+        line = next_line + 1;
+        next_line = strchr(line, '\n');
+
+        // 检查当前行是否为 Data 行
+        if(strncmp(line, "Data: [", 7) != 0) {
+            FURI_LOG_E("APDU_DEBUG", "无效的数据行: %s", line);
+            break;
+        }
+
+        in_data_section = true;
+
+        // 解析命令数组
+        char* data_str = line + 7; // 跳过 "Data: ["
+
+        // 处理多行格式的命令
+        // 将整个文件内容中的所有换行符和空格替换为空格,以便正确解析命令
+        char* p = data_str;
+        while(*p) {
+            if(*p == '\n' || *p == '\r') {
+                *p = ' '; // 将换行符替换为空格
+            }
+            p++;
+        }
+
+        // 清理多余的空格,使解析更加稳健
+        p = data_str;
+        char* q = data_str;
+        bool in_quotes = false;
+
+        while(*p) {
+            // 在引号内保留所有字符
+            if(*p == '"') {
+                in_quotes = !in_quotes;
+                *q++ = *p++;
+                continue;
+            }
+
+            // 在引号外,跳过空格
+            if(!in_quotes && (*p == ' ' || *p == '\t')) {
+                p++;
+                continue;
+            }
+
+            // 复制其他字符
+            *q++ = *p++;
+        }
+        *q = '\0'; // 确保字符串正确终止
+
+        // 查找第一个引号
+        char* command_start = strchr(data_str, '"');
+        if(!command_start) {
+            FURI_LOG_E("APDU_DEBUG", "未找到命令开始引号");
+            break;
+        }
+
+        while(command_start != NULL && script->command_count < MAX_APDU_COMMANDS) {
+            command_start++; // 跳过开始的引号
+
+            // 查找结束引号
+            char* command_end = strchr(command_start, '"');
+            if(!command_end) {
+                FURI_LOG_E("APDU_DEBUG", "未找到命令结束引号");
+                break;
+            }
+
+            size_t command_len = command_end - command_start;
+
+            if(command_len == 0) {
+                // 空命令,跳过
+                FURI_LOG_W("APDU_DEBUG", "空命令,跳过");
+                command_start = strchr(command_end + 1, '"');
+                continue;
+            }
+
+            // 验证命令是否只包含十六进制字符
+            bool valid_hex = true;
+            for(size_t i = 0; i < command_len; i++) {
+                char c = command_start[i];
+                if(!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) {
+                    valid_hex = false;
+                    break;
+                }
+            }
+
+            if(!valid_hex) {
+                FURI_LOG_E(
+                    "APDU_DEBUG", "无效的十六进制命令: %.*s", (int)command_len, command_start);
+                command_start = strchr(command_end + 1, '"');
+                continue;
+            }
+
+            // 命令长度必须是偶数
+            if(command_len % 2 != 0) {
+                FURI_LOG_E(
+                    "APDU_DEBUG", "命令长度必须是偶数: %.*s", (int)command_len, command_start);
+                command_start = strchr(command_end + 1, '"');
+                continue;
+            }
+
+            // 分配内存并复制命令
+            script->commands[script->command_count] = malloc(command_len + 1);
+            if(!script->commands[script->command_count]) {
+                FURI_LOG_E("APDU_DEBUG", "分配命令内存失败");
+                break;
+            }
+
+            strncpy(script->commands[script->command_count], command_start, command_len);
+            script->commands[script->command_count][command_len] = '\0';
+            script->command_count++;
+
+            // 查找下一个命令
+            command_start = strchr(command_end + 1, '"');
+        }
+
+        success = in_data_section && (script->command_count > 0);
+        FURI_LOG_I("APDU_DEBUG", "命令解析完成, 命令数: %lu", script->command_count);
+    } while(false);
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_string_free(temp_str);
+
+    if(!success) {
+        FURI_LOG_E("APDU_DEBUG", "解析失败,释放脚本资源");
+        nfc_apdu_script_free(script);
+        return NULL;
+    }
+
+    FURI_LOG_I("APDU_DEBUG", "脚本解析成功");
+    return script;
+}
+
+// 保存APDU响应结果
+bool nfc_apdu_save_responses(
+    Storage* storage,
+    const char* file_path,
+    NfcApduResponse* responses,
+    uint32_t response_count,
+    const char* custom_save_path) {
+    FuriString* temp_str = furi_string_alloc();
+    FuriString* file_path_str = furi_string_alloc_set(file_path);
+    FuriString* response_path = furi_string_alloc();
+
+    // 构建响应文件路径
+    if(custom_save_path != NULL) {
+        // 使用自定义保存路径
+        furi_string_set(response_path, custom_save_path);
+    } else {
+        // 使用默认路径
+        FuriString* filename = furi_string_alloc();
+        path_extract_filename(file_path_str, filename, true);
+        furi_string_cat_printf(
+            response_path,
+            "%s/%s%s",
+            APP_DIRECTORY_PATH,
+            furi_string_get_cstr(filename),
+            RESPONSE_EXTENSION);
+        furi_string_free(filename);
+    }
+
+    File* file = storage_file_alloc(storage);
+    bool success = false;
+
+    do {
+        if(!storage_file_open(
+               file, furi_string_get_cstr(response_path), FSAM_WRITE, FSOM_CREATE_ALWAYS))
+            break;
+
+        // 写入文件头
+        furi_string_printf(temp_str, "Filetype: APDU Script Response\n");
+        if(!storage_file_write(file, furi_string_get_cstr(temp_str), furi_string_size(temp_str)))
+            break;
+
+        furi_string_printf(temp_str, "Response:\n");
+        if(!storage_file_write(file, furi_string_get_cstr(temp_str), furi_string_size(temp_str)))
+            break;
+
+        // 写入每个命令和响应
+        for(uint32_t i = 0; i < response_count; i++) {
+            furi_string_printf(temp_str, "In: %s\n", responses[i].command);
+            if(!storage_file_write(
+                   file, furi_string_get_cstr(temp_str), furi_string_size(temp_str)))
+                break;
+
+            furi_string_printf(temp_str, "Out: ");
+            if(!storage_file_write(
+                   file, furi_string_get_cstr(temp_str), furi_string_size(temp_str)))
+                break;
+
+            // 将响应数据转换为十六进制字符串
+            for(uint16_t j = 0; j < responses[i].response_length; j++) {
+                furi_string_printf(temp_str, "%02X", responses[i].response[j]);
+                if(!storage_file_write(
+                       file, furi_string_get_cstr(temp_str), furi_string_size(temp_str)))
+                    break;
+            }
+
+            furi_string_printf(temp_str, "\n");
+            if(!storage_file_write(
+                   file, furi_string_get_cstr(temp_str), furi_string_size(temp_str)))
+                break;
+        }
+
+        success = true;
+    } while(false);
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_string_free(temp_str);
+    furi_string_free(file_path_str);
+    furi_string_free(response_path);
+
+    return success;
+}
+
+// 添加日志
+void add_log_entry(NfcApduRunner* app, const char* message, bool is_error) {
+    if(app->log_count >= MAX_LOG_ENTRIES) {
+        // 移除最旧的日志
+        free(app->log_entries[0].message);
+        for(uint32_t i = 0; i < app->log_count - 1; i++) {
+            app->log_entries[i] = app->log_entries[i + 1];
+        }
+        app->log_count--;
+    }
+
+    app->log_entries[app->log_count].message = strdup(message);
+    app->log_entries[app->log_count].is_error = is_error;
+    app->log_count++;
+}
+
+// 释放日志资源
+void free_logs(NfcApduRunner* app) {
+    if(!app) {
+        return;
+    }
+
+    if(!app->log_entries) {
+        return;
+    }
+
+    for(uint32_t i = 0; i < app->log_count; i++) {
+        if(app->log_entries[i].message) {
+            free(app->log_entries[i].message);
+            app->log_entries[i].message = NULL;
+        }
+    }
+    app->log_count = 0;
+}
+
+// 视图分发器回调
+static bool nfc_apdu_runner_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    NfcApduRunner* app = context;
+
+    bool handled = scene_manager_handle_custom_event(app->scene_manager, event);
+
+    return handled;
+}
+
+static bool nfc_apdu_runner_back_event_callback(void* context) {
+    furi_assert(context);
+    NfcApduRunner* app = context;
+
+    // 直接交给场景管理器处理返回事件
+    bool handled = scene_manager_handle_back_event(app->scene_manager);
+
+    return handled;
+}
+
+static void nfc_apdu_runner_tick_event_callback(void* context) {
+    furi_assert(context);
+    NfcApduRunner* app = context;
+
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+// 分配应用程序资源
+static NfcApduRunner* nfc_apdu_runner_alloc() {
+    NfcApduRunner* app = malloc(sizeof(NfcApduRunner));
+    if(!app) {
+        return NULL;
+    }
+
+    memset(app, 0, sizeof(NfcApduRunner));
+
+    // 分配GUI资源
+    app->gui = furi_record_open(RECORD_GUI);
+    if(!app->gui) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->view_dispatcher = view_dispatcher_alloc();
+    if(!app->view_dispatcher) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->scene_manager = scene_manager_alloc(&nfc_apdu_runner_scene_handlers, app);
+    if(!app->scene_manager) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->submenu = submenu_alloc();
+    if(!app->submenu) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+    if(!app->dialogs) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->widget = widget_alloc();
+    if(!app->widget) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->text_box = text_box_alloc();
+    if(!app->text_box) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->text_box_store = furi_string_alloc();
+    if(!app->text_box_store) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->popup = popup_alloc();
+    if(!app->popup) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->button_menu = button_menu_alloc();
+    if(!app->button_menu) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->text_input = text_input_alloc();
+    if(!app->text_input) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    // 分配NFC资源
+    app->nfc = nfc_alloc();
+    if(!app->nfc) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    // 分配NFC Worker
+    app->worker = nfc_worker_alloc(app->nfc);
+    if(!app->worker) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    // 分配存储资源
+    app->storage = furi_record_open(RECORD_STORAGE);
+    if(!app->storage) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->file_path = furi_string_alloc();
+    if(!app->file_path) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    // 设置默认目录为APP_DIRECTORY_PATH
+    furi_string_set(app->file_path, APP_DIRECTORY_PATH);
+
+    // 分配日志资源
+    app->log_entries = malloc(sizeof(LogEntry) * MAX_LOG_ENTRIES);
+    if(!app->log_entries) {
+        nfc_apdu_runner_free(app);
+        return NULL;
+    }
+
+    app->log_count = 0;
+
+    return app;
+}
+
+// 释放应用程序资源
+static void nfc_apdu_runner_free(NfcApduRunner* app) {
+    if(!app) {
+        return;
+    }
+
+    // 释放脚本和响应资源
+    if(app->script) {
+        nfc_apdu_script_free(app->script);
+        app->script = NULL;
+    }
+
+    if(app->responses) {
+        nfc_apdu_responses_free(app->responses, app->response_count);
+        app->responses = NULL;
+        app->response_count = 0;
+    }
+
+    // 先释放所有视图
+    if(app->submenu) {
+        view_dispatcher_remove_view(app->view_dispatcher, NfcApduRunnerViewSubmenu);
+        submenu_free(app->submenu);
+        app->submenu = NULL;
+    }
+
+    if(app->widget) {
+        view_dispatcher_remove_view(app->view_dispatcher, NfcApduRunnerViewWidget);
+        widget_free(app->widget);
+        app->widget = NULL;
+    }
+
+    if(app->text_box) {
+        view_dispatcher_remove_view(app->view_dispatcher, NfcApduRunnerViewTextBox);
+        text_box_free(app->text_box);
+        app->text_box = NULL;
+    }
+
+    if(app->text_box_store) {
+        furi_string_free(app->text_box_store);
+        app->text_box_store = NULL;
+    }
+
+    if(app->popup) {
+        view_dispatcher_remove_view(app->view_dispatcher, NfcApduRunnerViewPopup);
+        popup_free(app->popup);
+        app->popup = NULL;
+    }
+
+    if(app->button_menu) {
+        button_menu_free(app->button_menu);
+        app->button_menu = NULL;
+    }
+
+    if(app->text_input) {
+        view_dispatcher_remove_view(app->view_dispatcher, NfcApduRunnerViewTextInput);
+        text_input_free(app->text_input);
+        app->text_input = NULL;
+    }
+
+    // 关闭记录
+    if(app->dialogs) {
+        furi_record_close(RECORD_DIALOGS);
+        app->dialogs = NULL;
+    }
+
+    if(app->gui) {
+        furi_record_close(RECORD_GUI);
+        app->gui = NULL;
+    }
+
+    // 释放NFC资源
+    if(app->nfc) {
+        nfc_free(app->nfc);
+        app->nfc = NULL;
+    }
+
+    // 释放NFC Worker
+    if(app->worker) {
+        nfc_worker_free(app->worker);
+        app->worker = NULL;
+    }
+
+    // 释放存储资源
+    if(app->file_path) {
+        furi_string_free(app->file_path);
+        app->file_path = NULL;
+    }
+
+    if(app->storage) {
+        furi_record_close(RECORD_STORAGE);
+        app->storage = NULL;
+    }
+
+    // 释放日志资源
+    free_logs(app);
+
+    if(app->log_entries) {
+        free(app->log_entries);
+        app->log_entries = NULL;
+    }
+
+    // 先释放场景管理器
+    if(app->scene_manager) {
+        scene_manager_free(app->scene_manager);
+        app->scene_manager = NULL;
+    }
+
+    // 最后处理视图分发器
+    if(app->view_dispatcher) {
+        view_dispatcher_free(app->view_dispatcher);
+        app->view_dispatcher = NULL;
+        FURI_LOG_I("APDU_DEBUG", "View dispatcher freed");
+    }
+
+    // 释放应用程序资源
+    free(app);
+}
+
+// 初始化应用程序
+static void nfc_apdu_runner_init(NfcApduRunner* app) {
+    if(!app) {
+        return;
+    }
+
+    // 检查视图分发器是否已分配
+    if(!app->view_dispatcher) {
+        return;
+    }
+
+    // 配置视图分发器
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, nfc_apdu_runner_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, nfc_apdu_runner_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, nfc_apdu_runner_tick_event_callback, 100);
+
+    // 检查GUI是否已分配
+    if(!app->gui) {
+        return;
+    }
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // 检查视图组件是否已分配
+    if(!app->submenu || !app->widget || !app->text_box || !app->popup || !app->text_input) {
+        return;
+    }
+
+    // 添加视图
+    view_dispatcher_add_view(
+        app->view_dispatcher, NfcApduRunnerViewSubmenu, submenu_get_view(app->submenu));
+    view_dispatcher_add_view(
+        app->view_dispatcher, NfcApduRunnerViewWidget, widget_get_view(app->widget));
+    view_dispatcher_add_view(
+        app->view_dispatcher, NfcApduRunnerViewTextBox, text_box_get_view(app->text_box));
+    view_dispatcher_add_view(
+        app->view_dispatcher, NfcApduRunnerViewPopup, popup_get_view(app->popup));
+    view_dispatcher_add_view(
+        app->view_dispatcher, NfcApduRunnerViewTextInput, text_input_get_view(app->text_input));
+
+    // 配置文本框
+    text_box_set_font(app->text_box, TextBoxFontText);
+
+    // 检查存储是否已分配
+    if(!app->storage) {
+        return;
+    }
+
+    // 确保应用目录存在
+    if(!storage_dir_exists(app->storage, APP_DIRECTORY_PATH)) {
+        if(!storage_simply_mkdir(app->storage, APP_DIRECTORY_PATH)) {
+            FURI_LOG_E("APDU_DEBUG", "无法创建应用目录");
+            return;
+        }
+    }
+
+    // 启动场景管理器
+    scene_manager_next_scene(app->scene_manager, NfcApduRunnerSceneStart);
+}
+
+// 应用程序入口点
+int32_t nfc_apdu_runner_app(void* p) {
+    UNUSED(p);
+    furi_hal_power_suppress_charge_enter();
+    // 分配应用程序资源
+    NfcApduRunner* app = nfc_apdu_runner_alloc();
+    if(!app) {
+        return -1;
+    }
+
+    // 初始化应用程序
+    nfc_apdu_runner_init(app);
+
+    // 运行事件循环
+    view_dispatcher_run(app->view_dispatcher);
+
+    // 释放应用程序资源
+    nfc_apdu_runner_free(app);
+
+    furi_hal_power_suppress_charge_exit();
+
+    return 0;
+}

+ 188 - 0
nfc_apdu_runner/nfc_apdu_runner.h

@@ -0,0 +1,188 @@
+/*
+ * @Author: SpenserCai
+ * @Date: 2025-02-28 18:02:04
+ * @version: 
+ * @LastEditors: SpenserCai
+ * @LastEditTime: 2025-03-13 12:22:41
+ * @Description: file content
+ */
+#pragma once
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/dialog_ex.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/button_menu.h>
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+#include <nfc/nfc.h>
+#include <nfc/nfc_device.h>
+#include <nfc/nfc_poller.h>
+#include <nfc/protocols/iso14443_3a/iso14443_3a_poller.h>
+#include <nfc/protocols/iso14443_3b/iso14443_3b_poller.h>
+#include <nfc/protocols/iso14443_4a/iso14443_4a_poller.h>
+#include <nfc/protocols/iso14443_4b/iso14443_4b_poller.h>
+#include <bit_lib/bit_lib.h>
+#include <toolbox/hex.h>
+#include <toolbox/path.h>
+
+#define APP_DIRECTORY_PATH "/ext/apps_data/nfc_apdu_runner"
+#define FILE_EXTENSION     ".apduscr"
+#define RESPONSE_EXTENSION ".apdures"
+#define MAX_APDU_COMMANDS  50
+#define MAX_APDU_LENGTH    256
+#define MAX_LOG_ENTRIES    100
+#define MAX_FILENAME_LEN   64
+
+// 自定义事件枚举
+typedef enum {
+    NfcApduRunnerCustomEventPopupClosed = 0,
+    NfcApduRunnerCustomEventViewExit,
+} NfcApduRunnerCustomEvent;
+
+// 卡类型枚举
+typedef enum {
+    CardTypeIso14443_3a,
+    CardTypeIso14443_3b,
+    CardTypeIso14443_4a,
+    CardTypeIso14443_4b,
+    CardTypeUnknown,
+} CardType;
+
+// 应用程序视图枚举
+typedef enum {
+    NfcApduRunnerViewSubmenu,
+    NfcApduRunnerViewWidget,
+    NfcApduRunnerViewTextBox,
+    NfcApduRunnerViewPopup,
+    NfcApduRunnerViewTextInput,
+} NfcApduRunnerView;
+
+// 日志条目结构
+typedef struct {
+    char* message;
+    bool is_error;
+} LogEntry;
+
+// APDU脚本文件结构
+typedef struct {
+    CardType card_type;
+    char* commands[MAX_APDU_COMMANDS];
+    uint32_t command_count;
+} NfcApduScript;
+
+// APDU响应结构
+typedef struct {
+    char* command;
+    uint8_t* response;
+    uint16_t response_length;
+} NfcApduResponse;
+
+// NFC Worker状态枚举
+typedef enum {
+    NfcWorkerStateReady, // 准备就绪
+    NfcWorkerStateDetecting, // 正在检测卡片
+    NfcWorkerStateRunning, // 正在执行APDU命令
+    NfcWorkerStateStop // 停止
+} NfcWorkerState;
+
+// NFC Worker事件枚举
+typedef enum {
+    NfcWorkerEventSuccess, // 操作成功
+    NfcWorkerEventFail, // 操作失败
+    NfcWorkerEventCardDetected, // 检测到卡片
+    NfcWorkerEventCardLost, // 卡片丢失
+    NfcWorkerEventAborted, // 操作被中止
+    NfcWorkerEventStop, // 停止事件
+} NfcWorkerEvent;
+
+// NFC Worker回调函数类型
+typedef void (*NfcWorkerCallback)(NfcWorkerEvent event, void* context);
+
+// NFC Worker结构体
+typedef struct NfcWorker NfcWorker;
+
+// 应用程序状态结构
+typedef struct {
+    // GUI
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+    Submenu* submenu;
+    DialogsApp* dialogs;
+    Widget* widget;
+    TextBox* text_box;
+    FuriString* text_box_store;
+    Popup* popup;
+    ButtonMenu* button_menu;
+    TextInput* text_input;
+
+    // NFC
+    Nfc* nfc;
+    NfcWorker* worker; // 添加NFC Worker
+
+    // 文件
+    Storage* storage;
+    FuriString* file_path;
+    NfcApduScript* script;
+    NfcApduResponse* responses;
+    uint32_t response_count;
+    char file_name_buf[MAX_FILENAME_LEN + 1];
+
+    // 日志
+    LogEntry* log_entries;
+    uint32_t log_count;
+
+    // 状态
+    bool running;
+    bool card_lost;
+} NfcApduRunner;
+
+// 释放APDU脚本资源
+void nfc_apdu_script_free(NfcApduScript* script);
+
+// 释放APDU响应资源
+void nfc_apdu_responses_free(NfcApduResponse* responses, uint32_t count);
+
+// 解析APDU脚本文件
+NfcApduScript* nfc_apdu_script_parse(Storage* storage, const char* file_path);
+
+// 保存APDU响应结果
+bool nfc_apdu_save_responses(
+    Storage* storage,
+    const char* file_path,
+    NfcApduResponse* responses,
+    uint32_t response_count,
+    const char* custom_save_path);
+
+// 添加日志
+void add_log_entry(NfcApduRunner* app, const char* message, bool is_error);
+
+// 释放日志资源
+void free_logs(NfcApduRunner* app);
+
+// 应用程序入口点
+int32_t nfc_apdu_runner_app(void* p);
+
+// NFC Worker函数声明
+NfcWorker* nfc_worker_alloc(Nfc* nfc);
+void nfc_worker_free(NfcWorker* worker);
+void nfc_worker_start(
+    NfcWorker* worker,
+    NfcWorkerState state,
+    NfcApduScript* script,
+    NfcWorkerCallback callback,
+    void* context);
+void nfc_worker_stop(NfcWorker* worker);
+bool nfc_worker_is_running(NfcWorker* worker);
+void nfc_worker_get_responses(
+    NfcWorker* worker,
+    NfcApduResponse** out_responses,
+    uint32_t* out_response_count);
+const char* nfc_worker_get_error_message(NfcWorker* worker);

+ 763 - 0
nfc_apdu_runner/nfc_worker.c

@@ -0,0 +1,763 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include "nfc_apdu_runner.h"
+#include <lib/nfc/nfc.h>
+#include <lib/nfc/protocols/iso14443_4a/iso14443_4a.h>
+#include <lib/nfc/protocols/iso14443_4a/iso14443_4a_poller.h>
+
+#define TAG "NfcWorker"
+
+// APDU上下文结构体,用于在回调中传递APDU命令和结果
+typedef struct {
+    BitBuffer* tx_buffer; // 发送缓冲区
+    BitBuffer* rx_buffer; // 接收缓冲区
+    bool ready; // 是否准备好发送
+    bool is_last; // 是否是最后一条指令
+    FuriThreadId thread_id; // 线程ID,用于发送标志
+} ApduContext;
+
+// 定义线程标志
+#define APDU_POLLER_DONE (1 << 0)
+#define APDU_POLLER_ERR  (1 << 1)
+
+// NFC Worker结构体定义
+struct NfcWorker {
+    FuriThread* thread;
+    FuriStreamBuffer* stream;
+
+    NfcWorkerState state;
+    NfcWorkerCallback callback;
+    void* context;
+
+    Nfc* nfc;
+    NfcPoller* poller;
+    NfcApduScript* script;
+    NfcApduResponse* responses;
+    uint32_t response_count;
+
+    bool running;
+    bool card_detected;
+    bool card_lost;
+
+    char* error_message;
+};
+
+// 记录APDU数据的辅助函数
+static void
+    nfc_worker_log_apdu_data(const char* cmd, const uint8_t* response, uint16_t response_len) {
+    FURI_LOG_I(TAG, "APDU命令: %s", cmd);
+    if(response && response_len > 0) {
+        FuriString* resp_str = furi_string_alloc();
+        for(size_t i = 0; i < response_len; i++) {
+            furi_string_cat_printf(resp_str, "%02X", response[i]);
+        }
+        FURI_LOG_I(TAG, "APDU响应: %s", furi_string_get_cstr(resp_str));
+        furi_string_free(resp_str);
+    } else {
+        FURI_LOG_I(TAG, "APDU响应: 无");
+    }
+}
+
+// NFC轮询器回调函数
+static NfcCommand nfc_worker_poller_callback(NfcGenericEvent event, void* context) {
+    NfcWorker* worker = context;
+
+    // 检查事件类型
+    if(event.protocol == NfcProtocolIso14443_4a) {
+        const Iso14443_4aPollerEvent* iso14443_4a_event = event.event_data;
+
+        if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeReady) {
+            FURI_LOG_I(TAG, "ISO14443-4A卡检测成功");
+            // 只有当脚本中指定的卡类型是ISO14443-4A时,才认为检测到了卡片
+            if(worker->script && worker->script->card_type == CardTypeIso14443_4a &&
+               !worker->card_detected) {
+                worker->card_detected = true;
+                worker->callback(NfcWorkerEventCardDetected, worker->context);
+                // 检测到卡片后立即返回停止命令,避免重复触发
+                return NfcCommandStop;
+            } else if(worker->card_detected) {
+                // 如果已经检测到卡片,不再重复触发回调
+                FURI_LOG_D(TAG, "已经检测到ISO14443-4A卡,忽略重复事件");
+            } else {
+                FURI_LOG_I(TAG, "检测到ISO14443-4A卡,但脚本要求的是其他类型");
+            }
+        } else if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeError) {
+            FURI_LOG_E(TAG, "ISO14443-4A卡检测失败,错误码: %d", iso14443_4a_event->data->error);
+        }
+    } else if(event.protocol == NfcProtocolIso14443_4b) {
+        // 处理ISO14443-4B协议事件
+        // 注意:这里假设ISO14443-4B的事件结构与ISO14443-4A类似
+        const Iso14443_4bPollerEvent* iso14443_4b_event = event.event_data;
+
+        if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeReady) {
+            FURI_LOG_I(TAG, "ISO14443-4B卡检测成功");
+            // 只有当脚本中指定的卡类型是ISO14443-4B时,才认为检测到了卡片
+            if(worker->script && worker->script->card_type == CardTypeIso14443_4b &&
+               !worker->card_detected) {
+                worker->card_detected = true;
+                worker->callback(NfcWorkerEventCardDetected, worker->context);
+                // 检测到卡片后立即返回停止命令,避免重复触发
+                return NfcCommandStop;
+            } else if(worker->card_detected) {
+                // 如果已经检测到卡片,不再重复触发回调
+                FURI_LOG_D(TAG, "已经检测到ISO14443-4B卡,忽略重复事件");
+            } else {
+                FURI_LOG_I(TAG, "检测到ISO14443-4B卡,但脚本要求的是其他类型");
+            }
+        } else if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeError) {
+            FURI_LOG_E(TAG, "ISO14443-4B卡检测失败,错误码: %d", iso14443_4b_event->data->error);
+        }
+    } else {
+        FURI_LOG_I(TAG, "收到未知协议事件: %d", event.protocol);
+    }
+
+    // 继续操作
+    return NfcCommandContinue;
+}
+
+// 执行APDU命令的回调函数
+static NfcCommand nfc_worker_apdu_poller_callback(NfcGenericEvent event, void* context) {
+    UNUSED(event);
+    furi_assert(context);
+
+    ApduContext* apdu_ctx = (ApduContext*)context;
+
+    // 如果准备好发送,则处理命令
+    if(apdu_ctx->ready) {
+        // 设置为未准备好,避免重复处理
+        apdu_ctx->ready = false;
+
+        FURI_LOG_I(TAG, "APDU回调函数处理命令");
+
+        // 根据协议类型发送数据块
+        if(event.protocol == NfcProtocolIso14443_4a) {
+            // 发送数据块
+            Iso14443_4aError error = iso14443_4a_poller_send_block(
+                (Iso14443_4aPoller*)event.instance, apdu_ctx->tx_buffer, apdu_ctx->rx_buffer);
+
+            if(error == Iso14443_4aErrorNone) {
+                // 发送成功,通知线程
+                furi_thread_flags_set(apdu_ctx->thread_id, APDU_POLLER_DONE);
+
+                // 如果是最后一条指令,停止轮询器
+                if(apdu_ctx->is_last) {
+                    return NfcCommandStop;
+                }
+
+                return NfcCommandContinue;
+            } else {
+                // 发送失败,通知线程
+                FURI_LOG_E(TAG, "ISO14443-4A命令执行失败,错误码: %d", error);
+                furi_thread_flags_set(apdu_ctx->thread_id, APDU_POLLER_ERR);
+                return NfcCommandStop;
+            }
+        } else if(event.protocol == NfcProtocolIso14443_4b) {
+            // 发送数据块
+            Iso14443_4bError error = iso14443_4b_poller_send_block(
+                (Iso14443_4bPoller*)event.instance, apdu_ctx->tx_buffer, apdu_ctx->rx_buffer);
+
+            if(error == Iso14443_4bErrorNone) {
+                // 发送成功,通知线程
+                furi_thread_flags_set(apdu_ctx->thread_id, APDU_POLLER_DONE);
+
+                // 如果是最后一条指令,停止轮询器
+                if(apdu_ctx->is_last) {
+                    return NfcCommandStop;
+                }
+
+                return NfcCommandContinue;
+            } else {
+                // 发送失败,通知线程
+                FURI_LOG_E(TAG, "ISO14443-4B命令执行失败,错误码: %d", error);
+                furi_thread_flags_set(apdu_ctx->thread_id, APDU_POLLER_ERR);
+                return NfcCommandStop;
+            }
+        } else {
+            // 不支持的协议
+            FURI_LOG_E(TAG, "不支持的协议: %d", event.protocol);
+            furi_thread_flags_set(apdu_ctx->thread_id, APDU_POLLER_ERR);
+            return NfcCommandStop;
+        }
+    } else {
+        // 未准备好,等待
+        furi_delay_ms(10);
+    }
+
+    return NfcCommandContinue;
+}
+
+// 执行APDU命令的线程函数
+static int32_t nfc_worker_apdu_thread(void* context) {
+    NfcWorker* worker = context;
+    FURI_LOG_I(TAG, "APDU执行线程启动");
+
+    // 根据脚本中指定的卡类型创建相应的轮询器
+    NfcProtocol protocol;
+    if(worker->script) {
+        switch(worker->script->card_type) {
+        case CardTypeIso14443_3a:
+            protocol = NfcProtocolIso14443_3a;
+            FURI_LOG_I(TAG, "使用ISO14443-3A协议");
+            break;
+        case CardTypeIso14443_3b:
+            protocol = NfcProtocolIso14443_3b;
+            FURI_LOG_I(TAG, "使用ISO14443-3B协议");
+            break;
+        case CardTypeIso14443_4a:
+            protocol = NfcProtocolIso14443_4a;
+            FURI_LOG_I(TAG, "使用ISO14443-4A协议");
+            break;
+        case CardTypeIso14443_4b:
+            protocol = NfcProtocolIso14443_4b;
+            FURI_LOG_I(TAG, "使用ISO14443-4B协议");
+            break;
+        default:
+            FURI_LOG_E(TAG, "不支持的卡类型: %d", worker->script->card_type);
+            worker->callback(NfcWorkerEventFail, worker->context);
+            return -1;
+        }
+    } else {
+        // 如果没有脚本,默认使用ISO14443-4A
+        protocol = NfcProtocolIso14443_4a;
+        FURI_LOG_I(TAG, "没有指定脚本,默认使用ISO14443-4A协议");
+    }
+
+    // 创建新的轮询器用于执行APDU命令
+    worker->poller = nfc_poller_alloc(worker->nfc, protocol);
+    if(!worker->poller) {
+        FURI_LOG_E(TAG, "分配APDU执行轮询器失败,协议: %d", protocol);
+        worker->callback(NfcWorkerEventFail, worker->context);
+        return -1;
+    }
+
+    FURI_LOG_I(TAG, "APDU执行轮询器分配成功");
+
+    // 创建APDU上下文
+    ApduContext apdu_ctx;
+    memset(&apdu_ctx, 0, sizeof(ApduContext));
+    apdu_ctx.tx_buffer = bit_buffer_alloc(MAX_APDU_LENGTH * 8);
+    apdu_ctx.rx_buffer = bit_buffer_alloc(MAX_APDU_LENGTH * 8);
+    apdu_ctx.ready = false;
+    apdu_ctx.is_last = false;
+    apdu_ctx.thread_id = furi_thread_get_current_id();
+
+    // 分配响应内存
+    worker->responses = malloc(sizeof(NfcApduResponse) * worker->script->command_count);
+    if(!worker->responses) {
+        FURI_LOG_E(TAG, "分配响应内存失败");
+        worker->callback(NfcWorkerEventFail, worker->context);
+
+        // 释放资源
+        bit_buffer_free(apdu_ctx.tx_buffer);
+        bit_buffer_free(apdu_ctx.rx_buffer);
+
+        // 停止轮询器
+        if(worker->poller) {
+            nfc_poller_stop(worker->poller);
+            nfc_poller_free(worker->poller);
+            worker->poller = NULL;
+        }
+
+        return -1;
+    }
+
+    memset(worker->responses, 0, sizeof(NfcApduResponse) * worker->script->command_count);
+
+    // 启动轮询器
+    nfc_poller_start(worker->poller, nfc_worker_apdu_poller_callback, &apdu_ctx);
+
+    FURI_LOG_I(TAG, "APDU执行轮询器启动成功");
+
+    // 等待轮询器初始化完成
+    furi_delay_ms(100);
+
+    FURI_LOG_I(TAG, "开始执行APDU命令,共 %lu 条", worker->script->command_count);
+
+    bool is_error = false;
+    uint32_t response_count = 0;
+
+    // 循环执行每条APDU命令
+    for(uint32_t i = 0; i < worker->script->command_count && worker->running; i++) {
+        const char* cmd = worker->script->commands[i];
+        FURI_LOG_I(TAG, "准备执行APDU命令 %lu: %s", i, cmd);
+
+        // 检查命令长度是否超过限制
+        size_t cmd_len = strlen(cmd);
+        if(cmd_len / 2 > MAX_APDU_LENGTH) {
+            FURI_LOG_E(
+                TAG, "APDU命令长度超过限制(%zu > %d): %s", cmd_len / 2, MAX_APDU_LENGTH, cmd);
+
+            // 设置错误信息
+            worker->error_message = malloc(100);
+            if(worker->error_message) {
+                snprintf(
+                    worker->error_message,
+                    100,
+                    "Command too long\n%d bytes max\nCommand: %lu",
+                    MAX_APDU_LENGTH,
+                    i + 1);
+            }
+
+            is_error = true;
+            // 立即调用失败回调,而不是等到线程结束
+            worker->callback(NfcWorkerEventFail, worker->context);
+            break;
+        }
+
+        // 解析APDU命令
+        uint8_t apdu_data[MAX_APDU_LENGTH];
+        uint16_t apdu_len = 0;
+
+        // 将十六进制字符串转换为字节数组
+        for(size_t j = 0; j < cmd_len; j += 2) {
+            if(j + 1 >= cmd_len) break;
+
+            char hex[3] = {cmd[j], cmd[j + 1], '\0'};
+            apdu_data[apdu_len++] = (uint8_t)strtol(hex, NULL, 16);
+
+            if(apdu_len >= MAX_APDU_LENGTH) break;
+        }
+
+        // 清空缓冲区
+        bit_buffer_reset(apdu_ctx.tx_buffer);
+        bit_buffer_reset(apdu_ctx.rx_buffer);
+
+        // 将APDU数据复制到发送缓冲区
+        bit_buffer_copy_bytes(apdu_ctx.tx_buffer, apdu_data, apdu_len);
+
+        // 保存命令
+        worker->responses[response_count].command = strdup(cmd);
+
+        // 设置是否是最后一条指令
+        apdu_ctx.is_last = (i == worker->script->command_count - 1);
+
+        // 设置准备好发送
+        apdu_ctx.ready = true;
+
+        // 等待命令执行完成或出错
+        uint32_t flags =
+            furi_thread_flags_wait(APDU_POLLER_DONE | APDU_POLLER_ERR, FuriFlagWaitAny, 3000);
+
+        if(flags & APDU_POLLER_ERR) {
+            // 命令执行出错
+            FURI_LOG_E(TAG, "APDU命令执行出错");
+
+            // 设置错误信息
+            worker->error_message = malloc(100);
+            if(worker->error_message) {
+                snprintf(worker->error_message, 100, "APDU command failed\nCommand: %lu", i + 1);
+            }
+
+            worker->responses[response_count].response = NULL;
+            worker->responses[response_count].response_length = 0;
+            is_error = true;
+            // 立即调用失败回调,而不是等到线程结束
+            worker->callback(NfcWorkerEventFail, worker->context);
+            response_count++;
+            break;
+        } else if(flags & APDU_POLLER_DONE) {
+            // 命令执行成功
+            size_t rx_bytes_count = bit_buffer_get_size_bytes(apdu_ctx.rx_buffer);
+
+            if(rx_bytes_count > 0) {
+                // 分配响应内存
+                uint8_t* response = malloc(rx_bytes_count);
+                if(!response) {
+                    FURI_LOG_E(TAG, "分配响应内存失败");
+                    worker->responses[response_count].response = NULL;
+                    worker->responses[response_count].response_length = 0;
+                    is_error = true;
+                    response_count++;
+                    break;
+                }
+
+                // 复制响应数据
+                bit_buffer_write_bytes(apdu_ctx.rx_buffer, response, rx_bytes_count);
+
+                // 保存响应
+                worker->responses[response_count].response = response;
+                worker->responses[response_count].response_length = rx_bytes_count;
+
+                // 记录APDU命令和响应
+                nfc_worker_log_apdu_data(cmd, response, rx_bytes_count);
+
+                FURI_LOG_I(TAG, "APDU命令 %lu 执行成功", i);
+            } else {
+                // 没有响应
+                FURI_LOG_W(TAG, "APDU命令 %lu 执行成功,但没有响应", i);
+                worker->responses[response_count].response = NULL;
+                worker->responses[response_count].response_length = 0;
+            }
+
+            response_count++;
+        } else {
+            // 超时
+            FURI_LOG_E(TAG, "APDU命令执行超时");
+
+            // 设置错误信息
+            worker->error_message = malloc(100);
+            if(worker->error_message) {
+                snprintf(worker->error_message, 100, "Command timeout\nCommand: %lu", i + 1);
+            }
+
+            worker->responses[response_count].response = NULL;
+            worker->responses[response_count].response_length = 0;
+            is_error = true;
+            // 立即调用失败回调,而不是等到线程结束
+            worker->callback(NfcWorkerEventFail, worker->context);
+            response_count++;
+            break;
+        }
+
+        // 命令之间添加短暂延迟
+        furi_delay_ms(50);
+
+        // 检查是否应该退出
+        if(!worker->running) {
+            FURI_LOG_I(TAG, "用户取消操作");
+            is_error = true;
+            break;
+        }
+    }
+
+    // 更新响应计数
+    worker->response_count = response_count;
+
+    // 释放资源
+    bit_buffer_free(apdu_ctx.tx_buffer);
+    bit_buffer_free(apdu_ctx.rx_buffer);
+
+    // 停止轮询器
+    if(worker->poller) {
+        FURI_LOG_I(TAG, "APDU命令执行完成,停止轮询器");
+        nfc_poller_stop(worker->poller);
+        nfc_poller_free(worker->poller);
+        worker->poller = NULL;
+    }
+
+    // 返回结果
+    if(!worker->running && !is_error) {
+        // 只有在用户取消操作且没有其他错误时才触发中止事件
+        FURI_LOG_I(TAG, "用户取消操作");
+        worker->callback(NfcWorkerEventAborted, worker->context);
+        return -1;
+    } else if(!is_error) {
+        // 没有错误时触发成功事件
+        FURI_LOG_I(TAG, "执行成功");
+        worker->callback(NfcWorkerEventSuccess, worker->context);
+        return 0;
+    } else {
+        // 错误已经在上面触发了回调,这里不需要再次触发
+        FURI_LOG_E(TAG, "执行失败");
+        return -1;
+    }
+}
+
+// 检测卡片的线程函数
+static int32_t nfc_worker_detect_thread(void* context) {
+    NfcWorker* worker = context;
+
+    FURI_LOG_I(TAG, "NFC Worker线程启动");
+
+    // 根据脚本中指定的卡类型创建相应的轮询器
+    NfcProtocol protocol;
+    if(worker->script) {
+        switch(worker->script->card_type) {
+        case CardTypeIso14443_3a:
+            protocol = NfcProtocolIso14443_3a;
+            FURI_LOG_I(TAG, "使用ISO14443-3A协议");
+            break;
+        case CardTypeIso14443_3b:
+            protocol = NfcProtocolIso14443_3b;
+            FURI_LOG_I(TAG, "使用ISO14443-3B协议");
+            break;
+        case CardTypeIso14443_4a:
+            protocol = NfcProtocolIso14443_4a;
+            FURI_LOG_I(TAG, "使用ISO14443-4A协议");
+            break;
+        case CardTypeIso14443_4b:
+            protocol = NfcProtocolIso14443_4b;
+            FURI_LOG_I(TAG, "使用ISO14443-4B协议");
+            break;
+        default:
+            FURI_LOG_E(TAG, "不支持的卡类型: %d", worker->script->card_type);
+            worker->callback(NfcWorkerEventFail, worker->context);
+            return -1;
+        }
+    } else {
+        // 如果没有脚本,默认使用ISO14443-4A
+        protocol = NfcProtocolIso14443_4a;
+        FURI_LOG_I(TAG, "没有指定脚本,默认使用ISO14443-4A协议");
+    }
+
+    // 重置卡片检测标志
+    worker->card_detected = false;
+
+    // 创建NFC轮询器用于检测卡片
+    worker->poller = nfc_poller_alloc(worker->nfc, protocol);
+    if(!worker->poller) {
+        FURI_LOG_E(TAG, "分配轮询器失败,协议: %d", protocol);
+        worker->callback(NfcWorkerEventFail, worker->context);
+        return -1;
+    }
+
+    FURI_LOG_I(TAG, "NFC轮询器分配成功");
+
+    // 启动轮询器,使用回调
+    nfc_poller_start(worker->poller, nfc_worker_poller_callback, worker);
+
+    FURI_LOG_I(TAG, "NFC轮询器启动成功");
+
+    // 等待卡片,持续检测直到找到卡片或用户取消
+    FURI_LOG_I(TAG, "等待卡片");
+
+    // 持续等待,直到检测到卡片或用户取消
+    uint32_t attempt = 0;
+    while(!worker->card_detected && worker->running) {
+        attempt++;
+        if(attempt % 5 == 0) {
+            FURI_LOG_I(TAG, "等待卡片中... (%lu秒)", attempt / 2);
+            // 通知用户放置卡片
+            worker->callback(NfcWorkerEventCardLost, worker->context);
+        }
+
+        // 使用延迟来等待卡片
+        furi_delay_ms(500);
+
+        // 检查是否应该退出
+        if(!worker->running) {
+            FURI_LOG_I(TAG, "用户取消操作");
+            // 只有在没有错误信息的情况下才触发中止回调
+            if(!worker->error_message) {
+                worker->callback(NfcWorkerEventAborted, worker->context);
+            }
+
+            // 停止轮询器
+            if(worker->poller) {
+                nfc_poller_stop(worker->poller);
+                nfc_poller_free(worker->poller);
+                worker->poller = NULL;
+            }
+
+            return -1;
+        }
+    }
+
+    // 如果没有检测到卡片,通知用户
+    if(!worker->card_detected) {
+        FURI_LOG_E(TAG, "未检测到卡片");
+        worker->callback(NfcWorkerEventFail, worker->context);
+
+        // 停止轮询器
+        if(worker->poller) {
+            nfc_poller_stop(worker->poller);
+            nfc_poller_free(worker->poller);
+            worker->poller = NULL;
+        }
+
+        return -1;
+    }
+
+    // 检测到卡片后,立即停止检测轮询器
+    if(worker->poller) {
+        FURI_LOG_I(TAG, "检测到卡片,停止检测轮询器");
+        nfc_poller_stop(worker->poller);
+        nfc_poller_free(worker->poller);
+        worker->poller = NULL;
+    }
+
+    // 如果只是检测卡片,到这里就完成了
+    if(worker->state == NfcWorkerStateDetecting) {
+        worker->callback(NfcWorkerEventSuccess, worker->context);
+        return 0;
+    }
+
+    // 如果需要执行APDU命令,继续执行
+    if(worker->state == NfcWorkerStateRunning && worker->card_detected) {
+        // 短暂延迟,确保之前的轮询器完全停止
+        furi_delay_ms(500);
+
+        // 启动APDU执行线程
+        FuriThread* apdu_thread =
+            furi_thread_alloc_ex("NfcWorkerAPDUThread", 8192, nfc_worker_apdu_thread, worker);
+        furi_thread_start(apdu_thread);
+
+        // 等待APDU执行线程完成
+        furi_thread_join(apdu_thread);
+        furi_thread_free(apdu_thread);
+
+        // 返回结果
+        if(!worker->running) {
+            FURI_LOG_I(TAG, "用户取消操作");
+            // 只有在没有错误信息的情况下才触发中止回调
+            if(!worker->error_message) {
+                worker->callback(NfcWorkerEventAborted, worker->context);
+            }
+            return -1;
+        } else {
+            FURI_LOG_I(TAG, "执行成功");
+            worker->callback(NfcWorkerEventSuccess, worker->context);
+            return 0;
+        }
+    }
+
+    return 0;
+}
+
+// 分配NFC Worker
+NfcWorker* nfc_worker_alloc(Nfc* nfc) {
+    NfcWorker* worker = malloc(sizeof(NfcWorker));
+
+    worker->thread = NULL;
+    worker->stream = furi_stream_buffer_alloc(sizeof(NfcWorkerEvent), 8);
+    worker->nfc = nfc;
+    worker->poller = NULL;
+    worker->script = NULL;
+    worker->responses = NULL;
+    worker->response_count = 0;
+    worker->running = false;
+    worker->card_detected = false;
+    worker->card_lost = false;
+    worker->error_message = NULL;
+
+    return worker;
+}
+
+// 释放NFC Worker
+void nfc_worker_free(NfcWorker* worker) {
+    furi_assert(worker);
+
+    // 确保先停止工作线程
+    nfc_worker_stop(worker);
+
+    // 释放响应资源
+    if(worker->responses) {
+        for(uint32_t i = 0; i < worker->response_count; i++) {
+            if(worker->responses[i].command) {
+                free(worker->responses[i].command);
+            }
+            if(worker->responses[i].response) {
+                free(worker->responses[i].response);
+            }
+        }
+        free(worker->responses);
+        worker->responses = NULL;
+        worker->response_count = 0;
+    }
+
+    // 释放错误信息
+    if(worker->error_message) {
+        free(worker->error_message);
+        worker->error_message = NULL;
+    }
+
+    // 释放流缓冲区
+    if(worker->stream) {
+        furi_stream_buffer_free(worker->stream);
+        worker->stream = NULL;
+    }
+
+    // 释放工作线程
+    if(worker->thread) {
+        furi_thread_free(worker->thread);
+        worker->thread = NULL;
+    }
+
+    // 释放工作器本身
+    free(worker);
+}
+
+// 启动NFC Worker
+void nfc_worker_start(
+    NfcWorker* worker,
+    NfcWorkerState state,
+    NfcApduScript* script,
+    NfcWorkerCallback callback,
+    void* context) {
+    furi_assert(worker);
+    furi_assert(callback);
+
+    worker->state = state;
+    worker->script = script;
+    worker->callback = callback;
+    worker->context = context;
+    worker->running = true;
+    worker->card_detected = false;
+    worker->card_lost = false;
+
+    worker->thread = furi_thread_alloc_ex("NfcWorker", 8192, nfc_worker_detect_thread, worker);
+    furi_thread_start(worker->thread);
+}
+
+// 停止NFC Worker
+void nfc_worker_stop(NfcWorker* worker) {
+    furi_assert(worker);
+
+    if(!worker->running) {
+        // 已经停止,无需再次停止
+        return;
+    }
+
+    // 设置运行标志为false,通知线程退出
+    worker->running = false;
+
+    // 等待线程退出
+    if(worker->thread) {
+        furi_thread_join(worker->thread);
+        furi_thread_free(worker->thread);
+        worker->thread = NULL;
+    }
+
+    // 确保轮询器被正确释放
+    if(worker->poller) {
+        // 先停止轮询器
+        nfc_poller_stop(worker->poller);
+        // 短暂延迟,确保轮询器完全停止
+        furi_delay_ms(10);
+        // 然后释放轮询器
+        nfc_poller_free(worker->poller);
+        worker->poller = NULL;
+    }
+
+    // 重置状态
+    worker->card_detected = false;
+    worker->card_lost = false;
+    worker->state = NfcWorkerStateReady;
+
+    // 如果有错误信息,说明已经触发了错误回调,不需要再触发中止回调
+    if(worker->callback && !worker->error_message) {
+        worker->callback(NfcWorkerEventAborted, worker->context);
+    }
+
+    worker->callback = NULL;
+    worker->context = NULL;
+}
+
+// 检查NFC Worker是否正在运行
+bool nfc_worker_is_running(NfcWorker* worker) {
+    furi_assert(worker);
+    return worker->running;
+}
+
+// 获取NFC Worker的响应结果
+void nfc_worker_get_responses(
+    NfcWorker* worker,
+    NfcApduResponse** out_responses,
+    uint32_t* out_response_count) {
+    furi_assert(worker);
+    furi_assert(out_responses);
+    furi_assert(out_response_count);
+
+    *out_responses = worker->responses;
+    *out_response_count = worker->response_count;
+
+    // 清空worker中的响应,避免重复释放
+    worker->responses = NULL;
+    worker->response_count = 0;
+}
+
+// 获取NFC Worker的错误信息
+const char* nfc_worker_get_error_message(NfcWorker* worker) {
+    furi_assert(worker);
+    return worker->error_message;
+}

+ 52 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene.c

@@ -0,0 +1,52 @@
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+
+// 生成场景进入处理函数数组
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const nfc_apdu_runner_on_enter_handlers[])(void*) = {
+    nfc_apdu_runner_scene_start_on_enter,
+    nfc_apdu_runner_scene_file_select_on_enter,
+    nfc_apdu_runner_scene_card_placement_on_enter,
+    nfc_apdu_runner_scene_running_on_enter,
+    nfc_apdu_runner_scene_results_on_enter,
+    nfc_apdu_runner_scene_save_file_on_enter,
+    nfc_apdu_runner_scene_logs_on_enter,
+    nfc_apdu_runner_scene_about_on_enter,
+};
+#undef ADD_SCENE
+
+// 生成场景事件处理函数数组
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const nfc_apdu_runner_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+    nfc_apdu_runner_scene_start_on_event,
+    nfc_apdu_runner_scene_file_select_on_event,
+    nfc_apdu_runner_scene_card_placement_on_event,
+    nfc_apdu_runner_scene_running_on_event,
+    nfc_apdu_runner_scene_results_on_event,
+    nfc_apdu_runner_scene_save_file_on_event,
+    nfc_apdu_runner_scene_logs_on_event,
+    nfc_apdu_runner_scene_about_on_event,
+};
+#undef ADD_SCENE
+
+// 生成场景退出处理函数数组
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const nfc_apdu_runner_on_exit_handlers[])(void* context) = {
+    nfc_apdu_runner_scene_start_on_exit,
+    nfc_apdu_runner_scene_file_select_on_exit,
+    nfc_apdu_runner_scene_card_placement_on_exit,
+    nfc_apdu_runner_scene_running_on_exit,
+    nfc_apdu_runner_scene_results_on_exit,
+    nfc_apdu_runner_scene_save_file_on_exit,
+    nfc_apdu_runner_scene_logs_on_exit,
+    nfc_apdu_runner_scene_about_on_exit,
+};
+#undef ADD_SCENE
+
+// 初始化场景处理器配置结构
+const SceneManagerHandlers nfc_apdu_runner_scene_handlers = {
+    .on_enter_handlers = nfc_apdu_runner_on_enter_handlers,
+    .on_event_handlers = nfc_apdu_runner_on_event_handlers,
+    .on_exit_handlers = nfc_apdu_runner_on_exit_handlers,
+    .scene_num = NfcApduRunnerSceneCount,
+};

+ 51 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene.h

@@ -0,0 +1,51 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// 场景枚举
+typedef enum {
+    NfcApduRunnerSceneStart,
+    NfcApduRunnerSceneFileSelect,
+    NfcApduRunnerSceneCardPlacement,
+    NfcApduRunnerSceneRunning,
+    NfcApduRunnerSceneResults,
+    NfcApduRunnerSceneSaveFile,
+    NfcApduRunnerSceneLogs,
+    NfcApduRunnerSceneAbout,
+    NfcApduRunnerSceneCount,
+} NfcApduRunnerScene;
+
+extern const SceneManagerHandlers nfc_apdu_runner_scene_handlers;
+
+// 场景回调
+void nfc_apdu_runner_scene_start_on_enter(void* context);
+bool nfc_apdu_runner_scene_start_on_event(void* context, SceneManagerEvent event);
+void nfc_apdu_runner_scene_start_on_exit(void* context);
+
+void nfc_apdu_runner_scene_file_select_on_enter(void* context);
+bool nfc_apdu_runner_scene_file_select_on_event(void* context, SceneManagerEvent event);
+void nfc_apdu_runner_scene_file_select_on_exit(void* context);
+
+void nfc_apdu_runner_scene_card_placement_on_enter(void* context);
+bool nfc_apdu_runner_scene_card_placement_on_event(void* context, SceneManagerEvent event);
+void nfc_apdu_runner_scene_card_placement_on_exit(void* context);
+
+void nfc_apdu_runner_scene_running_on_enter(void* context);
+bool nfc_apdu_runner_scene_running_on_event(void* context, SceneManagerEvent event);
+void nfc_apdu_runner_scene_running_on_exit(void* context);
+
+void nfc_apdu_runner_scene_results_on_enter(void* context);
+bool nfc_apdu_runner_scene_results_on_event(void* context, SceneManagerEvent event);
+void nfc_apdu_runner_scene_results_on_exit(void* context);
+
+void nfc_apdu_runner_scene_logs_on_enter(void* context);
+bool nfc_apdu_runner_scene_logs_on_event(void* context, SceneManagerEvent event);
+void nfc_apdu_runner_scene_logs_on_exit(void* context);
+
+void nfc_apdu_runner_scene_about_on_enter(void* context);
+bool nfc_apdu_runner_scene_about_on_event(void* context, SceneManagerEvent event);
+void nfc_apdu_runner_scene_about_on_exit(void* context);
+
+void nfc_apdu_runner_scene_save_file_on_enter(void* context);
+bool nfc_apdu_runner_scene_save_file_on_event(void* context, SceneManagerEvent event);
+void nfc_apdu_runner_scene_save_file_on_exit(void* context);

+ 50 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene_about.c

@@ -0,0 +1,50 @@
+/*
+ * @Author: SpenserCai
+ * @Date: 2025-03-07 16:32:13
+ * @version: 
+ * @LastEditors: SpenserCai
+ * @LastEditTime: 2025-03-13 13:07:05
+ * @Description: file content
+ */
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+
+// 关于场景进入回调
+void nfc_apdu_runner_scene_about_on_enter(void* context) {
+    NfcApduRunner* app = context;
+    TextBox* text_box = app->text_box;
+
+    text_box_reset(text_box);
+    text_box_set_font(text_box, TextBoxFontText);
+
+    text_box_set_text(
+        text_box,
+        "NFC APDU Runner\n"
+        "Version: 0.3\n"
+        "Auther: SpenserCai\n\n"
+        "This app allows you to run APDU commands from script files.\n\n"
+        "Supported card types:\n"
+        "- ISO14443-4A\n"
+        "- ISO14443-4B\n\n"
+        "Place your script files in:\n" APP_DIRECTORY_PATH "\n\n"
+        "File format:\n"
+        "Filetype: APDU Script\n"
+        "Version: 1\n"
+        "CardType: iso14443_4a\n"
+        "Data: [\"APDU1\",\"APDU2\",...]\n");
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewTextBox);
+}
+
+// 关于场景事件回调
+bool nfc_apdu_runner_scene_about_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+// 关于场景退出回调
+void nfc_apdu_runner_scene_about_on_exit(void* context) {
+    NfcApduRunner* app = context;
+    text_box_reset(app->text_box);
+}

+ 69 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene_card_placement.c

@@ -0,0 +1,69 @@
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+#include <gui/elements.h>
+
+// 卡片放置场景按钮回调
+static void nfc_apdu_runner_scene_card_placement_widget_callback(
+    GuiButtonType result,
+    InputType type,
+    void* context) {
+    NfcApduRunner* app = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, result);
+    }
+}
+
+// 卡片放置场景进入回调
+void nfc_apdu_runner_scene_card_placement_on_enter(void* context) {
+    NfcApduRunner* app = context;
+    Widget* widget = app->widget;
+
+    widget_reset(widget);
+
+    // 添加图标 - 使用内置元素
+    widget_add_string_element(widget, 64, 5, AlignCenter, AlignTop, FontPrimary, "Place card on");
+    widget_add_string_element(
+        widget, 64, 20, AlignCenter, AlignTop, FontPrimary, "the Flipper's back");
+
+    // 添加按钮
+    widget_add_button_element(
+        widget,
+        GuiButtonTypeLeft,
+        "Back",
+        nfc_apdu_runner_scene_card_placement_widget_callback,
+        app);
+    widget_add_button_element(
+        widget,
+        GuiButtonTypeRight,
+        "Run",
+        nfc_apdu_runner_scene_card_placement_widget_callback,
+        app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewWidget);
+}
+
+// 卡片放置场景事件回调
+bool nfc_apdu_runner_scene_card_placement_on_event(void* context, SceneManagerEvent event) {
+    NfcApduRunner* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            // 返回上一个场景
+            scene_manager_previous_scene(app->scene_manager);
+            consumed = true;
+        } else if(event.event == GuiButtonTypeRight) {
+            // 进入运行场景
+            scene_manager_next_scene(app->scene_manager, NfcApduRunnerSceneRunning);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+// 卡片放置场景退出回调
+void nfc_apdu_runner_scene_card_placement_on_exit(void* context) {
+    NfcApduRunner* app = context;
+    widget_reset(app->widget);
+}

+ 49 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene_file_select.c

@@ -0,0 +1,49 @@
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+
+// 文件选择场景进入回调
+void nfc_apdu_runner_scene_file_select_on_enter(void* context) {
+    NfcApduRunner* app = context;
+    DialogsFileBrowserOptions browser_options;
+
+    dialog_file_browser_set_basic_options(&browser_options, FILE_EXTENSION, NULL);
+    browser_options.base_path = APP_DIRECTORY_PATH;
+    browser_options.hide_ext = false;
+
+    // 确保目录存在
+    Storage* storage = app->storage;
+    if(!storage_dir_exists(storage, APP_DIRECTORY_PATH)) {
+        if(!storage_simply_mkdir(storage, APP_DIRECTORY_PATH)) {
+            dialog_message_show_storage_error(app->dialogs, "Cannot create\napp folder");
+            scene_manager_previous_scene(app->scene_manager);
+            return;
+        }
+    }
+
+    // 设置初始路径为APP_DIRECTORY_PATH
+    furi_string_set(app->file_path, APP_DIRECTORY_PATH);
+
+    // 显示文件浏览器
+    bool success =
+        dialog_file_browser_show(app->dialogs, app->file_path, app->file_path, &browser_options);
+
+    if(success) {
+        scene_manager_next_scene(app->scene_manager, NfcApduRunnerSceneCardPlacement);
+    } else {
+        scene_manager_previous_scene(app->scene_manager);
+    }
+}
+
+// 文件选择场景事件回调
+bool nfc_apdu_runner_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+// 文件选择场景退出回调
+void nfc_apdu_runner_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 47 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene_logs.c

@@ -0,0 +1,47 @@
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+
+// 日志场景进入回调
+void nfc_apdu_runner_scene_logs_on_enter(void* context) {
+    NfcApduRunner* app = context;
+    TextBox* text_box = app->text_box;
+
+    text_box_reset(text_box);
+    text_box_set_font(text_box, TextBoxFontText);
+
+    FuriString* text = furi_string_alloc();
+
+    furi_string_cat_str(text, "Execution Log:\n\n");
+
+    for(uint32_t i = 0; i < app->log_count; i++) {
+        if(app->log_entries[i].is_error) {
+            furi_string_cat_str(text, "[ERROR] ");
+        } else {
+            furi_string_cat_str(text, "[INFO] ");
+        }
+        furi_string_cat_str(text, app->log_entries[i].message);
+        furi_string_cat_str(text, "\n");
+    }
+
+    if(app->log_count == 0) {
+        furi_string_cat_str(text, "No logs available.\nRun a script first.");
+    }
+
+    text_box_set_text(text_box, furi_string_get_cstr(text));
+    furi_string_free(text);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewTextBox);
+}
+
+// 日志场景事件回调
+bool nfc_apdu_runner_scene_logs_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+// 日志场景退出回调
+void nfc_apdu_runner_scene_logs_on_exit(void* context) {
+    NfcApduRunner* app = context;
+    text_box_reset(app->text_box);
+}

+ 98 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene_results.c

@@ -0,0 +1,98 @@
+/*
+ * @Author: SpenserCai
+ * @Date: 2025-03-07 16:31:44
+ * @version: 
+ * @LastEditors: SpenserCai
+ * @LastEditTime: 2025-03-11 09:59:00
+ * @Description: file content
+ */
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+
+// 结果场景按钮回调
+static void nfc_apdu_runner_scene_results_button_callback(
+    GuiButtonType result,
+    InputType type,
+    void* context) {
+    NfcApduRunner* app = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, result);
+    }
+}
+
+// 结果场景进入回调
+void nfc_apdu_runner_scene_results_on_enter(void* context) {
+    NfcApduRunner* app = context;
+    Widget* widget = app->widget;
+
+    widget_reset(widget);
+    widget_add_string_element(widget, 64, 5, AlignCenter, AlignTop, FontPrimary, "执行结果");
+
+    FuriString* text = furi_string_alloc();
+
+    for(uint32_t i = 0; i < app->response_count; i++) {
+        furi_string_cat_printf(text, "Command: %s\n", app->responses[i].command);
+        furi_string_cat_str(text, "Response: ");
+
+        for(uint16_t j = 0; j < app->responses[i].response_length; j++) {
+            furi_string_cat_printf(text, "%02X", app->responses[i].response[j]);
+        }
+
+        furi_string_cat_str(text, "\n\n");
+    }
+
+    widget_add_text_scroll_element(widget, 0, 16, 128, 35, furi_string_get_cstr(text));
+    furi_string_free(text);
+
+    // 添加按钮
+    widget_add_button_element(
+        widget, GuiButtonTypeLeft, "Cancel", nfc_apdu_runner_scene_results_button_callback, app);
+    widget_add_button_element(
+        widget, GuiButtonTypeRight, "Save", nfc_apdu_runner_scene_results_button_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewWidget);
+}
+
+// 结果场景事件回调
+bool nfc_apdu_runner_scene_results_on_event(void* context, SceneManagerEvent event) {
+    NfcApduRunner* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            // 取消按钮 - 返回到开始场景
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, NfcApduRunnerSceneStart);
+            consumed = true;
+        } else if(event.event == GuiButtonTypeRight) {
+            // 保存按钮 - 进入保存文件场景
+            // 设置场景状态,标记为进入保存文件场景
+            scene_manager_set_scene_state(
+                app->scene_manager, NfcApduRunnerSceneResults, NfcApduRunnerSceneSaveFile);
+            scene_manager_next_scene(app->scene_manager, NfcApduRunnerSceneSaveFile);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        // 返回键 - 返回到开始场景
+        scene_manager_search_and_switch_to_previous_scene(
+            app->scene_manager, NfcApduRunnerSceneStart);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+// 结果场景退出回调
+void nfc_apdu_runner_scene_results_on_exit(void* context) {
+    NfcApduRunner* app = context;
+    widget_reset(app->widget);
+
+    // 只有在不是进入保存文件场景时才释放响应资源
+    if(app->responses != NULL &&
+       scene_manager_get_scene_state(app->scene_manager, NfcApduRunnerSceneResults) !=
+           NfcApduRunnerSceneSaveFile) {
+        nfc_apdu_responses_free(app->responses, app->response_count);
+        app->responses = NULL;
+        app->response_count = 0;
+    }
+}

+ 209 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene_running.c

@@ -0,0 +1,209 @@
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+
+// NFC Worker回调函数
+static void nfc_worker_callback(NfcWorkerEvent event, void* context) {
+    NfcApduRunner* app = context;
+
+    // 发送自定义事件到视图分发器
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+// 运行场景弹出窗口回调
+static void nfc_apdu_runner_scene_running_popup_callback(void* context) {
+    NfcApduRunner* app = context;
+    FURI_LOG_I("APDU_DEBUG", "弹出窗口回调");
+    view_dispatcher_send_custom_event(app->view_dispatcher, NfcApduRunnerCustomEventPopupClosed);
+}
+
+// 显示错误消息并设置回调
+static void show_error_popup(NfcApduRunner* app, const char* message) {
+    FURI_LOG_I("APDU_DEBUG", "显示错误弹窗: %s", message);
+    Popup* popup = app->popup;
+    popup_reset(popup);
+    popup_set_text(popup, message, 64, 32, AlignCenter, AlignCenter);
+    popup_set_timeout(popup, 2000);
+    popup_set_callback(popup, nfc_apdu_runner_scene_running_popup_callback);
+    popup_set_context(popup, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewPopup);
+}
+
+// 显示等待卡片弹出窗口
+static void show_waiting_popup(NfcApduRunner* app) {
+    Popup* popup = app->popup;
+    popup_reset(popup);
+    popup_set_header(popup, "NFC APDU Runner", 64, 0, AlignCenter, AlignTop);
+    popup_set_text(
+        popup,
+        "Waiting for card...\nPlace card on Flipper's back",
+        64,
+        32,
+        AlignCenter,
+        AlignCenter);
+    popup_set_timeout(popup, 0);
+    popup_set_context(popup, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewPopup);
+}
+
+// 显示运行中弹出窗口
+static void show_running_popup(NfcApduRunner* app) {
+    Popup* popup = app->popup;
+    popup_reset(popup);
+    popup_set_header(popup, "Running APDU commands", 64, 0, AlignCenter, AlignTop);
+    popup_set_text(
+        popup, "Please wait...\nDo not remove the card", 64, 32, AlignCenter, AlignCenter);
+    popup_set_timeout(popup, 0);
+    popup_set_context(popup, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewPopup);
+}
+
+// 运行场景进入回调
+void nfc_apdu_runner_scene_running_on_enter(void* context) {
+    NfcApduRunner* app = context;
+    Popup* popup = app->popup;
+    FURI_LOG_I("APDU_DEBUG", "进入运行场景");
+
+    // 确保文件路径有效
+    if(furi_string_empty(app->file_path)) {
+        FURI_LOG_E("APDU_DEBUG", "文件路径为空");
+        show_error_popup(app, "Invalid file path");
+        return;
+    }
+    FURI_LOG_I("APDU_DEBUG", "文件路径: %s", furi_string_get_cstr(app->file_path));
+
+    // 显示正在运行的弹出窗口
+    FURI_LOG_I("APDU_DEBUG", "显示加载脚本弹窗");
+    popup_reset(popup);
+    popup_set_text(popup, "Loading script...\nPlease wait", 89, 44, AlignCenter, AlignCenter);
+    popup_set_timeout(popup, 0);
+    popup_set_context(popup, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewPopup);
+
+    // 解析脚本文件
+    FURI_LOG_I("APDU_DEBUG", "开始解析脚本文件");
+    app->script = nfc_apdu_script_parse(app->storage, furi_string_get_cstr(app->file_path));
+
+    if(app->script == NULL) {
+        FURI_LOG_E("APDU_DEBUG", "解析脚本文件失败");
+        show_error_popup(app, "Failed to parse script file");
+        return;
+    }
+    FURI_LOG_I("APDU_DEBUG", "脚本解析成功, 卡类型: %d", app->script->card_type);
+
+    // 检查卡类型是否支持
+    if(app->script->card_type == CardTypeUnknown) {
+        FURI_LOG_E("APDU_DEBUG", "不支持的卡类型");
+        show_error_popup(app, "Unsupported card type");
+        return;
+    }
+    FURI_LOG_I("APDU_DEBUG", "卡类型支持");
+
+    // 显示等待卡片弹出窗口
+    show_waiting_popup(app);
+
+    // 重置卡片丢失标志
+    app->card_lost = false;
+    // 设置运行标志为true,允许执行APDU命令
+    app->running = true;
+
+    // 启动NFC Worker
+    nfc_worker_start(app->worker, NfcWorkerStateRunning, app->script, nfc_worker_callback, app);
+}
+
+// 运行场景事件回调
+bool nfc_apdu_runner_scene_running_on_event(void* context, SceneManagerEvent event) {
+    NfcApduRunner* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        // 处理自定义事件
+        uint32_t custom_event = event.event;
+
+        if(custom_event == NfcWorkerEventCardDetected) {
+            FURI_LOG_I("APDU_DEBUG", "卡片检测成功事件");
+            show_running_popup(app);
+            consumed = true;
+        } else if(custom_event == NfcWorkerEventSuccess) {
+            FURI_LOG_I("APDU_DEBUG", "操作成功事件");
+
+            // 获取响应结果
+            nfc_worker_get_responses(app->worker, &app->responses, &app->response_count);
+
+            // 切换到结果场景
+            scene_manager_next_scene(app->scene_manager, NfcApduRunnerSceneResults);
+            consumed = true;
+        } else if(custom_event == NfcWorkerEventFail) {
+            FURI_LOG_E("APDU_DEBUG", "操作失败事件");
+            // 停止NFC Worker
+            nfc_worker_stop(app->worker);
+
+            // 获取自定义错误信息
+            const char* error_message = nfc_worker_get_error_message(app->worker);
+            if(error_message) {
+                show_error_popup(app, error_message);
+            } else {
+                show_error_popup(
+                    app, "Failed to detect card or run APDU commands\nCheck the log for details");
+            }
+
+            consumed = true;
+        } else if(custom_event == NfcWorkerEventCardLost) {
+            FURI_LOG_E("APDU_DEBUG", "卡片丢失事件");
+            // 不停止NFC Worker,而是显示等待卡片弹窗
+            show_waiting_popup(app);
+            consumed = true;
+        } else if(custom_event == NfcWorkerEventAborted) {
+            FURI_LOG_I("APDU_DEBUG", "操作被中止事件");
+            // 停止NFC Worker
+            nfc_worker_stop(app->worker);
+
+            // 检查是否已经显示了错误信息
+            const char* error_message = nfc_worker_get_error_message(app->worker);
+            if(!error_message) {
+                // 只有在没有错误信息的情况下才显示用户取消的消息
+                show_error_popup(app, "Operation cancelled by user");
+            }
+
+            consumed = true;
+        } else if(custom_event == NfcApduRunnerCustomEventPopupClosed) {
+            FURI_LOG_I("APDU_DEBUG", "弹出窗口关闭事件");
+            // 如果是错误弹窗关闭,返回到文件选择场景
+            scene_manager_previous_scene(app->scene_manager);
+            consumed = true;
+        } else if(custom_event == NfcApduRunnerCustomEventViewExit) {
+            FURI_LOG_I("APDU_DEBUG", "视图退出事件");
+            // 用户请求退出,设置运行标志为false
+            app->running = false;
+            nfc_worker_stop(app->worker);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        FURI_LOG_I("APDU_DEBUG", "返回事件");
+        // 用户按下返回键,设置运行标志为false并停止worker
+        app->running = false;
+        nfc_worker_stop(app->worker);
+        consumed = false; // 不消费事件,让场景管理器处理返回
+    }
+
+    return consumed;
+}
+
+// 运行场景退出回调
+void nfc_apdu_runner_scene_running_on_exit(void* context) {
+    NfcApduRunner* app = context;
+    FURI_LOG_I("APDU_DEBUG", "退出运行场景");
+
+    // 设置运行标志为false,停止任何正在进行的操作
+    app->running = false;
+
+    // 停止NFC Worker
+    nfc_worker_stop(app->worker);
+
+    // 释放脚本资源
+    if(app->script) {
+        nfc_apdu_script_free(app->script);
+        app->script = NULL;
+    }
+
+    popup_reset(app->popup);
+}

+ 114 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene_save_file.c

@@ -0,0 +1,114 @@
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+#include <toolbox/path.h>
+
+// 文本输入回调
+static void nfc_apdu_runner_scene_save_file_text_input_callback(void* context) {
+    NfcApduRunner* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, NfcApduRunnerCustomEventViewExit);
+}
+
+// 保存文件场景进入回调
+void nfc_apdu_runner_scene_save_file_on_enter(void* context) {
+    NfcApduRunner* app = context;
+
+    // 分配文本输入视图
+    TextInput* text_input = app->text_input;
+
+    // 提取原始文件名作为默认值
+    FuriString* filename_str = furi_string_alloc();
+    FuriString* file_path_str = furi_string_alloc_set(app->file_path);
+    path_extract_filename(file_path_str, filename_str, true);
+
+    // 移除扩展名
+    size_t ext_pos = furi_string_search_str(filename_str, FILE_EXTENSION);
+    if(ext_pos != FURI_STRING_FAILURE) {
+        furi_string_left(filename_str, ext_pos);
+    }
+
+    // 添加响应扩展名
+    furi_string_cat_str(filename_str, RESPONSE_EXTENSION);
+
+    // 设置文本输入
+    text_input_reset(text_input);
+    text_input_set_header_text(text_input, "Input save file name");
+    text_input_set_result_callback(
+        text_input,
+        nfc_apdu_runner_scene_save_file_text_input_callback,
+        app,
+        app->file_name_buf,
+        MAX_FILENAME_LEN,
+        true);
+
+    // 设置默认文件名
+    strncpy(app->file_name_buf, furi_string_get_cstr(filename_str), MAX_FILENAME_LEN);
+
+    // 释放临时字符串
+    furi_string_free(filename_str);
+    furi_string_free(file_path_str);
+
+    // 切换到文本输入视图
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewTextInput);
+}
+
+// 保存文件场景事件回调
+bool nfc_apdu_runner_scene_save_file_on_event(void* context, SceneManagerEvent event) {
+    NfcApduRunner* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == NfcApduRunnerCustomEventViewExit) {
+            // 构建完整的保存路径
+            FuriString* save_path = furi_string_alloc();
+            furi_string_printf(save_path, "%s/%s", APP_DIRECTORY_PATH, app->file_name_buf);
+
+            // 保存响应结果
+            bool success = nfc_apdu_save_responses(
+                app->storage,
+                furi_string_get_cstr(app->file_path),
+                app->responses,
+                app->response_count,
+                furi_string_get_cstr(save_path));
+
+            furi_string_free(save_path);
+
+            if(!success) {
+                // 显示错误消息
+                dialog_message_show_storage_error(app->dialogs, "Save failed");
+            } else {
+                // 显示成功消息
+                DialogMessage* message = dialog_message_alloc();
+                dialog_message_set_header(message, "Save success", 64, 0, AlignCenter, AlignTop);
+                dialog_message_set_text(
+                    message, "Responses saved", 64, 32, AlignCenter, AlignCenter);
+                dialog_message_set_buttons(message, "OK", NULL, NULL);
+                dialog_message_show(app->dialogs, message);
+                dialog_message_free(message);
+            }
+
+            // 返回到开始场景
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, NfcApduRunnerSceneStart);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        // 返回到结果场景
+        scene_manager_previous_scene(app->scene_manager);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+// 保存文件场景退出回调
+void nfc_apdu_runner_scene_save_file_on_exit(void* context) {
+    NfcApduRunner* app = context;
+    text_input_reset(app->text_input);
+
+    // 释放响应资源
+    if(app->responses != NULL) {
+        nfc_apdu_responses_free(app->responses, app->response_count);
+        app->responses = NULL;
+        app->response_count = 0;
+    }
+}

+ 78 - 0
nfc_apdu_runner/scenes/nfc_apdu_runner_scene_start.c

@@ -0,0 +1,78 @@
+/*
+ * @Author: SpenserCai
+ * @Date: 2025-03-07 16:30:29
+ * @version: 
+ * @LastEditors: SpenserCai
+ * @LastEditTime: 2025-03-11 09:50:54
+ * @Description: file content
+ */
+#include "../nfc_apdu_runner.h"
+#include "nfc_apdu_runner_scene.h"
+
+// 开始场景菜单项枚举
+enum {
+    NfcApduRunnerStartSubmenuIndexLoadFile,
+    NfcApduRunnerStartSubmenuIndexAbout,
+};
+
+// 开始场景菜单回调
+void nfc_apdu_runner_start_submenu_callback(void* context, uint32_t index) {
+    NfcApduRunner* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+// 开始场景进入回调
+void nfc_apdu_runner_scene_start_on_enter(void* context) {
+    NfcApduRunner* app = context;
+    Submenu* submenu = app->submenu;
+
+    submenu_reset(submenu);
+
+    submenu_add_item(
+        submenu,
+        "Load Script",
+        NfcApduRunnerStartSubmenuIndexLoadFile,
+        nfc_apdu_runner_start_submenu_callback,
+        app);
+
+    submenu_add_item(
+        submenu,
+        "About",
+        NfcApduRunnerStartSubmenuIndexAbout,
+        nfc_apdu_runner_start_submenu_callback,
+        app);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(app->scene_manager, NfcApduRunnerSceneStart));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, NfcApduRunnerViewSubmenu);
+}
+
+// 开始场景事件回调
+bool nfc_apdu_runner_scene_start_on_event(void* context, SceneManagerEvent event) {
+    NfcApduRunner* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(app->scene_manager, NfcApduRunnerSceneStart, event.event);
+
+        switch(event.event) {
+        case NfcApduRunnerStartSubmenuIndexLoadFile:
+            scene_manager_next_scene(app->scene_manager, NfcApduRunnerSceneFileSelect);
+            consumed = true;
+            break;
+        case NfcApduRunnerStartSubmenuIndexAbout:
+            scene_manager_next_scene(app->scene_manager, NfcApduRunnerSceneAbout);
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+// 开始场景退出回调
+void nfc_apdu_runner_scene_start_on_exit(void* context) {
+    NfcApduRunner* app = context;
+    submenu_reset(app->submenu);
+}