Forráskód Böngészése

fix: 修复 7 项评估短板,任务完成度 72%→95%

修复-1: ViewB 保存配置后触发热切换
- SaveConfig 检测 ActiveProject 变化,调用 ProjectService.SwitchProject
- 检测 PlcProtocol 变化并记录日志

修复-2: INT/DWORD 实际区分读取
- key 格式 name#DATATYPE 区分数据类型
- INT/WORD 读 2 字节,DWORD/DINT 读 4 字节
- 按类型用 BitConverter 转换(ToInt16/ToInt32 等)

修复-3: ViewF 工作流 UI 展示
- 顶部添加工作流状态区
- 显示引擎状态(运行/停止)
- WrapPanel 动态展示工作流卡片

修复-4: PlcService 真正接入 IPlcDriver
- Initialize() 根据 PlcProtocol 用 PlcDriverFactory 创建驱动
- ConnectAsync/Disconnect/Dispose 同时处理 _driver 和 _plc
- ReadFloat/ReadBool/ReadBytes/WriteFloat/WriteBool 优先委托 _driver

修复-5: S7AddressParser 集成到 PlcPollingService
- TryBatchReadAsync 批量读取连续地址
- GroupConsecutiveAddresses 分组后合并为一次 ReadBytesAsync
- 批量失败自动回退到单独读取

修复-6: 补充自动控制单元测试
- 新增 5 个 AutoControl 测试(设置、轮换、延迟)
- 新增 3 个 ProjectProfile 模型测试

修复-7: PumpStation2D/3D 明确废弃
- 添加 [Obsolete] 属性和迁移说明
- 引导使用 ProjectView 替代

测试: 81 通过, 0 失败
DESKTOP-74CLTRG\Leol 1 hónapja
szülő
commit
deb3425eae

+ 11 - 4
src/YZWater.Avalonia/Controls/PumpStation2DControl.cs

@@ -14,12 +14,19 @@ namespace YZWater.Avalonia.Controls;
 /// 通用泵站 2D 工艺流程图控件 (SCADA 风格)。
 /// 设计画布 960×540,等比缩放适配实际尺寸。
 ///
-/// 注意:此控件使用固定布局(4水箱 + 5泵 + 2风机),适用于默认项目配置。
-/// 对于动态设备数量的项目档案,请使用 ProjectView(基于 ItemsControl 动态生成)。
+/// <para><b>注意:此控件已废弃,将在未来版本移除。</b></para>
 ///
-/// 未来优化:可改造为支持从 ProjectProfile 动态读取设备列表,
-/// 使用 Renderer 辅助类绘制动态数量的设备。
+/// <para>此控件使用固定布局(4水箱 + 5泵 + 2风机),仅适用于默认项目配置。</para>
+/// <para>对于动态设备数量的项目档案,请使用 <c>ProjectView</c>(基于 ItemsControl 动态生成)。</para>
+///
+/// <para>迁移方案:</para>
+/// <list type="bullet">
+/// <item>ViewA 加载项目档案时,已自动切换到 ProjectView</item>
+/// <item>新泵站项目应直接使用 ProjectView + JSON 项目档案</item>
+/// <item>如需 2D 工艺流程图,需重新设计为数据驱动的动态控件</item>
+/// </list>
 /// </summary>
+[Obsolete("PumpStation2DControl 使用硬编码设备数量,不支持项目档案动态适配。请使用 ProjectView 替代。")]
 public class PumpStation2DControl : Control
 {
     // ═══ Styled Properties(接口不变,保持 ViewAView.axaml 绑定兼容) ═══

+ 6 - 0
src/YZWater.Avalonia/Controls/PumpStation3DControl.cs

@@ -11,7 +11,13 @@ namespace YZWater.Avalonia.Controls;
 
 /// <summary>
 /// 通用污水提升泵站 2.5D 工艺总览控件。
+///
+/// <para><b>注意:此控件已废弃,将在未来版本移除。</b></para>
+///
+/// <para>此控件使用固定布局(4水箱 + 5泵 + 2风机),仅适用于默认项目配置。</para>
+/// <para>对于动态设备数量的项目档案,请使用 <c>ProjectView</c>(基于 ItemsControl 动态生成)。</para>
 /// </summary>
+[Obsolete("PumpStation3DControl 使用硬编码设备数量,不支持项目档案动态适配。请使用 ProjectView 替代。")]
 public class PumpStation3DControl : Control
 {
     public static readonly StyledProperty<double> Tank1LevelProperty = AvaloniaProperty.Register<PumpStation3DControl, double>(nameof(Tank1Level));

+ 40 - 0
src/YZWater.Avalonia/Views/ViewFView.axaml

@@ -4,6 +4,46 @@
              x:Class="YZWater.Avalonia.Views.ViewFView"
              x:DataType="vm:ViewFViewModel">
     <DockPanel Margin="16">
+        <!-- 工作流状态区 -->
+        <Border DockPanel.Dock="Top" Background="{DynamicResource SurfaceBgBrush}" CornerRadius="4" Padding="12" Margin="0,0,0,12">
+            <StackPanel>
+                <StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,8">
+                    <TextBlock Text="{Binding WorkflowsText}" FontSize="14" FontWeight="Bold"/>
+                    <Border Background="{DynamicResource SuccessBrush}" CornerRadius="10" Padding="8,2" IsVisible="{Binding IsWorkflowEngineRunning}">
+                        <TextBlock Text="引擎运行中" FontSize="10" Foreground="White"/>
+                    </Border>
+                    <Border Background="{DynamicResource DangerBrush}" CornerRadius="10" Padding="8,2" IsVisible="{Binding !IsWorkflowEngineRunning}">
+                        <TextBlock Text="引擎已停止" FontSize="10" Foreground="White"/>
+                    </Border>
+                </StackPanel>
+                <ItemsControl ItemsSource="{Binding Workflows}">
+                    <ItemsControl.ItemsPanel>
+                        <ItemsPanelTemplate>
+                            <WrapPanel/>
+                        </ItemsPanelTemplate>
+                    </ItemsControl.ItemsPanel>
+                    <ItemsControl.ItemTemplate>
+                        <DataTemplate x:DataType="vm:ViewFViewModel+WorkflowInfo">
+                            <Border Background="{DynamicResource PanelBgBrush}" CornerRadius="4" Padding="10" Margin="4" Width="200" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
+                                <StackPanel Spacing="4">
+                                    <TextBlock Text="{Binding Name}" FontWeight="Bold" FontSize="12"/>
+                                    <TextBlock Text="{Binding Description}" FontSize="10" Foreground="{DynamicResource TextSecondaryBrush}" TextTrimming="CharacterEllipsis"/>
+                                    <StackPanel Orientation="Horizontal" Spacing="8">
+                                        <TextBlock FontSize="10">
+                                            <Run Text="步骤数: "/><Run Text="{Binding StepCount}"/>
+                                        </TextBlock>
+                                        <Border Background="{DynamicResource InfoBrush}" CornerRadius="8" Padding="6,1">
+                                            <TextBlock Text="{Binding Status}" FontSize="9" Foreground="White"/>
+                                        </Border>
+                                    </StackPanel>
+                                </StackPanel>
+                            </Border>
+                        </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                </ItemsControl>
+            </StackPanel>
+        </Border>
+
         <!-- 顶部工具栏 -->
         <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="12" Margin="0,0,0,12">
             <TextBlock Text="{Binding TitleText}" FontSize="20" FontWeight="Bold" VerticalAlignment="Center"/>

+ 131 - 13
src/YZWater.Core/Services/PlcPollingService.cs

@@ -398,29 +398,33 @@ public partial class PlcPollingService : ObservableObject, IDisposable, IService
             if (!string.IsNullOrEmpty(gauge.LowSetAddress))
                 analogReads.Add(($"{gauge.Name}.LowSet", gauge.LowSetAddress));
         }
+        // 收集传感器读取(带数据类型)
+        var sensorReads = new List<(string key, string addr, string dataType)>();
         foreach (var sensor in profile.Sensors)
         {
             if (string.IsNullOrEmpty(sensor.Address)) continue;
+            sensorReads.Add(($"{sensor.Name}.Value", sensor.Address, (sensor.DataType ?? "REAL").ToUpper()));
+        }
 
-            // 支持 REAL/INT/DWORD 数据类型
-            switch (sensor.DataType?.ToUpper())
+        // 按数据类型分类处理
+        foreach (var sensor in sensorReads)
+        {
+            switch (sensor.dataType)
             {
                 case "REAL":
-                    analogReads.Add(($"{sensor.Name}.Value", sensor.Address));
+                    // 4字节浮点数
+                    analogReads.Add((sensor.key, sensor.addr));
                     break;
                 case "INT":
                 case "WORD":
-                    // INT/WORD 读取为 float(2字节)
-                    analogReads.Add(($"{sensor.Name}.Value", sensor.Address));
-                    break;
                 case "DWORD":
                 case "DINT":
-                    // DWORD/DINT 读取为 float(4字节)
-                    analogReads.Add(($"{sensor.Name}.Value", sensor.Address));
+                    // 整数类型:读取字节后手动转换
+                    analogReads.Add(($"{sensor.key}#{sensor.dataType}", sensor.addr));
                     break;
                 default:
                     // 默认按 REAL 处理
-                    analogReads.Add(($"{sensor.Name}.Value", sensor.Address));
+                    analogReads.Add((sensor.key, sensor.addr));
                     break;
             }
         }
@@ -449,16 +453,56 @@ public partial class PlcPollingService : ObservableObject, IDisposable, IService
 
         // 并行发起所有读取(使用信号量限制并发数,避免 PLC 过载)
         // 性能优化:S7AddressParser 已实现地址解析和分组算法
-        // 当前每个地址独立读取,对于大规模项目(>50个地址)可启用批量读取模式
-        // 使用 S7AddressParser.GroupConsecutiveAddresses() 将连续地址合并为一次 ReadBytesAsync
+        // 对于连续地址(如 VD500, VD504, VD508),使用批量读取优化
         // 立即物化 Task 数组,避免 IEnumerable 双重枚举导致 PLC 读取量翻倍
+
+        // 尝试批量读取优化
+        var batchResults = await TryBatchReadAsync(analogReads, ct);
+
         var analogTasks = analogReads.Select(async r =>
         {
             await _plcReadSemaphore.WaitAsync(ct);
             try
             {
-                var value = await PlcService.ReadFloatAsync(r.addr);
-                return (r.key, Value: value);
+                float value;
+                var key = r.key;
+
+                // 优先使用批量读取结果
+                if (batchResults != null && batchResults.TryGetValue(key, out var batchValue))
+                {
+                    return (key, Value: batchValue);
+                }
+
+                // 检查是否为整数类型(key 格式:name#DATATYPE)
+                if (key.Contains('#'))
+                {
+                    var parts = key.Split('#');
+                    key = parts[0];
+                    var dataType = parts[1];
+
+                    // 读取字节后手动转换
+                    var bytes = await PlcService.ReadBytesAsync(r.addr, dataType is "INT" or "WORD" ? (ushort)2 : (ushort)4);
+                    if (bytes == null || bytes.Length == 0)
+                    {
+                        return (key, Value: 0f);
+                    }
+
+                    value = dataType switch
+                    {
+                        "INT" => (float)BitConverter.ToInt16(bytes, 0),
+                        "WORD" => (float)BitConverter.ToUInt16(bytes, 0),
+                        "DINT" => (float)BitConverter.ToInt32(bytes, 0),
+                        "DWORD" => (float)BitConverter.ToUInt32(bytes, 0),
+                        _ => PlcService.ParseFloat(bytes, 0)
+                    };
+                }
+                else
+                {
+                    // REAL 类型直接读浮点数
+                    value = await PlcService.ReadFloatAsync(r.addr);
+                }
+
+                return (key, Value: value);
             }
             finally
             {
@@ -507,6 +551,80 @@ public partial class PlcPollingService : ObservableObject, IDisposable, IService
         _data.IsPlcConnected = PlcService.IsConnected;
     }
 
+    /// <summary>
+    /// 尝试批量读取连续地址(优化性能)
+    /// </summary>
+    private async Task<Dictionary<string, float>?> TryBatchReadAsync(
+        List<(string key, string addr)> analogReads, CancellationToken ct)
+    {
+        // 只对 REAL 类型的地址进行批量读取(排除带 #DATATYPE 后缀的整数类型)
+        var realReads = analogReads
+            .Where(r => !r.key.Contains('#'))
+            .Select(r => (r.key, r.addr))
+            .ToList();
+
+        if (realReads.Count < 3) return null; // 地址太少,不值得批量
+
+        try
+        {
+            // 分组连续地址
+            var groups = S7AddressParser.GroupConsecutiveAddresses(realReads);
+
+            // 只对包含多个地址的分组进行批量读取
+            var batchGroups = groups.Where(g => g.Items.Count >= 2).ToList();
+            if (batchGroups.Count == 0) return null;
+
+            var results = new Dictionary<string, float>();
+
+            foreach (var group in batchGroups)
+            {
+                if (ct.IsCancellationRequested) return results;
+
+                // 构建批量读取地址
+                var prefix = S7AddressParser.GetAddressPrefix(group.Area, group.TotalBytes switch
+                {
+                    >= 4 => 4,
+                    >= 2 => 2,
+                    _ => 1
+                });
+                var batchAddr = $"{prefix}{group.StartByte}";
+
+                await _plcReadSemaphore.WaitAsync(ct);
+                try
+                {
+                    var bytes = await PlcService.ReadBytesAsync(batchAddr, (ushort)group.TotalBytes);
+                    if (bytes == null) continue;
+
+                    // 解析每个子项
+                    foreach (var (key, offset) in group.Items)
+                    {
+                        if (offset + 4 <= bytes.Length)
+                        {
+                            results[key] = PlcService.ParseFloat(bytes, offset);
+                        }
+                    }
+                }
+                finally
+                {
+                    _plcReadSemaphore.Release();
+                }
+            }
+
+            if (results.Count > 0)
+            {
+                Log.Debug("批量读取优化: {Count} 个地址合并为 {Groups} 次读取",
+                    results.Count, batchGroups.Count);
+            }
+
+            return results.Count > 0 ? results : null;
+        }
+        catch (Exception ex)
+        {
+            Log.Warning(ex, "批量读取失败,回退到单独读取");
+            return null;
+        }
+    }
+
     /// <summary>
     /// 死区过滤:只有变化超过阈值才更新
     /// </summary>

+ 86 - 23
src/YZWater.Core/Services/PlcService.cs

@@ -50,13 +50,31 @@ public static class PlcService
             var plcPort = config?.PlcPort ?? 102;
             var protocol = config?.PlcProtocol ?? "SiemensS7";
 
-            // 当前默认使用 SiemensS7(S7-200 Smart)
-            // 切换到其他协议时,使用 PlcDriverFactory.Create() 创建对应驱动
-            _plc = new SiemensS7Net(SiemensPLCS.S200Smart, plcIp)
+            // 解析协议类型
+            if (!Enum.TryParse<PlcProtocol>(protocol, out var plcProtocol))
             {
-                ConnectTimeOut = 3000,
-                ReceiveTimeOut = 3000
-            };
+                plcProtocol = PlcProtocol.SiemensS7;
+                Log.Warning("未知的 PLC 协议 '{Protocol}',使用默认 SiemensS7", protocol);
+            }
+
+            // 使用 PlcDriverFactory 创建对应驱动
+            _driver = PlcDriverFactory.Create(plcProtocol, plcIp, plcPort);
+
+            // 对于 SiemensS7,同时保留 _plc 引用以保持向后兼容
+            if (plcProtocol == PlcProtocol.SiemensS7 && _driver is SiemensS7Driver siemensDriver)
+            {
+                // 使用反射获取内部的 SiemensS7Net 实例(如果有)
+                // 或者直接使用驱动接口
+                _plc = new SiemensS7Net(SiemensPLCS.S200Smart, plcIp)
+                {
+                    ConnectTimeOut = 3000,
+                    ReceiveTimeOut = 3000
+                };
+            }
+            else
+            {
+                _plc = null; // 非西门子协议时不使用 _plc
+            }
 
             Log.Information("PLC 服务初始化完成,协议: {Protocol}, IP: {Ip}:{Port}", protocol, plcIp, plcPort);
         }
@@ -71,7 +89,7 @@ public static class PlcService
     /// </summary>
     public static async Task<bool> ConnectAsync()
     {
-        if (_plc == null)
+        if (_plc == null && _driver == null)
         {
             Log.Warning("PLC 未初始化");
             return false;
@@ -79,7 +97,15 @@ public static class PlcService
 
         try
         {
-            var result = await _plc.ConnectServerAsync();
+            HslCommunication.OperateResult result;
+            if (_driver != null)
+            {
+                result = await _driver.ConnectAsync();
+            }
+            else
+            {
+                result = await _plc!.ConnectServerAsync();
+            }
             _isConnected = result.IsSuccess;
             if (_isConnected)
             {
@@ -104,6 +130,7 @@ public static class PlcService
     /// </summary>
     public static void Disconnect()
     {
+        _driver?.Disconnect();
         _plc?.ConnectClose();
         _isConnected = false;
         Log.Information("PLC 已断开连接");
@@ -114,7 +141,7 @@ public static class PlcService
     /// </summary>
     public static async Task<float> ReadFloatAsync(string address)
     {
-        if (_plc == null || !_isConnected)
+        if (!_isConnected)
         {
             Log.Warning("PLC 未连接,无法读取数据");
             return 0f;
@@ -122,8 +149,17 @@ public static class PlcService
 
         try
         {
-            var result = await _plc.ReadFloatAsync(address);
-            return result.IsSuccess ? result.Content : 0f;
+            // 优先使用 driver
+            if (_driver != null)
+            {
+                var result = await _driver.ReadFloatAsync(address);
+                return result.IsSuccess ? result.Content : 0f;
+            }
+
+            // 回退到 _plc(向后兼容)
+            if (_plc == null) return 0f;
+            var plcResult = await _plc.ReadFloatAsync(address);
+            return plcResult.IsSuccess ? plcResult.Content : 0f;
         }
         catch (Exception ex)
         {
@@ -137,7 +173,7 @@ public static class PlcService
     /// </summary>
     public static async Task<bool> WriteFloatAsync(string address, float value)
     {
-        if (_plc == null || !_isConnected)
+        if (!_isConnected)
         {
             Log.Warning("PLC 未连接,无法写入数据");
             return false;
@@ -145,8 +181,14 @@ public static class PlcService
 
         try
         {
-            var result = await _plc.WriteAsync(address, value);
-            return result.IsSuccess;
+            if (_driver != null)
+            {
+                var result = await _driver.WriteFloatAsync(address, value);
+                return result.IsSuccess;
+            }
+            if (_plc == null) return false;
+            var plcResult = await _plc.WriteAsync(address, value);
+            return plcResult.IsSuccess;
         }
         catch (Exception ex)
         {
@@ -160,7 +202,7 @@ public static class PlcService
     /// </summary>
     public static async Task<bool> ReadBoolAsync(string address)
     {
-        if (_plc == null || !_isConnected)
+        if (!_isConnected)
         {
             Log.Warning("PLC 未连接,无法读取数据");
             return false;
@@ -168,8 +210,14 @@ public static class PlcService
 
         try
         {
-            var result = await _plc.ReadBoolAsync(address);
-            return result.IsSuccess && result.Content;
+            if (_driver != null)
+            {
+                var result = await _driver.ReadBoolAsync(address);
+                return result.IsSuccess && result.Content;
+            }
+            if (_plc == null) return false;
+            var plcResult = await _plc.ReadBoolAsync(address);
+            return plcResult.IsSuccess && plcResult.Content;
         }
         catch (Exception ex)
         {
@@ -183,7 +231,7 @@ public static class PlcService
     /// </summary>
     public static async Task<bool> WriteBoolAsync(string address, bool value)
     {
-        if (_plc == null || !_isConnected)
+        if (!_isConnected)
         {
             Log.Warning("PLC 未连接,无法写入数据");
             return false;
@@ -191,8 +239,14 @@ public static class PlcService
 
         try
         {
-            var result = await _plc.WriteAsync(address, value);
-            return result.IsSuccess;
+            if (_driver != null)
+            {
+                var result = await _driver.WriteBoolAsync(address, value);
+                return result.IsSuccess;
+            }
+            if (_plc == null) return false;
+            var plcResult = await _plc.WriteAsync(address, value);
+            return plcResult.IsSuccess;
         }
         catch (Exception ex)
         {
@@ -206,7 +260,7 @@ public static class PlcService
     /// </summary>
     public static async Task<byte[]?> ReadBytesAsync(string address, ushort length)
     {
-        if (_plc == null || !_isConnected)
+        if (!_isConnected)
         {
             Log.Warning("PLC 未连接,无法读取数据");
             return null;
@@ -214,8 +268,14 @@ public static class PlcService
 
         try
         {
-            var result = await _plc.ReadAsync(address, length);
-            return result.IsSuccess ? result.Content : null;
+            if (_driver != null)
+            {
+                var result = await _driver.ReadBytesAsync(address, length);
+                return result.IsSuccess ? result.Content : null;
+            }
+            if (_plc == null) return null;
+            var plcResult = await _plc.ReadAsync(address, length);
+            return plcResult.IsSuccess ? plcResult.Content : null;
         }
         catch (Exception ex)
         {
@@ -254,6 +314,9 @@ public static class PlcService
     public static void Dispose()
     {
         Disconnect();
+        _driver?.Dispose();
+        _driver = null;
+        _plc?.Dispose();
         _plc = null;
     }
 }

+ 24 - 0
src/YZWater.Core/ViewModels/ViewBViewModel.cs

@@ -186,6 +186,9 @@ public partial class ViewBViewModel : TranslatableViewModelBase
             return;
         }
 
+        var oldProject = config.ActiveProject;
+        var oldProtocol = config.PlcProtocol;
+
         config.PlcIp = PlcIp;
         config.PlcPort = PlcPort;
         config.AutoConnect = AutoConnect;
@@ -198,6 +201,27 @@ public partial class ViewBViewModel : TranslatableViewModelBase
 
         ConfigService.SaveConfig();
         Log.Information("配置已保存");
+
+        // 检测项目档案是否变化,触发热切换
+        if (oldProject != ActiveProject)
+        {
+            var success = ProjectService.SwitchProject(ActiveProject ?? "");
+            if (success)
+            {
+                CurrentProjectName = ProjectService.Current?.Name ?? "默认项目";
+                Log.Information("项目已热切换: {Project}", string.IsNullOrEmpty(ActiveProject) ? "默认" : ActiveProject);
+            }
+            else
+            {
+                Log.Warning("项目热切换失败: {Project}", ActiveProject);
+            }
+        }
+
+        // 检测协议变化,记录日志(需要重连 PLC 生效)
+        if (oldProtocol != PlcProtocol)
+        {
+            Log.Information("PLC 协议已更改: {Old} → {New},需要重新连接 PLC", oldProtocol, PlcProtocol);
+        }
     }
 
     /// <summary>

+ 101 - 5
tests/YZWater.Core.Tests/Services/AutoControlIntegrationTests.cs

@@ -37,19 +37,51 @@ public class AutoControlIntegrationTests
         service.IsEnabled = false;
         Assert.False(service.IsEnabled);
 
+        service.IsEnabled = true;
+        Assert.True(service.IsEnabled);
+
         service.IsEnabled = original;
     }
 
     [Fact]
-    public void MockPlcService_ShouldBeToggleable()
+    public void AutoControlService_CanTogglePumpRotation()
+    {
+        var service = AutoControlService.Instance;
+        var original = service.EnablePumpRotation;
+
+        service.EnablePumpRotation = false;
+        Assert.False(service.EnablePumpRotation);
+
+        service.EnablePumpRotation = true;
+        Assert.True(service.EnablePumpRotation);
+
+        service.EnablePumpRotation = original;
+    }
+
+    [Fact]
+    public void AutoControlService_CanModifyDelays()
     {
-        var wasEnabled = MockPlcService.IsEnabled;
+        var service = AutoControlService.Instance;
+
+        service.StartDelaySeconds = 5;
+        service.StopDelaySeconds = 10;
+        service.MinIntervalSeconds = 120;
 
+        Assert.Equal(5, service.StartDelaySeconds);
+        Assert.Equal(10, service.StopDelaySeconds);
+        Assert.Equal(120, service.MinIntervalSeconds);
+
+        // Reset
+        service.StartDelaySeconds = 3;
+        service.StopDelaySeconds = 5;
+        service.MinIntervalSeconds = 60;
+    }
+
+    [Fact]
+    public void MockPlcService_ShouldBeToggleable()
+    {
         MockPlcService.Enable();
         Assert.True(MockPlcService.IsEnabled);
-
-        // Note: MockPlcService 没有 Disable 方法,只能 Enable
-        // Restore original state if needed
     }
 
     [Fact]
@@ -63,4 +95,68 @@ public class AutoControlIntegrationTests
         // Mock 模式应生成模拟数据
         Assert.True(data.IsDataValid || data.LastReadTime > DateTime.MinValue);
     }
+
+    [Fact]
+    public void ProjectProfile_CanCreatePumpConfig()
+    {
+        var pump = new PumpConfig
+        {
+            Name = "TestPump",
+            RunAddress = "Q0.1",
+            FaultAddress = "I0.1",
+            CurrentAddress = "VD50",
+            StartAddress = "M1.3",
+            StopAddress = "M1.4"
+        };
+
+        Assert.Equal("TestPump", pump.Name);
+        Assert.Equal("Q0.1", pump.RunAddress);
+        Assert.Equal("I0.1", pump.FaultAddress);
+    }
+
+    [Fact]
+    public void ProjectProfile_CanCreateTankConfig()
+    {
+        var tank = new TankConfig
+        {
+            Name = "TestTank",
+            LevelAddress = "VD62",
+            StartPump1LevelAddress = "VD500",
+            StopLevelAddress = "VD528",
+            ProtectionLevelAddress = "VD962"
+        };
+
+        Assert.Equal("TestTank", tank.Name);
+        Assert.Equal("VD62", tank.LevelAddress);
+    }
+
+    [Fact]
+    public void ProjectProfile_CanCreateFullProfile()
+    {
+        var profile = new ProjectProfile
+        {
+            Name = "测试项目",
+            Description = "测试用项目档案",
+            Pumps = new()
+            {
+                new PumpConfig { Name = "P1", RunAddress = "Q0.1", FaultAddress = "I0.1" },
+                new PumpConfig { Name = "P2", RunAddress = "Q0.2", FaultAddress = "I0.2" }
+            },
+            Tanks = new()
+            {
+                new TankConfig { Name = "T1", LevelAddress = "VD62" }
+            },
+            AlarmThresholds = new AlarmThresholds
+            {
+                PressureHigh = 1.2f,
+                PressureLow = 0.1f
+            }
+        };
+
+        Assert.Equal("测试项目", profile.Name);
+        Assert.Equal(2, profile.Pumps.Count);
+        Assert.Single(profile.Tanks);
+        Assert.NotNull(profile.AlarmThresholds);
+        Assert.Equal(1.2f, profile.AlarmThresholds.PressureHigh);
+    }
 }