Browse Source

feat: 增加命令模式, excel加载优化
1. 优化修复了excel加载的相关问题
2. 修复了配置文件加载的问题
3. 增加测试命令的使用, 为加载文本功能做铺垫

kindring 1 week ago
parent
commit
b8024772d4
7 changed files with 1602 additions and 231 deletions
  1. 392 0
      bird_tool/Command.cs
  2. 38 11
      bird_tool/Config.cs
  3. 43 8
      bird_tool/ExcelLoad.cs
  4. 596 42
      bird_tool/ImageServer.cs
  5. 73 110
      bird_tool/TestEngine.cs
  6. 459 60
      bird_tool/bird_tool.cs
  7. 1 0
      bird_tool/bird_tool.csproj

+ 392 - 0
bird_tool/Command.cs

@@ -0,0 +1,392 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Bird_tool
+{
+    public class CommandParameter
+    {
+        public string Name { get; set; }
+        public Type Type { get; set; }
+        public string Description { get; set; }
+        public bool IsOptional { get; set; }
+        public object DefaultValue { get; set; }
+
+        public CommandParameter(string name, Type type, string description,
+                               bool isOptional = false, object defaultValue = null)
+        {
+            Name = name;
+            Type = type;
+            Description = description;
+            IsOptional = isOptional;
+            DefaultValue = defaultValue;
+        }
+    }
+
+    public class CommandInfo
+    {
+        public string Key { get; }
+        public string Name { get; }
+        public string Description { get; }
+        public string FailedMessage { get; set; }
+        public IReadOnlyList<CommandParameter> Parameters { get; }
+        public Func<object[], bool> Execute { get; }
+
+        public CommandInfo(string key, string name, 
+                            string description,
+                            string failedMessage,
+                          Func<object[], bool> execute,
+                          params CommandParameter[] parameters)
+        {
+            Key = key;
+            Name = name;
+            Description = description;
+            Execute = execute;
+            Parameters = parameters.ToList().AsReadOnly();
+            FailedMessage = failedMessage;
+        }
+
+        public int MinParameterCount => Parameters.Count(p => !p.IsOptional);
+        public int MaxParameterCount => Parameters.Count;
+
+        /// <summary>
+        /// 尝试转换字符串参数为所需类型
+        /// </summary>
+        public bool TryConvertParameters(string[] stringArgs, out object[] convertedArgs, out string errorMessage)
+        {
+            convertedArgs = new object[Parameters.Count];
+            errorMessage = null;
+
+            // 检查参数数量
+            if (stringArgs.Length < MinParameterCount)
+            {
+                errorMessage = $"需要至少 {MinParameterCount} 个参数,但只收到 {stringArgs.Length} 个";
+                return false;
+            }
+
+            if (stringArgs.Length > MaxParameterCount)
+            {
+                errorMessage = $"最多接受 {MaxParameterCount} 个参数,但收到 {stringArgs.Length} 个";
+                return false;
+            }
+
+            // 转换参数
+            for (int i = 0; i < Parameters.Count; i++)
+            {
+                var param = Parameters[i];
+
+                // 处理可选参数缺失
+                if (i >= stringArgs.Length)
+                {
+                    if (param.IsOptional)
+                    {
+                        convertedArgs[i] = param.DefaultValue;
+                        continue;
+                    }
+
+                    errorMessage = $"缺少必需参数: {param.Name}";
+                    return false;
+                }
+
+                // 尝试转换参数值
+                if (!TryConvertValue(stringArgs[i], param.Type, out object convertedValue))
+                {
+                    errorMessage = $"无法将 '{stringArgs[i]}' 转换为 {param.Type.Name} (参数: {param.Name})";
+                    return false;
+                }
+
+                convertedArgs[i] = convertedValue;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// 智能值转换(特别优化数值处理)
+        /// </summary>
+        private bool TryConvertValue(string input, Type targetType, out object result)
+        {
+            result = null;
+
+            try
+            {
+                // 处理空值
+                if (string.IsNullOrWhiteSpace(input))
+                {
+                    if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null)
+                        return false;
+
+                    result = null;
+                    return true;
+                }
+
+                // 字符串类型直接返回
+                if (targetType == typeof(string))
+                {
+                    result = input;
+                    return true;
+                }
+
+                // 数值类型处理(支持各种格式)
+                if (targetType == typeof(int) || targetType == typeof(int?))
+                {
+                    if (int.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out int intValue))
+                    {
+                        result = intValue;
+                        return true;
+                    }
+                }
+                else if (targetType == typeof(double) || targetType == typeof(double?))
+                {
+                    // 支持科学计数法、逗号分隔等格式
+                    if (double.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out double doubleValue))
+                    {
+                        result = doubleValue;
+                        return true;
+                    }
+                }
+                else if (targetType == typeof(decimal) || targetType == typeof(decimal?))
+                {
+                    if (decimal.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal decimalValue))
+                    {
+                        result = decimalValue;
+                        return true;
+                    }
+                }
+                else if (targetType == typeof(float) || targetType == typeof(float?))
+                {
+                    if (float.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out float floatValue))
+                    {
+                        result = floatValue;
+                        return true;
+                    }
+                }
+                else if (targetType == typeof(bool) || targetType == typeof(bool?))
+                {
+                    // 支持多种布尔值表示法
+                    if (bool.TryParse(input, out bool boolValue))
+                    {
+                        result = boolValue;
+                        return true;
+                    }
+
+                    switch (input.ToLowerInvariant())
+                    {
+                        case "1":
+                        case "true":
+                        case "yes":
+                        case "on":
+                        case "t":
+                        case "y":
+                            result = true;
+                            return true;
+                        case "0":
+                        case "false":
+                        case "no":
+                        case "off":
+                        case "f":
+                        case "n":
+                            result = false;
+                            return true;
+                    }
+                }
+                else if (targetType == typeof(DateTime) || targetType == typeof(DateTime?))
+                {
+                    // 支持多种日期格式
+                    if (DateTime.TryParse(input, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dateValue))
+                    {
+                        result = dateValue;
+                        return true;
+                    }
+                }
+                else if (targetType.IsEnum)
+                {
+                    try
+                    {
+                        result = Enum.Parse(targetType, input, true);
+                        return true;
+                    }
+                    catch
+                    {
+                        return false;
+                    }
+                }
+                else if (targetType == typeof(string[]))
+                {
+                    // 支持逗号分隔的数组
+                    result = input.Split(',').Select(s => s.Trim()).ToArray();
+                    return true;
+                }
+                else if (targetType == typeof(double[]))
+                {
+                    // 支持逗号分隔的数值数组
+                    var values = input.Split(',')
+                        .Select(s => double.TryParse(s.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out double val)
+                            ? val : (double?)null)
+                        .ToList();
+
+                    if (values.Any(v => v == null))
+                        return false;
+
+                    result = values.Select(v => v.Value).ToArray();
+                    return true;
+                }
+
+                // 通用类型转换
+                result = Convert.ChangeType(input, targetType, CultureInfo.InvariantCulture);
+                return true;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+    }
+
+
+    // 命令管理器
+    public class CommandManager
+    {
+        private readonly Dictionary<string, CommandInfo> _commands =
+            new Dictionary<string, CommandInfo>(StringComparer.OrdinalIgnoreCase);
+
+        // 注册命令
+        public void RegisterCommand(CommandInfo command)
+        {
+            if (string.IsNullOrWhiteSpace(command.Key))
+                throw new ArgumentException("命令键不能为空");
+
+            _commands[command.Key] = command;
+        }
+
+        /// <summary>
+        /// 执行命令(全部字符串参数输入)
+        /// </summary>
+        public bool ExecuteCommand(string commandKey, params string[] stringArgs)
+        {
+            return ExecuteCommand(commandKey, stringArgs.ToList());
+        }
+
+        /// <summary>
+        /// 执行命令(全部字符串参数输入)
+        /// </summary>
+        public bool ExecuteCommand(string commandKey, List<string> stringArgs)
+        {
+            if (!_commands.TryGetValue(commandKey, out var command))
+            {
+                throw new KeyNotFoundException($"未找到命令 '{commandKey}'");
+            }
+
+            // 转换参数
+            if (!command.TryConvertParameters(stringArgs.ToArray(), out var convertedArgs, out var error))
+            {
+                throw new ArgumentException($"参数转换错误: {error}");
+            }
+
+            try
+            {
+                return command.Execute(convertedArgs);
+            }
+            catch (Exception ex)
+            {
+                throw new InvalidOperationException($"执行错误: {ex.Message}", ex);
+            }
+        }
+
+        /// <summary>
+        /// 执行命令并返回详细结果
+        /// </summary>
+        public CommandResult ExecuteCommandDetailed(string commandKey, List<string> stringArgs)
+        {
+            var result = new CommandResult { CommandKey = commandKey };
+
+            if (!_commands.TryGetValue(commandKey, out var command))
+            {
+                result.Error = $"未找到命令 '{commandKey}'";
+                return result;
+            }
+
+            result.CommandInfo = command;
+
+            // 转换参数
+            if (!command.TryConvertParameters(stringArgs.ToArray(), out var convertedArgs, out var conversionError))
+            {
+                result.Error = $"参数转换错误: {conversionError}";
+                return result;
+            }
+
+            result.ConvertedArguments = convertedArgs;
+
+            try
+            {
+                result.Success = command.Execute(convertedArgs);
+                return result;
+            }
+            catch (Exception ex)
+            {
+                result.Error = $"执行错误: {ex.Message}";
+                return result;
+            }
+        }
+
+        // 获取所有命令信息
+        public IEnumerable<CommandInfo> GetAllCommands() => _commands.Values;
+
+        // 获取命令帮助信息
+        public string GetCommandHelp(string key)
+        {
+            if (!_commands.TryGetValue(key, out var command))
+                return $"未找到命令 '{key}'";
+
+            var help = new System.Text.StringBuilder();
+            help.AppendLine($"命令: {command.Key}");
+            help.AppendLine($"名称: {command.Name}");
+            help.AppendLine($"描述: {command.Description}");
+
+            if (command.Parameters.Count > 0)
+            {
+                help.AppendLine("参数:");
+                foreach (var param in command.Parameters)
+                {
+                    help.Append($"  {param.Name}: {param.Type.Name} - {param.Description}");
+                    if (param.IsOptional)
+                    {
+                        help.Append($" (可选,默认值: {param.DefaultValue ?? "null"})");
+                    }
+                    help.AppendLine();
+                }
+            }
+
+            return help.ToString();
+        }
+    }
+
+    /// <summary>
+    /// 命令执行结果
+    /// </summary>
+    public class CommandResult
+    {
+        public string CommandKey { get; set; }
+        public CommandInfo CommandInfo { get; set; }
+        public object[] ConvertedArguments { get; set; }
+        public bool Success { get; set; }
+        public string Error { get; set; }
+
+        public bool HasError => !string.IsNullOrEmpty(Error);
+
+        public override string ToString()
+        {
+            if (HasError)
+                return $"[{CommandKey}] 错误: {Error}";
+
+            var argsInfo = ConvertedArguments != null
+                ? string.Join(", ", ConvertedArguments.Select(a => a?.ToString() ?? "null"))
+                : "无参数";
+
+            return $"[{CommandKey}] 结果: {Success} | 参数: [{argsInfo}]";
+        }
+    }
+}

+ 38 - 11
bird_tool/Config.cs

@@ -37,7 +37,7 @@ namespace Bird_tool
         // 是否加载excel文件
         public bool enable_excel_load { get; set; } = true;
         
-        // 工具针对的设备类型 lw:乐微  fy:梵悦
+        // 工具针对的设备类型 lw:乐微  fy:梵悦 
         public string c_dev_key { get; set; } = "lw";
         
 
@@ -47,8 +47,6 @@ namespace Bird_tool
         // todo excel 中需要的列名 用于表示excel中列名对应的别名, 例如 key 对应authorKey. 用于在其它地方进行功能映射, 并且允许某些值为空, 配置默认值
         public List<ExcelColumnConfig> excel_columns { get; set; } = new List<ExcelColumnConfig>
         {
-            new ExcelColumnConfig { OriginalName = "uuid", Alias = "uuid", IsRequired = true },
-            new ExcelColumnConfig { OriginalName = "key", Alias = "authkey", IsRequired = true },
             // 添加其他列配置示例
             // new ExcelColumnConfig { OriginalName = "device_id", Alias = "deviceId", IsRequired = true, DefaultValue = "DEFAULT_001" }
         };
@@ -67,29 +65,58 @@ namespace Bird_tool
         public static AppConfig LoadConfig(string ConfigName = "config.json")
         {
             ConfigPath = Path.Combine(Environment.CurrentDirectory, ConfigName);
+
+            // 定义默认配置(不包含excel_columns)
+            var defaultConfig = new AppConfig
+            {
+                excel_columns = new List<ExcelColumnConfig>() // 初始化为空列表
+            };
+
             if (!File.Exists(ConfigPath))
             {
-                var defaultConfig = new AppConfig();
-                SaveConfig(defaultConfig);
-                return defaultConfig;
+                // 首次创建时使用完整默认配置
+                var fullDefaultConfig = new AppConfig();
+                SaveConfig(fullDefaultConfig);
+                return fullDefaultConfig;
             }
 
-            //string json = File.ReadAllText(ConfigPath);
-            
             try
             {
                 using (var streamReader = new StreamReader(ConfigPath))
                 using (var jsonReader = new JsonTextReader(streamReader))
                 {
                     var serializer = new JsonSerializer();
-                    return serializer.Deserialize<AppConfig>(jsonReader);
+                    var loadedConfig = serializer.Deserialize<AppConfig>(jsonReader);
+
+                    // 关键修复:仅在excel_columns为空时添加默认配置
+                    if (loadedConfig.excel_columns == null ||
+                        loadedConfig.excel_columns.Count == 0)
+                    {
+                        loadedConfig.excel_columns = new List<ExcelColumnConfig>
+                {
+                    new ExcelColumnConfig {
+                        OriginalName = "uuid",
+                        Alias = "uuid",
+                        IsRequired = true
+                    },
+                    new ExcelColumnConfig {
+                        OriginalName = "key",
+                        Alias = "authkey",
+                        IsRequired = true
+                    }
+                };
+
+                        // 可选:保存更新后的配置
+                        SaveConfig(loadedConfig);
+                    }
+
+                    return loadedConfig;
                 }
             }
             catch (JsonException ex)
             {
                 Console.WriteLine($"配置文件加载失败: {ex.Message}");
-                // 处理损坏的配置文件
-                File.Delete(ConfigPath);
+                // 返回带默认列配置的对象
                 return new AppConfig();
             }
             catch (IOException ex)

+ 43 - 8
bird_tool/ExcelLoad.cs

@@ -43,6 +43,11 @@ namespace Bird_tool
             _config = config;
             _excelPath = config.excel_path;
             _progressPath = progressPath;
+            
+            _primaryKeyColumn = config.excel_primary_key;
+        }
+        public void StartLoad()
+        {
             if (_config.enable_excel_load)
             {
                 LoadExcelData();
@@ -57,11 +62,11 @@ namespace Bird_tool
                 }
             }
         }
-
         private void LoadExcelData()
         {
             if (!File.Exists(_excelPath))
             {
+                OnLog?.Invoke($"Excel文件不存在: {_excelPath}", LogLevel.error);
                 ValidationErrors.Add($"Excel文件不存在: {_excelPath}");
                 return;
             }
@@ -73,6 +78,7 @@ namespace Bird_tool
                 {
                     if (package.Workbook.Worksheets.Count == 0)
                     {
+                        OnLog?.Invoke($"Excel文件中没有工作表", LogLevel.error);
                         ValidationErrors.Add("Excel文件中没有工作表");
                         return;
                     }
@@ -80,9 +86,10 @@ namespace Bird_tool
 
                     int rowCount = worksheet.Dimension?.Rows ?? 0;
                     int colCount = worksheet.Dimension?.Columns ?? 0;
-
+                    OnLog?.Invoke($"工作表行数: {rowCount}, 列数: {colCount}", LogLevel.info);
                     if (rowCount < 2 || colCount == 0)
                     {
+                        OnLog?.Invoke($"Excel文件没有数据", LogLevel.error);
                         ValidationErrors.Add("Excel文件没有数据");
                         return;
                     }
@@ -93,18 +100,36 @@ namespace Bird_tool
                     {
                         _headers[col - 1] = worksheet.Cells[1, col].Text?.Trim() ?? $"Column{col}";
                     }
-
+                    OnLog?.Invoke($"检测到Excel列: {string.Join(", ", _headers)}", LogLevel.info);
                     // 读取数据行
                     for (int row = 2; row <= rowCount; row++)
                     {
+                        bool isRowEmpty = true;
                         var rowData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+                        
                         for (int col = 1; col <= colCount; col++)
                         {
                             string header = _headers[col - 1];
                             string cellValue = worksheet.Cells[row, col].Text?.Trim() ?? "";
+                            if (row <= 10)
+                            {
+                                OnLog?.Invoke($"行{row}列{col}({header}): '{cellValue}'", LogLevel.debug);
+                            }
+                            if (!string.IsNullOrWhiteSpace(cellValue))
+                            {
+                                //OnLog?.Invoke($"isRowEmpty = false; 行{row}列{col}({header}): '{cellValue}'", LogLevel.debug);
+                                isRowEmpty = false;
+
+                            }
                             rowData[header] = cellValue;
                         }
-
+                        // 跳过空白行
+                        if (isRowEmpty)
+                        {
+                            //OnLog?.Invoke($"跳过空白行: {row}", LogLevel.debug);
+                            continue;
+                        }
+                        //OnLog?.Invoke($"添加行 {_primaryKeyColumn} {rowData.ContainsKey(_primaryKeyColumn)} {row} ==> ", LogLevel.debug);
                         // 只添加有主键的行
                         if (rowData.ContainsKey(_primaryKeyColumn) &&
                             !string.IsNullOrWhiteSpace(rowData[_primaryKeyColumn]))
@@ -116,6 +141,7 @@ namespace Bird_tool
             }
             catch (Exception ex)
             {
+                OnLog?.Invoke($"加载Excel数据失败: {ex.Message}", LogLevel.error);
                 ValidationErrors.Add($"加载Excel数据失败: {ex.Message}");
             }
         }
@@ -126,12 +152,14 @@ namespace Bird_tool
             // 1. 检查主键配置是否存在
             if (string.IsNullOrWhiteSpace(_config.excel_primary_key))
             {
+                OnLog?.Invoke($"配置错误: 未指定主键列名", LogLevel.error);
                 ValidationErrors.Add("配置错误: 未指定主键列名");
             }
 
             // 2. 检查列映射配置
             if (_config.excel_columns == null || _config.excel_columns.Count == 0)
             {
+                OnLog?.Invoke($"配置错误: 未配置列映射", LogLevel.error);
                 ValidationErrors.Add("配置错误: 未配置列映射");
             }
             else
@@ -149,6 +177,7 @@ namespace Bird_tool
 
                 if (!primaryKeyFound)
                 {
+                    OnLog?.Invoke($"配置错误: 主键列 '{_config.excel_primary_key}' 未在列映射中配置", LogLevel.error);
                     ValidationErrors.Add($"配置错误: 主键列 '{_config.excel_primary_key}' 未在列映射中配置");
                 }
             }
@@ -156,6 +185,7 @@ namespace Bird_tool
             // 3. 检查Excel列是否存在
             if (_rows.Count == 0)
             {
+                OnLog?.Invoke($"Excel中没有有效数据行", LogLevel.error);
                 ValidationErrors.Add("Excel中没有有效数据行");
                 IsValid = false;
                 return;
@@ -164,8 +194,10 @@ namespace Bird_tool
             // 检查必需的原始列是否存在
             foreach (var colConfig in _config.excel_columns)
             {
+                //OnLog?.Invoke($"检查必需的原始列是否存在: {colConfig.OriginalName}", LogLevel.debug);
                 if (colConfig.IsRequired && !_headers.Contains(colConfig.OriginalName, StringComparer.OrdinalIgnoreCase))
                 {
+                    OnLog?.Invoke($"Excel中缺少必需的列: {colConfig.OriginalName}", LogLevel.error);
                     ValidationErrors.Add($"Excel中缺少必需的列: {colConfig.OriginalName}");
                 }
             }
@@ -183,6 +215,7 @@ namespace Bird_tool
 
             if (!primaryKeyColumnExists)
             {
+                OnLog?.Invoke($"Excel中缺少主键列: {_config.excel_primary_key}", LogLevel.error);
                 ValidationErrors.Add($"Excel中缺少主键列: {_config.excel_primary_key}");
             }
 
@@ -195,6 +228,7 @@ namespace Bird_tool
                 {
                     if (keyValues.Contains(keyValue))
                     {
+                        OnLog?.Invoke($"主键值重复: {keyValue} (行 {i + 2})", LogLevel.error);
                         ValidationErrors.Add($"主键值重复: {keyValue} (行 {i + 2})");
                     }
                     else
@@ -216,6 +250,7 @@ namespace Bird_tool
                     var row = _rows[i];
                     if (!row.TryGetValue(colConfig.OriginalName, out string value) || string.IsNullOrWhiteSpace(value))
                     {
+                        OnLog?.Invoke($"必需列 '{colConfig.OriginalName}' 值为空 (行 {i + 2})", LogLevel.error);
                         ValidationErrors.Add($"必需列 '{colConfig.OriginalName}' 值为空 (行 {i + 2})");
                     }
                 }
@@ -475,10 +510,10 @@ namespace Bird_tool
 
         private void LogValidationErrors()
         {
-            foreach (var error in ValidationErrors)
-            {
-                OnLog?.Invoke(error, LogLevel.error);
-            }
+            //foreach (var error in ValidationErrors)
+            //{
+            //    OnLog?.Invoke(error, LogLevel.error);
+            //}
             OnLogShow?.Invoke("Excel数据验证失败,详情查看日志");
         }
 

+ 596 - 42
bird_tool/ImageServer.cs

@@ -14,9 +14,43 @@ using System.Drawing;
 using HttpMultipartParser;
 using System.Net.NetworkInformation;
 using System.Net.Sockets;
+using Newtonsoft.Json;
+using System.Text.RegularExpressions;
 
 namespace Bird_tool
 {
+    // 统一响应格式
+    public class ApiResponse
+    {
+        public int code { get; set; }
+        public string msg { get; set; }
+        public object data { get; set; }
+
+        public ApiResponse(int code, string msg, object data = null)
+        {
+            this.code = code;
+            this.msg = msg;
+            this.data = data;
+        }
+
+        public string ToJson()
+        {
+            return JsonConvert.SerializeObject(this);
+        }
+    }
+
+    // 图像处理结果
+    public class ImageProcessingResult
+    {
+        public string original_name { get; set; } // 原始文件名
+        public string file_name { get; set; }     // 保存后的文件名
+        public double score { get; set; } // 清晰度分数
+        public string device_id { get; set; }      // 设备ID
+        public string timestamp { get; set; }      // 时间戳
+        public string save_path { get; set; }      // 保存路径
+        public string image_url { get; set; }      // 图像访问URL
+        public string msg { get; set; }            // 错误消息
+    }
 
 
     class ImageSharpnessServer
@@ -31,15 +65,20 @@ namespace Bird_tool
         private readonly string _ip;
         private bool _isRunning;
 
+        private readonly string _imageStoragePath = "upload";
+        private readonly object _fileLock = new object();
+
         public ImageSharpnessServer(string ip, int port = 8080)
         {
             _ip = ip;
             _port = port;
-            
+
             _listener = new HttpListener();
-            string urlPrefix = $"http://*:{_port}/";
+            string urlPrefix = $"http://+:{_port}/";
             OnLog?.Invoke($"Server started on {urlPrefix}", LogLevel.info);
             _listener.Prefixes.Add(urlPrefix);
+
+            Directory.CreateDirectory(_imageStoragePath);
         }
 
         public bool Start()
@@ -52,6 +91,12 @@ namespace Bird_tool
                 ThreadPool.QueueUserWorkItem(Listen);
                 return true;
             }
+            // 端口占用
+            catch (HttpListenerException ex) when (ex.ErrorCode == 32)
+            {
+                OnLog?.Invoke("端口被占用, 请修改配置文件后重新启动", LogLevel.error);
+                return false;
+            }
             catch (HttpListenerException ex) when (ex.ErrorCode == 5) // 拒绝访问
             {
                 if (_ip == "+" || _ip == "*")
@@ -72,14 +117,12 @@ namespace Bird_tool
                         OnLog?.Invoke($"无法启动服务器: {innerEx.Message}", LogLevel.info);
                         OnLogShow?.Invoke("无法启动服务器, 请尝试使用管理员模式启动");
                         return false;
-                        throw;
                     }
                 }
                 else
                 {
                     OnLogShow?.Invoke("无法注册服务, 修改配置文件后再次启动");
                     return false;
-                    throw;
                 }
             }
         }
@@ -96,9 +139,15 @@ namespace Bird_tool
             {
                 try
                 {
+                    OnLog?.Invoke("listen to", LogLevel.info);
                     var context = _listener.GetContext();
                     ThreadPool.QueueUserWorkItem(ProcessRequest, context);
                 }
+                catch (HttpListenerException _ex) when (!_isRunning)
+                {
+                    // 正常停止时忽略
+                    OnLog?.Invoke("listen exit", LogLevel.info);
+                }
                 catch (Exception ex)
                 {
                     if (_isRunning) OnLog?.Invoke($"监听端口失败 Error: {ex.Message}", LogLevel.info);
@@ -180,40 +229,180 @@ namespace Bird_tool
             return ipAddresses;
         }
 
+        private string SanitizeFilename(string input)
+        {
+            if (string.IsNullOrEmpty(input))
+                return "unknown";
+
+            // 定义非法字符集合(包括Windows和Unix文件系统非法字符)
+            char[] invalidChars = Path.GetInvalidFileNameChars();
+
+            // 替换非法字符为下划线
+            string sanitized = new string(input.Select(c =>
+                invalidChars.Contains(c) ? '-' : c).ToArray());
+
+            // 移除连续的下划线
+            sanitized = Regex.Replace(sanitized, @"_{2,}", "_");
+
+            // 截断过长的文件名(保留前100个字符)
+            return sanitized.Length > 100 ? sanitized.Substring(0, 100) : sanitized;
+        }
+
         private void ProcessRequest(object state)
         {
             var context = (HttpListenerContext)state;
             try
             {
-                switch (context.Request.HttpMethod)
+                OnLog?.Invoke($"{context.Request.HttpMethod} query {context.Request.Url.AbsolutePath}", LogLevel.info);
+                var path = context.Request.Url.AbsolutePath;
+                var method = context.Request.HttpMethod;
+                ApiResponse response = null;
+
+                Regex.Match(path, @"^/upload/([^/]+)/([^/]+)$");
+                var match = Regex.Match(path, @"^/upload/([^/]+)/([^/]+)$");
+                if (method == "POST" && (path == "/upload" || match.Success))
                 {
-                    case "POST":
-                        HandleImageUpload(context);
-                        break;
-                    case "GET":
+                    HandleImageUpload(context);
+                }
+                else if (method == "GET")
+                {
+                    if (path == "/api/latest")
+                    {
+                        HandleGetLatestImage(context);
+                    }
+                    else if (path == "/api/images")
+                    {
+                        HandleGetImageList(context);
+                    }
+                    else if (path == "/api/devices")
+                    {
+                        HandleGetDevices(context);
+                    }
+                    else if (path == "/api/image")
+                    {
+                        HandleGetImage(context); // 新增的图像下载接口
+                    }
+                    else
+                    {
                         HandleGetRequest(context);
-                        break;
-                    default:
-                        SendResponse(context, 405, "Method Not Allowed");
-                        break;
+                    }
+                }
+                else
+                {
+                    // 使用统一的JSON错误响应
+                    response = new ApiResponse(404, "Not Found");
+                    SendResponse(context, 404, response.ToJson(), "application/json");
                 }
             }
             catch (Exception ex)
             {
                 OnLog?.Invoke($"Processing error: {ex}", LogLevel.info);
-                SendResponse(context, 500, $"Internal Server Error: {ex.Message}");
+                var response = new ApiResponse(500, $"Internal Server Error: {ex.Message}");
+                SendResponse(context, 500, response.ToJson(), "application/json");
+            }
+        }
+
+        private void HandleGetImage(HttpListenerContext context)
+        {
+            try
+            {
+                var query = context.Request.QueryString;
+                string fileName = query["file_name"];
+                ApiResponse response = null;
+
+                if (string.IsNullOrEmpty(fileName))
+                {
+                    response = new ApiResponse(400, $"Missing file_name parameter");
+                    SendResponse(context, 400, response.ToJson(), "application/json");
+                    return;
+                }
+
+                // 安全处理文件名,防止路径遍历攻击
+                string safeFileName = Path.GetFileName(fileName);
+                if (safeFileName != fileName)
+                {
+                    response = new ApiResponse(400, $"Invalid file name");
+                    SendResponse(context, 400, response.ToJson(), "application/json");
+                    return;
+                }
+
+                string filePath = Path.Combine(_imageStoragePath, safeFileName);
+
+                if (!File.Exists(filePath))
+                {
+                    response = new ApiResponse(404, $"Image not found");
+                    SendResponse(context, 404, response.ToJson(), "application/json");
+                    return;
+                }
+
+                // 读取图像数据
+                byte[] imageData = File.ReadAllBytes(filePath);
+
+                // 设置正确的Content-Type
+                context.Response.ContentType = "image/jpeg";
+                context.Response.ContentLength64 = imageData.Length;
+                context.Response.OutputStream.Write(imageData, 0, imageData.Length);
+                context.Response.Close();
+            }
+            catch (Exception ex)
+            {
+                OnLog?.Invoke($"获取图片出错: {ex}", LogLevel.error);
+                var response = new ApiResponse(500, $"Internal server error: {ex.Message}");
+                SendResponse(context, 500, response.ToJson(), "application/json");
             }
         }
 
         private void HandleImageUpload(HttpListenerContext context)
         {
+            var path = context.Request.Url.AbsolutePath;
+            var query = context.Request.QueryString;
+            var match = Regex.Match(path, @"^/upload/([^/]+)/([^/]+)$");
+            string rawTimestamp = null, 
+                   rawDeviceId = null;
+            if (match.Success)
+            {
+                // 尝试从query中获取
+                rawDeviceId = match.Groups[1].Value;
+                rawTimestamp = match.Groups[2].Value;
+                OnLog?.Invoke($"get path parameter ", LogLevel.info);
+            }
+            else
+            {
+                rawTimestamp = query["timestamp"];
+                rawDeviceId = query["device_id"];
+                OnLog?.Invoke($"get query param ", LogLevel.info);
+            }
+            var results = new List<ImageProcessingResult>();
+            double maxScore = -1; // 最高分初始值
+           
             try
             {
+                // 获取查询参数
+                
+                ApiResponse response = null;
+
+                // 处理空值情况
+                if (string.IsNullOrEmpty(rawTimestamp) || string.IsNullOrEmpty(rawDeviceId))
+                {
+                    response = new ApiResponse(400, "Missing required query parameters: timestamp and device_id");
+                    SendResponse(context, 400, response.ToJson(), "application/json");
+                    return;
+                }
+
+                // 安全处理文件名
+                string timestamp = SanitizeFilename(rawTimestamp);
+                string deviceId = SanitizeFilename(rawDeviceId);
+
+                // 为设备创建专用目录
+                string deviceDirectory = Path.Combine(_imageStoragePath, deviceId);
+                Directory.CreateDirectory(deviceDirectory);
+
                 // 获取内容类型
                 string contentType = context.Request.ContentType;
                 if (string.IsNullOrEmpty(contentType) || !contentType.Contains("multipart/form-data"))
                 {
-                    SendResponse(context, 400, "Content-Type must be multipart/form-data");
+                    response = new ApiResponse(400, "Content-Type must be multipart/form-data");
+                    SendResponse(context, 400, response.ToJson(), "application/json");
                     return;
                 }
 
@@ -221,55 +410,420 @@ namespace Bird_tool
                 string boundary = GetBoundaryFromContentType(contentType);
                 if (string.IsNullOrEmpty(boundary))
                 {
-                    SendResponse(context, 400, "Invalid boundary in Content-Type");
+                    response = new ApiResponse(400, "Invalid boundary in Content-Type");
+                    SendResponse(context, 400, response.ToJson(), "application/json");
                     return;
                 }
 
-                // 读取整个请求体
-                byte[] requestBody;
-                using (var ms = new MemoryStream())
+                // 使用MultipartFormDataParser解析多个文件
+                var parser = MultipartFormDataParser.Parse(context.Request.InputStream, boundary, Encoding.UTF8);
+
+                // 处理所有上传的文件
+                foreach (var file in parser.Files)
                 {
-                    context.Request.InputStream.CopyTo(ms);
-                    requestBody = ms.ToArray();
+                    ImageProcessingResult result = new ImageProcessingResult
+                    {
+                        original_name = file.FileName,
+                        device_id = deviceId,
+                        timestamp = timestamp,
+                        msg = ""
+                    };
+
+                    try
+                    {
+                        // 读取文件数据
+                        byte[] imageData;
+                        using (var memoryStream = new MemoryStream())
+                        {
+                            file.Data.CopyTo(memoryStream);
+                            imageData = memoryStream.ToArray();
+                        }
+
+                        // 验证图片数据
+                        if (imageData.Length == 0)
+                        {
+                            result.msg = "Empty image data";
+                            results.Add(result);
+                            continue;
+                        }
+
+                        // 计算清晰度
+                        var (score, isValid) = CalculateSharpness(imageData);
+                        if (!isValid)
+                        {
+                            result.msg = "Unsupported image format or corrupted data";
+                            results.Add(result);
+                            continue;
+                        }
+
+                        OnLog?.Invoke($"Image score: {score} for device {deviceId} at {timestamp}", LogLevel.info);
+
+                        // 更新最高分
+                        if (score > maxScore)
+                        {
+                            maxScore = score;
+                        }
+
+                        // 保存图片(文件名格式:设备ID_时间戳_唯一标识_分值.jpg)
+                        string uniqueId = Guid.NewGuid().ToString("N").Substring(0, 8);
+                        string fileName = $"{deviceId}_{timestamp}_{uniqueId}_{score:F2}.jpg";
+                        string filePath = Path.Combine(deviceDirectory, fileName);
+
+                        lock (_fileLock)
+                        {
+                            File.WriteAllBytes(filePath, imageData);
+                        }
+
+                        result.file_name = fileName;
+                        result.score = score;
+                        result.save_path = filePath;
+                        result.image_url = $"/api/image?file_name={Uri.EscapeDataString(fileName)}";
+                        results.Add(result);
+                    }
+                    catch (Exception ex)
+                    {
+                        result.msg = $"Error processing image: {ex.Message}";
+                        results.Add(result);
+                        OnLog?.Invoke($"Error processing image {file.FileName}: {ex}", LogLevel.error);
+                    }
                 }
 
-                // 解析multipart数据
-                byte[] imageData = ExtractImageFromMultipart(requestBody, boundary);
-                if (imageData == null)
+                // 构建响应数据
+                var responseData = new
                 {
-                    SendResponse(context, 400, "Failed to extract image from multipart data");
+                    score = maxScore, // 最高分
+                    device_id = deviceId,
+                    timestamp = timestamp,
+                    count = results.Count, // 图片数量
+                    images = results.Select(r => new
+                    {
+                        image_url = r.image_url,
+                        score = r.score,
+                        msg = r.msg
+                    }).ToList(),
+                    msg = "评估结果测试" // 自定义评估结果消息
+                };
+
+                // 检查是否有成功处理的图像
+                if (results.All(r => r.score < 0))
+                {
+                    response = new ApiResponse(400, "All images failed to process", responseData);
+                    SendResponse(context, 400, response.ToJson(), "application/json");
                     return;
                 }
 
-                // 验证图片数据
-                if (imageData.Length == 0)
+                // 返回结果 - 使用统一的JSON格式
+                response = new ApiResponse(200, "Images processed successfully", responseData);
+                SendResponse(context, 200, response.ToJson(), "application/json");
+            }
+            catch (Exception ex)
+            {
+                // 错误情况下的响应数据
+                var responseData = new
                 {
-                    SendResponse(context, 400, "Empty image data");
+                    score = -1,
+                    device_id = rawDeviceId,
+                    timestamp = rawTimestamp,
+                    count = results.Count,
+                    images = results.Select(r => new
+                    {
+                        image_url = r.image_url,
+                        score = r.score,
+                        msg = r.msg
+                    }).ToList(),
+                    msg = $"处理过程中发生错误: {ex.Message}"
+                };
+
+                OnLog?.Invoke($"Upload processing error: {ex}", LogLevel.error);
+                var response = new ApiResponse(500, $"Internal server error: {ex.Message}", responseData);
+                SendResponse(context, 500, response.ToJson(), "application/json");
+            }
+        }
+
+
+        private void HandleGetLatestImage(HttpListenerContext context)
+        {
+            try
+            {
+                var query = context.Request.QueryString;
+                string rawDeviceId = query["device_id"];
+                string rawTimestamp = query["timestamp"];
+                ApiResponse response = null;
+
+                string deviceId = SanitizeFilename(rawDeviceId);
+                if (string.IsNullOrEmpty(deviceId))
+                {
+                    response = new ApiResponse(400, "Missing required parameter: device_id");
+                    SendResponse(context, 400, response.ToJson(), "application/json");
                     return;
                 }
 
-                Directory.CreateDirectory("upload");
-                // 保存原始数据用于调试
-                File.WriteAllBytes($"upload/upload_{DateTime.Now:yyyyMMddHHmmss}.jpg", imageData);
+                // 获取设备目录
+                string deviceDirectory = Path.Combine(_imageStoragePath, deviceId);
 
-                // 计算清晰度
-                var (score, isValid) = CalculateSharpness(imageData);
-                if (!isValid)
+                if (!Directory.Exists(deviceDirectory))
                 {
-                    SendResponse(context, 400, "Unsupported image format or corrupted data");
+                    response = new ApiResponse(404, $"No images found for device: {deviceId}");
+                    SendResponse(context, 404, response.ToJson(), "application/json");
                     return;
                 }
-                OnLog?.Invoke($"img score:{score}", LogLevel.info);
-                // 返回结果
-                string response = $"{{\"sharpness_score\": {score}}}";
-                SendResponse(context, 200, response, "application/json");
+
+                // 获取设备的所有图片
+                var allImages = Directory.GetFiles(deviceDirectory, "*.jpg")
+                    .Select(f => new
+                    {
+                        Path = f,
+                        FileName = Path.GetFileName(f),
+                        Created = File.GetCreationTime(f)
+                    })
+                    .ToList();
+
+                // 如果没有找到图片
+                if (!allImages.Any())
+                {
+                    response = new ApiResponse(404, $"No images found for device: {deviceId}");
+                    SendResponse(context, 404, response.ToJson(), "application/json");
+                    return;
+                }
+
+                string timestamp = !string.IsNullOrEmpty(rawTimestamp) ?
+                    SanitizeFilename(rawTimestamp) : null;
+
+                // 根据查询条件过滤
+                if (!string.IsNullOrEmpty(timestamp))
+                {
+                    allImages = allImages
+                        .Where(img => img.FileName.Contains($"_{timestamp}_"))
+                        .ToList();
+
+                    if (!allImages.Any())
+                    {
+                        response = new ApiResponse(404, $"No image found for device {deviceId} with timestamp {timestamp}");
+                        SendResponse(context, 404, response.ToJson(), "application/json");
+                        return;
+                    }
+                }
+
+                // 获取最新图片
+                var latestImage = allImages
+                    .OrderByDescending(img => img.Created)
+                    .First();
+
+                // 从文件名解析信息
+                var info = ParseImageInfo(latestImage.FileName);
+
+                // 构建响应 - 使用统一的JSON格式
+                var data = new
+                {
+                    device_id = deviceId,
+                    timestamp = info.Timestamp,
+                    sharpness_score = info.Score,
+                    image_url = $"/{_imageStoragePath}/{deviceId}/{latestImage.FileName}",
+                    created_time = latestImage.Created.ToString("yyyy-MM-dd HH:mm:ss"),
+                    file_name = latestImage.FileName
+                };
+
+                response = new ApiResponse(200, "Latest image retrieved successfully", data);
+                SendResponse(context, 200, response.ToJson(), "application/json");
             }
             catch (Exception ex)
             {
-                OnLog?.Invoke($"Upload processing error: {ex}", LogLevel.error);
-                SendResponse(context, 500, $"Internal server error: {ex.Message}");
+                OnLog?.Invoke($"Get latest image error: {ex}", LogLevel.error);
+                var response = new ApiResponse(500, $"Internal server error: {ex.Message}");
+                SendResponse(context, 500, response.ToJson(), "application/json");
             }
         }
+
+        private void HandleGetImageList(HttpListenerContext context)
+        {
+            try
+            {
+                var query = context.Request.QueryString;
+                string rawDeviceId = query["device_id"];
+                int page = int.TryParse(query["page"], out int p) ? p : 1;
+                int perPage = int.TryParse(query["per_page"], out int pp) ? pp : 10;
+                ApiResponse response = null;
+
+                if (string.IsNullOrEmpty(rawDeviceId))
+                {
+                    response = new ApiResponse(400, "Missing required parameter: device_id");
+                    SendResponse(context, 400, response.ToJson(), "application/json");
+                    return;
+                }
+
+                string deviceId = SanitizeFilename(rawDeviceId);
+                string deviceDirectory = Path.Combine(_imageStoragePath, deviceId);
+
+                if (!Directory.Exists(deviceDirectory))
+                {
+                    response = new ApiResponse(404, $"No images found for device: {deviceId}");
+                    SendResponse(context, 404, response.ToJson(), "application/json");
+                    return;
+                }
+
+                // 获取设备的所有图片
+                var allImages = Directory.GetFiles(deviceDirectory, "*.jpg")
+                    .Select(f => new
+                    {
+                        Path = f,
+                        FileName = Path.GetFileName(f),
+                        Created = File.GetCreationTime(f)
+                    })
+                    .OrderByDescending(img => img.Created)
+                    .ToList();
+
+                // 分页处理
+                int totalCount = allImages.Count;
+                int totalPages = (int)Math.Ceiling((double)totalCount / perPage);
+                page = Math.Max(1, Math.Min(page, totalPages));
+
+                var pagedImages = allImages
+                    .Skip((page - 1) * perPage)
+                    .Take(perPage)
+                    .Select(img =>
+                    {
+                        var info = ParseImageInfo(img.FileName);
+                        return new
+                        {
+                            device_id = deviceId,
+                            timestamp = info.Timestamp,
+                            score = info.Score,
+                            image_url = $"/{_imageStoragePath}/{deviceId}/{img.FileName}",
+                            created_time = img.Created.ToString("yyyy-MM-dd HH:mm:ss"),
+                            file_name = img.FileName
+                        };
+                    })
+                    .ToList();
+
+                // 构建响应 - 使用统一的JSON格式
+                var data = new
+                {
+                    total_count = totalCount,
+                    total_pages = totalPages,
+                    current_page = page,
+                    per_page = perPage,
+                    images = pagedImages
+                };
+
+                response = new ApiResponse(200, "Image list retrieved successfully", data);
+                SendResponse(context, 200, response.ToJson(), "application/json");
+            }
+            catch (Exception ex)
+            {
+                OnLog?.Invoke($"Get image list error: {ex}", LogLevel.error);
+                var response = new ApiResponse(500, $"Internal server error: {ex.Message}");
+                SendResponse(context, 500, response.ToJson(), "application/json");
+            }
+        }
+
+        private void HandleGetDevices(HttpListenerContext context)
+        {
+            try
+            {
+                var query = context.Request.QueryString;
+                int page = int.TryParse(query["page"], out int p) ? p : 1;
+                int perPage = int.TryParse(query["per_page"], out int pp) ? pp : 10;
+                ApiResponse response = null;
+
+                // 获取所有设备目录
+                var deviceDirectories = Directory.GetDirectories(_imageStoragePath)
+                    .Select(Path.GetFileName)
+                    .Where(name => !string.IsNullOrEmpty(name))
+                    .ToList();
+
+                // 分页处理
+                int totalCount = deviceDirectories.Count;
+                int totalPages = (int)Math.Ceiling((double)totalCount / perPage);
+                page = Math.Max(1, Math.Min(page, totalPages));
+
+                var pagedDevices = deviceDirectories
+                    .Skip((page - 1) * perPage)
+                    .Take(perPage)
+                    .ToList();
+
+                // 获取每个设备的图像数量
+                var deviceStats = pagedDevices.Select(deviceId =>
+                {
+                    string deviceDir = Path.Combine(_imageStoragePath, deviceId);
+                    int imageCount = Directory.Exists(deviceDir) ?
+                        Directory.GetFiles(deviceDir, "*.jpg").Length : 0;
+
+                    // 获取最新图像信息(如果有)
+                    var latestImage = Directory.Exists(deviceDir) ?
+                        Directory.GetFiles(deviceDir, "*.jpg")
+                            .Select(f => new FileInfo(f))
+                            .OrderByDescending(f => f.CreationTime)
+                            .FirstOrDefault() : null;
+
+                    return new
+                    {
+                        device_id = deviceId,
+                        image_count = imageCount,
+                        last_upload = latestImage?.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") ?? "N/A",
+                        last_image = latestImage?.Name ?? "N/A"
+                    };
+                }).ToList();
+
+                // 构建响应 - 使用统一的JSON格式
+                var data = new
+                {
+                    total_count = totalCount,
+                    total_pages = totalPages,
+                    current_page = page,
+                    per_page = perPage,
+                    devices = deviceStats
+                };
+
+                response = new ApiResponse(200, "Device list retrieved successfully", data);
+                SendResponse(context, 200, response.ToJson(), "application/json");
+            }
+            catch (Exception ex)
+            {
+                OnLog?.Invoke($"Get devices error: {ex}", LogLevel.error);
+                var response = new ApiResponse(500, $"Internal server error: {ex.Message}");
+                SendResponse(context, 500, response.ToJson(), "application/json");
+            }
+        }
+
+        private (string DeviceId, string Timestamp, double Score) ParseImageInfo(string fileName)
+        {
+            try
+            {
+                // 移除文件扩展名
+                string nameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
+
+                // 查找最后一个下划线的位置(分值前)
+                int lastUnderscore = nameWithoutExt.LastIndexOf('_');
+                if (lastUnderscore == -1)
+                    throw new FormatException($"Invalid file name format: {fileName}");
+
+                // 提取分值部分
+                string scorePart = nameWithoutExt.Substring(lastUnderscore + 1);
+                if (!double.TryParse(scorePart, out double score))
+                {
+                    score = -1; // 无法解析时使用-1
+                }
+
+                // 剩余部分是设备ID和时间戳的组合
+                string prefix = nameWithoutExt.Substring(0, lastUnderscore);
+
+                // 查找第一个下划线的位置(设备ID和时间戳之间)
+                int firstUnderscore = prefix.IndexOf('_');
+                if (firstUnderscore == -1)
+                    throw new FormatException($"Invalid file name format: {fileName}");
+
+                string deviceId = prefix.Substring(0, firstUnderscore);
+                string timestamp = prefix.Substring(firstUnderscore + 1);
+
+                return (deviceId, timestamp, score);
+            }
+            catch (Exception ex)
+            {
+                OnLog?.Invoke($"解析文件名失败: {fileName}, 错误: {ex.Message}", LogLevel.error);
+                return ("unknown", "unknown", -1);
+            }
+        }
+
         private string GetBoundaryFromContentType(string contentType)
         {
             // 查找boundary参数
@@ -477,4 +1031,4 @@ namespace Bird_tool
             context.Response.Close();
         }
     }
-}
+}

+ 73 - 110
bird_tool/TestEngine.cs

@@ -96,12 +96,37 @@ namespace Bird_tool
         Failed,
         Success
     }
+    
     public enum ActionMode
     {
         SendCmd,
         SetVal,
         WaitTime,
+        // 变量验证
+        Validate,
+        // http请求
+        HTTPView,
+        // 显示图片
+        ViewImage,
     }
+
+    /**
+     * 验证器类,用于定义验证逻辑和错误提示。
+     * 验证器类包含验证函数和错误提示。
+     * 
+     */
+    public class Validate
+    {
+        // 验证器ID (用于匹配验证函数
+        private string fn = "";
+        // 验证参数列表, 支持变量, 与固定字符.
+        private List<string> args = new List<string>();
+        // 错误提示.
+        public string ErrorMsg { get; set; }
+    }
+
+
+
     public class TestGroup
     {
         // 测试组, 用于多组测试项合并测试
@@ -161,6 +186,17 @@ namespace Bird_tool
         public string Command { get; set; }
         public Func<TestContext, string> CommandGenerator { get; set; }
 
+        // 匹配成功后的验证器
+        public string MatchedValidator { get; set; }
+        // 用于填入验证器的参数, 允许为空
+        public List<string> ValidatorParams { get; set; } = new List<string>();
+
+        // 验证失败后的处理器 http 请求
+        public string FailCommand { get; set; }
+        // 请求参数
+        public List<string> FailCommandParams { get; set; } = new List<string>();
+
+
         public string SuccessPattern { get; set; }
         public string SuccessText { get; set; }
         public string FailurePattern { get; set; }
@@ -181,14 +217,18 @@ namespace Bird_tool
         // 是否创建对应的提示列表
         public bool ShowStep { get; set; } = true;
 
-        // 跳转任务失败次数只有在包含 RetryFromKey 时生效
+        // 跳转任务失败次数只有在包含 JumpKey 时生效
         public string Key { get; set; }
         // 失败后要跳转到的条目
-        public string RetryFromKey { get; set; }
-        // 失败后是否重试当前步骤
+        public string JumpKey { get; set; }
+        // 失败后是否重试当前步骤, 会先重试, 重试失败后在执行跳转
         public bool StepRetry { get; set; } = true;
         public int MaxJump { get; set; } = 3;
         public int JumpCount { get; set; } = 0;
+        // 是否为跳转执行命令. true: 顺序执行则直接跳过, 由跳转执行则执行 false: 
+        public bool JumpExecute { get; set; } = false;
+        // 跳转执行对应ID, 用于跳转后执行多段任务, 对应key组的命令执行完成后就视作任务结束
+        public string JumpId { get; set; }
 
         // 失败后是否继续测试
         public bool FailContinue { get; set; } = false;
@@ -222,50 +262,6 @@ namespace Bird_tool
         public Action<TestContext> OnSuccess { get; set; }
         public Action<TestContext> OnFailure { get; set; }
 
-        // 自动生成描述(如果未设置)
-        public string GetDescription()
-        {
-            if (!string.IsNullOrEmpty(Description))
-                return Description;
-
-            var sb = new StringBuilder();
-
-            if (!string.IsNullOrEmpty(Command))
-            {
-                sb.AppendLine($"执行命令: {Command}");
-            }
-
-            if (!string.IsNullOrEmpty(SuccessPattern))
-            {
-                sb.AppendLine($"成功匹配: {SuccessPattern}");
-            }
-            if (!string.IsNullOrEmpty(SuccessText))
-            {
-                sb.AppendLine($"成功匹配: {SuccessText}");
-            }
-
-            if (!string.IsNullOrEmpty(FailurePattern))
-            {
-                sb.AppendLine($"失败匹配: {FailurePattern}");
-            }
-
-            if (!string.IsNullOrEmpty(FailureText))
-            {
-                sb.AppendLine($"失败匹配: {FailureText}");
-            }
-
-            if (RequiresUserPrompt)
-            {
-                sb.AppendLine($"需要用户确认: {PromptQuestion}");
-            }
-
-            if (Validator != null)
-            {
-                sb.AppendLine("包含自定义验证逻辑");
-            }
-
-            return sb.ToString();
-        }
 
     }
 
@@ -330,6 +326,8 @@ namespace Bird_tool
         public event TestSuccessHandler OnSuccess;
         public event TestEndHandler OnTestEnd;
 
+        private CommandManager _commandManager;
+
         private readonly SerialManager _serialManager;
         private TestContext _context;
         private System.Threading.Timer _timeoutTimer;
@@ -351,15 +349,17 @@ namespace Bird_tool
         private string _csvReportPath = "TestReport/report.csv";
         private static readonly object _csvLock = new object();
 
-        public TestExecutor(SerialManager serialManager)
+        public TestExecutor(SerialManager serialManager, CommandManager commandManager)
         {
             bird_tool.Log("初始化测试引擎 TestExecutor init");
             _serialManager = serialManager;
+            _commandManager = commandManager;
             _taskQueue = Channel.CreateBounded<Func<CancellationToken, Task>>(1000);
 
             StartQueueProcessor();
         }
 
+
         private void StartQueueProcessor()
         {
             // 确保之前的队列处理器已停止
@@ -866,8 +866,10 @@ namespace Bird_tool
                     for (int i = 0; i < step.VariableValues.Count; i++)
                     {
                         var varName = step.VariableNames[i];
-                        string tmp_str = $"设置变量: {varName} = {step.VariableValues[i]}";
-                        _context.Variables[varName] = step.VariableValues[i];
+                        var varVal = ReplaceTemplateVariables(step.VariableValues[i], _context.Variables);
+                        // 替换变量类型
+                        string tmp_str = $"设置变量: {varName} = {varVal}";
+                        _context.Variables[varName] = varVal;
                         if (step.PrivateCammand)
                         {
                             OnLog?.Invoke("设置变量", LogLevel.debug);
@@ -989,9 +991,18 @@ namespace Bird_tool
 
                     // 自定义验证逻辑
                     bool shouldRetry = false;
-                    if (step.Validator != null)
+                    if (!string.IsNullOrEmpty(step.MatchedValidator))
                     {
-                        shouldRetry = step.Validator(data, _context);
+                        // 执行验证逻辑, 
+                        List<string> validatorParams = new List<string>();
+                        // 替换参数中的变量, 随后添加至参数列表
+                        foreach (var param in step.ValidatorParams)
+                        {
+                            var nextParam = ReplaceTemplateVariables(param, _context.Variables);
+                            validatorParams.Add(nextParam);
+                        }
+
+                        shouldRetry = !_commandManager.ExecuteCommand(step.MatchedValidator, validatorParams);
                     }
 
                     if (shouldRetry)
@@ -1035,53 +1046,6 @@ namespace Bird_tool
         }
 
 
-        // 处理响应
-        private async void ProcessResponse(string data, TestStepConfig step, CancellationToken ct)
-        {
-            if ((!string.IsNullOrEmpty(step.SuccessPattern) && Regex.IsMatch(data, step.SuccessPattern)) ||
-                (!string.IsNullOrEmpty(step.SuccessText) && data.Contains(step.SuccessText)))
-            {
-                if (!string.IsNullOrEmpty(step.ExtractPattern) && !ExtractVariables(data, step))
-                {
-                    if (!ExtractVariables(data, step))
-                    {
-                        OnLog?.Invoke($"匹配成功, 但是无法提取变量 {data} ", LogLevel.error);
-                        return;
-                    }
-                }
-
-                // 自定义验证逻辑
-                bool shouldRetry = false;
-                if (step.Validator != null)
-                {
-                    shouldRetry = step.Validator(data, _context);
-                }
-
-                if (shouldRetry)
-                {
-                    OnLog?.Invoke("验证失败,需要重试", LogLevel.info);
-                    HandleStepFailure("自定义验证失败", false);
-                }
-                else
-                {
-                    // 判断是否需要进行提问. 如果需要提问则先进行提问
-                    if (step.RequiresUserPrompt)
-                    {
-                        await PromptQuestionAsync(step, ct);
-                    }
-                    else
-                    {
-                        HandleSetpSuccess("匹配到成功关键词");
-                    }
-                }
-            }
-            else if ((!string.IsNullOrEmpty(step.FailurePattern) && Regex.IsMatch(data, step.FailurePattern)) ||
-                     (!string.IsNullOrEmpty(step.FailureText) && data.Contains(step.FailureText)))
-            {
-                HandleStepFailure("匹配到失败关键词", false);
-            }
-        }
-
         public void HandleResponse(string data)
         {
             OnLog?.Invoke($"收到响应 {data} ", LogLevel.debug);
@@ -1095,10 +1059,10 @@ namespace Bird_tool
             var match = Regex.Match(data, step.ExtractPattern);
             if (match.Success)
             {
-                for (int i = 1; i < match.Groups.Count; i++)
+                for (int i = 0; i < match.Groups.Count; i++)
                 {
-                    var varName = step.VariableNames.Count >= i ?
-                        step.VariableNames[i - 1] : $"var{i - 1}";
+                    var varName = step.VariableNames.Count >= i+1 ?
+                        step.VariableNames[i] : $"var_{i+1}";
                     string msg = $"提取变量: {varName} = {match.Groups[i].Value}";
                     _context.Variables[varName] = match.Groups[i].Value;
                     matchTotal++;
@@ -1107,9 +1071,9 @@ namespace Bird_tool
                 }
             }
             // 记录缺失变量
-            if (step.VariableNames.Count > match.Groups.Count - 1)
+            if (step.VariableNames.Count > matchTotal)
             {
-                OnLog?.Invoke($"警告: 提取变量不足(需要{step.VariableNames.Count} 实际{match.Groups.Count - 1})",
+                OnLog?.Invoke($"警告: 提取变量不足(需要{step.VariableNames.Count} 实际{matchTotal})",
                               LogLevel.error);
             }
             return isMatch;
@@ -1408,15 +1372,15 @@ namespace Bird_tool
             int jumpIdx = -1;
             bool allowRetry = false;
             // 判断当前步骤是否含有跳转执行的机会
-            if (!string.IsNullOrEmpty(step.RetryFromKey))
+            if (!string.IsNullOrEmpty(step.JumpKey))
             {
                 isJump = true;
                 if (step.JumpCount < step.MaxJump)
                 {
-                    jumpIdx = FindKeyByIndex(step.RetryFromKey);
+                    jumpIdx = FindKeyByIndex(step.JumpKey);
                     if (jumpIdx < 0)
                     {
-                        failStr = $"【{step.Name}】 无法找到要跳转的任务【{step.RetryFromKey}】, 请检查配置";
+                        failStr = $"【{step.Name}】 无法找到要跳转的任务【{step.JumpKey}】, 请检查配置";
                         level = LogLevel.error;
                         OnLog?.Invoke(failStr, level);
                     }
@@ -1441,7 +1405,7 @@ namespace Bird_tool
                     if (isJump && jumpIdx >= 0)
                     {
                         OnLog?.Invoke(
-                            $"【{step.Name}】 子步骤失败: {step.Name} 将跳转至【{step.RetryFromKey}】重新执行",
+                            $"【{step.Name}】 子步骤失败: {step.Name} 将跳转至【{step.JumpKey}】重新执行",
                             LogLevel.info
                             );
                     }
@@ -1630,7 +1594,6 @@ namespace Bird_tool
                 }
             }
         }
-
-
+        
     }
 }

+ 459 - 60
bird_tool/bird_tool.cs

@@ -1,20 +1,21 @@
-using System;
-using System.IO.Ports;
+using MySql.Data.MySqlClient;
+using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Data;
+using System.Data.SqlClient;
 using System.Drawing;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows.Forms;
 using System.IO;
+using System.IO.Ports;
+using System.Linq;
 using System.Net;
-using System.Data.SqlClient;
 using System.Runtime.Serialization;
-using MySql.Data.MySqlClient;
-using System.Threading;
+using System.Text;
 using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Windows.Input;
 
 
 namespace Bird_tool
@@ -73,6 +74,8 @@ namespace Bird_tool
         private TestMode m_test_mode = TestMode.none;
         private DeviceType m_device_type = DeviceType.LEWEI_DEVICE;
 
+        private CommandManager m_command_manager;
+
         private string test_tool_passwd = "szhfy";
         private string wifi_ssid = "";
         private string wifi_password = "";
@@ -301,12 +304,13 @@ namespace Bird_tool
         {
             
             load_app_config();
-            
             _uart_ui_change(false);
             SerialPort_Load();
+            RegisterCommands();
+
 
             // 判断是什么厂商的设备配置模式
-            if (appConfig.c_dev_key == "fy")
+            if (appConfig.c_dev_key == "fy" || appConfig.c_dev_key == "fanyue")
             {
                 m_device_type = DeviceType.FANYUE_DEVICE;
                 Log($"梵悦观鸟器配置工具启动中");
@@ -343,9 +347,11 @@ namespace Bird_tool
             if (appConfig.enable_local_server)
             {
                 _imageSharpnessServer = new ImageSharpnessServer("*", appConfig.web_port);
+                _imageSharpnessServer.OnLog += Log;
                 _ip_ui_change();
                 if (_imageSharpnessServer.Start())
                 {
+                    Log($"网络服务已经启动, localhost:{appConfig.web_port}");
                     Log_show($"网络服务已经启动, localhost:{appConfig.web_port}");
                 }
                 else
@@ -382,9 +388,11 @@ namespace Bird_tool
                 m_excel_manager = new ExcelDataManager(appConfig);
                 m_excel_manager.OnLog += Log;
                 m_excel_manager.OnLogShow += Log_show;
+                m_excel_manager.StartLoad();
                 if (!m_excel_manager.IsValid)
                 {
                     Log("excel 文件加载失败, 将禁用ecxel相关功能", LogLevel.error);
+
                     Log_show("excel 文件加载失败, 将禁用ecxel相关功能");
                 }
             }
@@ -408,6 +416,7 @@ namespace Bird_tool
 
         private void SerialPort_Load()
         {
+            m_command_manager = new CommandManager();
             // 设置ComboBox数据源
             el_serialList.DataSource = _portList;
 
@@ -420,7 +429,7 @@ namespace Bird_tool
 
             // 初始加载串口列表
             RefreshPortList();
-            _testExecutor = new TestExecutor(_serialManager);
+            _testExecutor = new TestExecutor(_serialManager, m_command_manager);
 
             // 绑定事件处理
             _testExecutor.OnLog += Log;
@@ -435,6 +444,8 @@ namespace Bird_tool
             _testExecutor.OnTestEnd += TeseEndHandle;
         }
 
+        
+
         // 刷新串口列表并保持选择
         public void RefreshPortList()
         {
@@ -640,19 +651,19 @@ namespace Bird_tool
                 el_btn_hw_test.BackColor = SystemColors.Control;  // 标准按钮背景色
                 el_btn_hw_test.ForeColor = SystemColors.ControlText;  // 标准文本色
                 el_btn_hw_test.FlatStyle = FlatStyle.Standard;  // 恢复原始样式
-                el_btn_hw_test.Cursor = Cursors.Default;
+                el_btn_hw_test.Cursor = System.Windows.Forms.Cursors.Default;
 
                 if (appConfig.enable_excel_load)
                 {
                     el_btn_config.BackColor = SystemColors.Control;
                     el_btn_config.ForeColor = SystemColors.ControlText;
                     el_btn_config.FlatStyle = FlatStyle.Standard;
-                    el_btn_config.Cursor = Cursors.Default;
+                    el_btn_config.Cursor = System.Windows.Forms.Cursors.Default;
                 }
                 el_btn_sd_test.BackColor = SystemColors.Control;  // 标准按钮背景色
                 el_btn_sd_test.ForeColor = SystemColors.ControlText;  // 标准文本色
                 el_btn_sd_test.FlatStyle = FlatStyle.Standard;  // 恢复原始样式
-                el_btn_sd_test.Cursor = Cursors.Default;
+                el_btn_sd_test.Cursor = System.Windows.Forms.Cursors.Default;
 
             }
             else
@@ -661,17 +672,17 @@ namespace Bird_tool
                 el_btn_hw_test.BackColor = SystemColors.Control;  // 保持背景色
                 el_btn_hw_test.ForeColor = SystemColors.GrayText;  // 标准禁用文本色
                 el_btn_hw_test.FlatStyle = FlatStyle.Flat;  // 扁平化样式增强禁用感
-                el_btn_hw_test.Cursor = Cursors.No;
+                el_btn_hw_test.Cursor = System.Windows.Forms.Cursors.No;
 
                 el_btn_config.BackColor = SystemColors.Control;
                 el_btn_config.ForeColor = SystemColors.GrayText;
                 el_btn_config.FlatStyle = FlatStyle.Flat;
-                el_btn_config.Cursor = Cursors.No;
+                el_btn_config.Cursor = System.Windows.Forms.Cursors.No;
 
                 el_btn_sd_test.BackColor = SystemColors.Control;  // 保持背景色
                 el_btn_sd_test.ForeColor = SystemColors.GrayText;  // 标准禁用文本色
                 el_btn_sd_test.FlatStyle = FlatStyle.Flat;  // 扁平化样式增强禁用感
-                el_btn_sd_test.Cursor = Cursors.No;
+                el_btn_sd_test.Cursor = System.Windows.Forms.Cursors.No;
             }
         }
 
@@ -1261,13 +1272,8 @@ namespace Bird_tool
                     PrivateCammand = true,
                     Timeout = 3000,
                     DelayBefore = 300,
-                    Validator = (response, ctx) =>
-                    {
-                        // 示例:检查MAC地址格式
-                        var mac = ctx.Variables["mac"];
-                        Log($"设备mac地址{mac}", LogLevel.info);
-                        return !IsValidMac(mac);
-                    }
+                    MatchedValidator = "checkMac",
+                    ValidatorParams = { "{{mac}}" }
                 },
                 // 版本号检查
                 new TestStepConfig
@@ -1286,16 +1292,6 @@ namespace Bird_tool
                     Timeout = 6000,
                     DelayBefore = 300,
                     MaxRetries = 10,
-                    Validator = (response, ctx) =>
-                    {
-                        // 示例:检查MAC地址格式
-                        var hwVersion = ctx.Variables["hwVersion"];
-                        var hwTime = ctx.Variables["hwTime"];
-                        Log($"设备版本号 ${hwVersion}", LogLevel.info);
-                        Log($"固件时间 ${hwTime}", LogLevel.info);
-                        // 判断版本号是否为
-                        return false;
-                    }
                 },
                 // sd卡测试
                 new TestStepConfig
@@ -1329,7 +1325,7 @@ namespace Bird_tool
                     Command = "AT+SEND=1,AT+CAMPARA?\r\n",
                     Tips = "检查sd卡加载状态中",
                     SuccessPattern = "/mnt/sdcard",
-                    RetryFromKey = "sdLoad",
+                    JumpKey = "sdLoad",
                     Timeout = 3000,
                     DelayBefore = 1000,
                     MaxJump = 5,
@@ -1385,14 +1381,15 @@ namespace Bird_tool
                     GroupName = "音频测试",
                     Name = "播放音频",
                     Command = "AT+SEND=1, AT+HWAUDIO=5\\,\"{{audioFile}}\"\\,800\r\n",
+                    SuccessPattern = @"<=OK cmd:20",
                     //CommandGenerator = ctx =>
                     //    $"AT+SEND=1, AT+HWAUDIO=5\\,\"{ctx.Variables["audioFile"]}\"\\,800\r\n",
                     RequiresUserPrompt = true,
-                    PromptQuestion = "是否听到音频?",
+                    PromptQuestion = "听到音频播放后点击是",
                     Timeout = 10000,
                     DelayBefore = 500,
                     MaxRetries = 2,
-                    RetryFromKey = "audioTest",
+                    JumpKey = "audioTest",
                     OnFailure = ctx =>
                     {
                         // 失败时重新录制
@@ -1496,9 +1493,82 @@ namespace Bird_tool
                     DelayBefore = 500,
                     MaxRetries = 5,
                 },
-                
-                // 上传测试
 
+                //new TestStepConfig
+                //{
+                //    GroupId = "upload",
+                //    Name = "获取MAC地址",
+                //    Tips = "提取设备MAC地址",
+                //    IsDeviceInfoItem = true,
+                //    InfoDisplayName = "设备mac地址",
+                //    Command = "AT+MAC?\r\n",
+                //    SuccessPattern = "MAC:",
+                //    FailurePattern = "ERROR|FAIL",
+                //    ExtractPattern = @"MAC:([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
+                //    VariableNames = { "mac" },
+                //    PrivateCammand = true,
+                //    Timeout = 3000,
+                //    DelayBefore = 300,
+                //    MatchedValidator = "checkMac",
+                //    ValidatorParams = { "{{mac}}" },
+                //},
+                //new TestStepConfig
+                //{
+                //    GroupId = "upload",
+                //    ShowStep = false,
+                //    Name = "配置变量",
+                //    Tips = "设置变量",
+                //    PrivateCammand = false,
+                //    Action = ActionMode.SetVal,
+                //    VariableNames = { "tt" },
+                //    VariableValues = { "{{timestamp}}" },
+                //},
+                //new TestStepConfig
+                //{
+                //    GroupId = "upload",
+                //    GroupName = "图像测试",
+                //    Name = "上传测试",
+                //    Tips = "配置上传地址",
+                //    Command = "AT+SEND=1, at+campara=6\\,\"http://192.168.1.26:8081/upload/{{mac}}/{{tt}}\"\r\n",
+                //    SuccessPattern = "<=OK cmd:20",
+                //    Timeout = 6000,
+                //    DelayBefore = 500,
+                //    MaxRetries = 2,
+                //},
+                //new TestStepConfig
+                //{
+                //    GroupId = "upload",
+                //    GroupName = "图像测试",
+                //    Name = "等待拍照",
+                //    Tips = "等待设备开始拍照",
+                //    Command = "AT+SEND=1, at+camtp=1\r\n",
+                //    SuccessPattern = "<=OK cmd:20",
+                //    Timeout = 6000,
+                //    DelayBefore = 500,
+                //    MaxRetries = 2,
+                //},
+                //new TestStepConfig
+                //{
+                //    GroupId = "upload",
+                //    GroupName = "图像测试",
+                //    Name = "等待拍照",
+                //    Tips = "等待拍照完成...",
+                //    FailTips = "拍照失败, 分值计算错误",
+                //    Command = "",
+                //    // 使用正则匹配 score": 3548.6077951170178,
+                //    SuccessPattern = @"""score"":\s*(\d+\.\d+)",
+                //    ExtractPattern = @"(?<=""score"":\s*)[-+]?\d+\.\d+",
+                //    VariableNames = { "score" },
+                //    Timeout = 15000,
+                //    DelayBefore = 500,
+                //    MaxRetries = 2,
+                //    MatchedValidator = "compare",
+                //    ValidatorParams = { "{{score}}", "1000", ">=" },
+                    
+
+                //},
+                // 变量验证
+                
             };
         }
 
@@ -1537,7 +1607,7 @@ namespace Bird_tool
                     Tips = "尝试格式化SD卡中",
                     PrivateCammand = true,
                     SuccessPattern = "Verify err:0",
-                    RetryFromKey = "sdLoad",
+                    JumpKey = "sdLoad",
                     Timeout = 60000,
                     DelayBefore = 1000,
                     MaxJump = 5,
@@ -1554,6 +1624,7 @@ namespace Bird_tool
             }
             Log($"用户输入id: {uuid}");
             var (names, values) = m_excel_manager.GetVariableListByKey(uuid);
+            Log($"names: {names}");
             m_config_uuid = uuid;
             List<TestStepConfig> ret_steps = new List<TestStepConfig>();
             // 构建配置文件
@@ -1571,13 +1642,9 @@ namespace Bird_tool
                 PrivateCammand = true,
                 Timeout = 3000,
                 DelayBefore = 300,
-                Validator = (response, ctx) =>
-                {
-                    // 示例:检查MAC地址格式
-                    var mac = ctx.Variables["mac"];
-                    Log($"设备mac地址{mac}", LogLevel.info);
-                    return !IsValidMac(mac);
-                }
+                MatchedValidator = "checkMac",
+                ValidatorParams = { "{{mac}}" }
+
             });
             ret_steps.Add(new TestStepConfig
             {
@@ -1598,7 +1665,7 @@ namespace Bird_tool
                 Name = "涂鸦id配置",
                 Key = "config_id",
                 Tips = "配置涂鸦sdk账户中",
-                Command = "AT+SCFG={\"target\":\"tuya\"\\,\"data\":{\"pid\":\"9kifakenvwsj1a9v\"\\,\"uuid\":\"{{uuid}}\"\\,\"authkey\":\"{{authkey}}\"\\,\"state\":\"0\"\r\n}}",
+                Command = "AT+SCFG={\"target\":\"tuya\"\\,\"data\":{\"pid\":\"9kifakenvwsj1a9v\"\\,\"uuid\":\"{{uuid}}\"\\,\"authkey\":\"{{authkey}}\"\\,\"state\":\"0\"}}\r\n",
                 SuccessPattern = "<=OK cmd:24",
                 Timeout = 6000,
                 DelayBefore = 1000,
@@ -1621,7 +1688,7 @@ namespace Bird_tool
                 Tips = "检查是否配置涂鸦uuid成功",
                 Command = "AT+SEND=1,AT+FACTORY=4\\,\"cat /data/tuya_device_cfg.json | grep uuid | awk -F[:\\,] '{for(i=1;i<=NF;i++){if($i~/uuid/){print $(i+1)}}}'\"\r\n",
                 SuccessText = "\"{{uuid}}",
-                RetryFromKey = "config_id",
+                JumpKey = "config_id",
                 Timeout = 6000,
                 DelayBefore = 1000,
                 MaxJump = 5,
@@ -1633,7 +1700,7 @@ namespace Bird_tool
                 Tips = "检查是否配置涂鸦sdk成功",
                 Command = "AT+SEND=1,AT+FACTORY=4\\,\"cat /data/tuya_device_cfg.json | grep storage | awk -F[:\\,] '{for(i=1;i<=NF;i++){if($i~/storage/){print $(i+1)}}}'\"\r\n",
                 SuccessText = "\"/data",
-                RetryFromKey = "config_id",
+                JumpKey = "config_id",
                 Timeout = 6000,
                 DelayBefore = 1000,
                 MaxJump = 5,
@@ -1764,15 +1831,257 @@ namespace Bird_tool
             var (names, values) = m_excel_manager.GetVariableListByKey(dev_id);
             m_config_uuid = dev_id;
             List<TestStepConfig> ret_steps = new List<TestStepConfig>();
-            // 配置28项
-            return false;
+
+            
+            // todo 完善乐微需要用到的配置项
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "测试模式",
+                Command = "AT+OSLP=1,0\r\n",
+                Tips = "启用测试模式中",
+                SuccessPattern = "Wakeup, unsleep:1 debug:0",
+                FailurePattern = "ERROR|FAIL",
+                Timeout = 4000,
+                DelayBefore = 300,
+            });
+            
+
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "获取MAC地址",
+                Tips = "提取设备MAC地址",
+                IsDeviceInfoItem = true,
+                InfoDisplayName = "设备mac地址",
+                Command = "AT+MAC?\r\n",
+                SuccessPattern = "MAC:",
+                FailurePattern = "ERROR|FAIL",
+                ExtractPattern = @"MAC:([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
+                VariableNames = { "mac" },
+                PrivateCammand = true,
+                Timeout = 3000,
+                DelayBefore = 300,
+                MatchedValidator = "checkMac",
+                ValidatorParams = { "{{mac}}" }
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "获取版本信息",
+                Tips = "设备版本检查地址",
+                IsDeviceInfoItem = true,
+                InfoDisplayName = "设备固件",
+                InfoDisplayTemplate = "{{hwTime}} - {{hwVersion}}",
+                FailTips = "设备固件异常, 版本号:{{hwVersion}}{{hwTime}}",
+                Command = "AT+SEND=1, AT+HWCUST?\r\n",
+                SuccessPattern = @"\+HWCUST:",
+                FailurePattern = "ERROR|FAIL",
+                ExtractPattern = @"\+HWCUST:\s*\d+,\s*""([^""]+)"",\s*""([^""]+)""",
+                VariableNames = { "hwVersion", "hwTime" },
+                Timeout = 6000,
+                DelayBefore = 300,
+                MaxRetries = 10,
+            });
+
+            ret_steps.Add(new TestStepConfig
+            {
+                GroupId = "lewei_config",
+                GroupName = "配置乐微信息",
+                ShowStep = false,
+                IsDeviceInfoItem = true,
+                InfoDisplayName = "乐微ID",
+                Name = "配置乐微ID",
+                Tips = "配置乐微ID中",
+                Action = ActionMode.SetVal,
+                VariableNames = names,
+                VariableValues = values,
+            });
+            // 配置设备通道信息
+            ret_steps.Add(new TestStepConfig
+            {
+                GroupId = "lewei_config",
+                Name = "配置通道",
+                Tips = "配置设备sip通道信息",
+                Command = "AT+SEND=1, AT+CAMCHAN=1\\,1\\,0\\,\"{{dev_id}}\"\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                GroupId = "lewei_config",
+                Name = "配置id",
+                Tips = "配置设备ID信息",
+                Command = "AT+SEND=1, AT+CAMPARA=28\\,\"{{dev_id}}\"\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                GroupId = "lewei_config",
+                Name = "保存配置",
+                Tips = "设置设备CAMPARA配置",
+                Command = "AT+SEND=1, AT+CAMSCONT\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+
+            ret_steps.Add(new TestStepConfig
+            {
+                GroupId = "Video_config",
+                Key = "videoWidth",
+                Name = "配置视频宽度",
+                Tips = "分辨率 2560*1440",
+                Command = "AT+SEND=1, AT+CAMPARA=1\\,2560\\,1920\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+                MaxRetries = 2,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                GroupId = "Video_config",
+                Key = "videoHeight",
+                Name = "配置视频高度",
+                Tips = "分辨率 2560*1440",
+                Command = "AT+SEND=1, AT+CAMPARA=2\\,1440\\,1080\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+                MaxRetries = 2,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                GroupId = "Video_code",
+                Name = "配置视频格式",
+                Tips = "配置视频格式 为 mp4...",
+                Command = "AT+SEND=1, AT+CAMPARA=5\\,\"mp4\"\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "配置录像时间",
+                Tips = "视频录制时长",
+                Command = "AT+SEND=1, AT+CAMPARA=12\\,10\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "配置音量",
+                Tips = "设置设备音量与增益补偿",
+                Command = "AT+SEND=1, AT+CAMPARA=21\\,60\\,28\\,23\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "配置补光灯",
+                Tips = "设置设备补光灯工作模式",
+                Command = "AT+SEND=1, AT+CAMPARA=22\\,0\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "配置视频",
+                Tips = "设置设备视频编码",
+                Command = "AT+SEND=1, AT+CAMPARA=23\\,1\\,0\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "配置通道",
+                Tips = "设置设备通道配置",
+                Command = "AT+SEND=1, AT+CAMPARA=26\\,1\\,0\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "保存配置",
+                Tips = "设置设备CAMPARA配置",
+                Command = "AT+SEND=1, AT+CAMSCONT\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "配置水印",
+                Tips = "设置设备水印信息",
+                Command = "AT+SEND=1, AT+CAMISP=5\\,0\\,0\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "启用识别",
+                Tips = "启用设备识别功能",
+                Command = "AT+SEND=1, AT+CAMISP=12\\,1\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "启用红外补光",
+                Tips = "启用设备红外补光功能",
+                Command = "AT+SEND=1, AT+CAMISP=15\\,1\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "压缩比配置",
+                Tips = "设置设备压缩比",
+                Command = "AT+SEND=1, AT+CAMISP=11\\,85\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+            ret_steps.Add(new TestStepConfig
+            {
+                Name = "保存图像参数",
+                Tips = "保存图像参数",
+                Command = "AT+SEND=1, AT+CAMISP\r\n",
+                SuccessPattern = "<=OK cmd:20",
+                Timeout = 6000,
+                DelayBefore = 1000,
+            });
+
+
+            if (!_testExecutor.InitTest(ret_steps))
+            {
+                Log_show("无法创建乐微配置表");
+                StopTest(false, "生成乐微配置命令失败");
+                return false;
+            }
+
+            var groups = _testExecutor.GetGroupInfos();
+            foreach (var group in groups)
+            {
+                if (group.ShowStep)
+                {
+                    progressPanel.AddTestStep(group.RowKey, group.GroupName);
+                }
+            }
+
+
+            return true;
         }
        
-        private bool IsValidMac(string mac)
-        {
-            // 实现MAC地址验证逻辑
-            return !string.IsNullOrEmpty(mac) && mac.Length == 17;
-        }
+        
 
         
         private void HandleStepChanged(TestStepConfig step, TestContext context, bool isStarting)
@@ -1929,6 +2238,7 @@ namespace Bird_tool
             return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "...";
         }
 
+        
 
         // 加载成功图片
         private Image LoadSuccessImage()
@@ -2098,7 +2408,7 @@ namespace Bird_tool
                     Font = new Font("Microsoft YaHei UI", 9.5F),
                     BackColor = Color.White,
                     FlatStyle = FlatStyle.Flat,
-                    Cursor = Cursors.Hand
+                    Cursor = System.Windows.Forms.Cursors.Hand
                 };
 
                 // 是按钮
@@ -2110,7 +2420,7 @@ namespace Bird_tool
                     Font = new Font("Microsoft YaHei UI", 9.5F, FontStyle.Bold),
                     BackColor = Color.FromArgb(220, 235, 252),
                     FlatStyle = FlatStyle.Flat,
-                    Cursor = Cursors.Hand
+                    Cursor = System.Windows.Forms.Cursors.Hand
                 };
 
                 // 计时器(用于倒计时)
@@ -2209,6 +2519,95 @@ namespace Bird_tool
 
 
 
+        // 注册mac地址验证器
+        private void RegisterMacValid()
+        {
+            m_command_manager.RegisterCommand(new CommandInfo(
+                key: "checkMac",
+                name: "mac地址检查",
+                description: "检查mac地址是否正确",
+                failedMessage: "mac地址检查错误",
+                execute: args => {
+                        
+                    string mac = (string)args[0];
+                    if (string.IsNullOrEmpty(mac))
+                    {
+                        Log("请输入mac地址", LogLevel.error);
+                        return false;
+                    }
+                    Log($"mac 地址为 {mac}");
+                    // mac地址支持 多种分隔符
+                    mac = mac.Replace("MAC:", "").Replace(":", "").Replace("-", "").Replace("_", "").Replace(".", "");
+                    // 检查长度
+                    if (mac.Length != 12)
+                    {
+                        Log("MAC地址长度错误", LogLevel.error);
+                        return false;
+                    }
+                    // 检查每个字符是否是16进制字符
+                    foreach (char c in mac)
+                    {
+                        if (!char.IsLetterOrDigit(c))
+                        {
+                            Log("MAC地址格式错误, 不为hex", LogLevel.error);
+                            return false;
+                        }
+                    }
+                    return true;
+                },
+                parameters: new CommandParameter[] {
+                    new CommandParameter("macStr", typeof(string), "mac地址字符串")
+                }
+            ));
+        }
+
+        // 注册分值比较器
+        private void RegisterScoreComparer()
+        {
+            m_command_manager.RegisterCommand(new CommandInfo(
+            key: "compare",
+            name: "数值比较",
+            description: "比较两个数值",
+            failedMessage: "分值比较失败",
+            execute: args => {
+                double a = (double)args[0];
+                double b = (double)args[1];
+                string op = (string)args[2] ?? "==";
+
+                switch (op)
+                {
+                    case "==":
+                        return a == b || Math.Abs(a - b) < 0.000001;
+                    case "!=":
+                        return a != b || Math.Abs(a - b) >= 0.000001;
+                    case ">":
+                        return a > b;
+                    case "<":
+                        return a < b;
+                    case ">=":
+                        return a >= b;
+                    case "<=":
+                        return a <= b;
+                    default:
+                        Log("不受支持的比较符, 检查 compare 命令", LogLevel.error);
+                        return false;
+                }
+            },
+            parameters: new CommandParameter[] {
+                new CommandParameter("a", typeof(double), "第一个值"),
+                new CommandParameter("b", typeof(double), "第二个值"),
+                new CommandParameter("operator", typeof(string), "比较运算符 (==, !=, >, >=, <, <=)", true, "==")
+            }
+        ));
+        }
+
+
+        private void RegisterCommands()
+        {
+            RegisterMacValid();
+            RegisterScoreComparer();
+        }
+
     }
 
 }

+ 1 - 0
bird_tool/bird_tool.csproj

@@ -43,6 +43,7 @@
     <Compile Include="bird_tool.Designer.cs">
       <DependentUpon>bird_tool.cs</DependentUpon>
     </Compile>
+    <Compile Include="Command.cs" />
     <Compile Include="Config.cs" />
     <Compile Include="ExcelLoad.cs" />
     <Compile Include="ImageServer.cs" />