更新202659

This commit is contained in:
GukSang.Jin
2026-05-09 15:16:26 +08:00
parent 0ea7f83cc2
commit 6af85741a2
9 changed files with 513 additions and 193 deletions

View File

@@ -14,6 +14,85 @@
<Window.Resources> <Window.Resources>
<converters:TrendPointCollectionConverter x:Key="TrendPointCollectionConverter" /> <converters:TrendPointCollectionConverter x:Key="TrendPointCollectionConverter" />
<converters:TrendLastPointCoordinateConverter x:Key="TrendLastPointCoordinateConverter" /> <converters:TrendLastPointCoordinateConverter x:Key="TrendLastPointCoordinateConverter" />
<DataTemplate x:Key="ProjectRs485PumpCardTemplate" DataType="{x:Type models:PumpControlChannel}">
<Grid Margin="0,0,10,6"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="112" />
<ColumnDefinition Width="6" />
<ColumnDefinition Width="82" />
<ColumnDefinition Width="44" />
<ColumnDefinition Width="6" />
<ColumnDefinition Width="54" />
<ColumnDefinition Width="6" />
<ColumnDefinition Width="54" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="52" />
<ColumnDefinition Width="6" />
<ColumnDefinition Width="96" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
FontSize="13"
FontWeight="Bold"
Foreground="{StaticResource HeaderBrush}"
Text="{Binding Name}"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding Name}" />
<TextBox Grid.Column="2"
Height="32"
VerticalContentAlignment="Center"
FontSize="14"
FontWeight="SemiBold"
Text="{Binding PendingSetpointText, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="True"
Cursor="Hand"
ToolTip="目标流量 L/min"
PreviewMouseLeftButtonDown="PumpSetpointTextBox_OnPreviewMouseLeftButtonDown"
GotKeyboardFocus="PumpSetpointTextBox_OnGotKeyboardFocus" />
<TextBlock Grid.Column="3"
Margin="4,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionStyle}"
Text="L/min" />
<Button Grid.Column="5"
Height="32"
MinWidth="0"
Padding="6,2"
Command="{Binding DataContext.StartSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Content="启动"
IsEnabled="{Binding CanStartRs485Action}"
Background="#FF2B8F6A"
ToolTipService.ShowOnDisabled="True"
ToolTip="{Binding StartActionHint}" />
<Button Grid.Column="7"
Height="32"
MinWidth="0"
Padding="6,2"
Command="{Binding DataContext.StopSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Content="停止"
IsEnabled="{Binding CanStopRs485Action}"
Background="#FFB85C38"
ToolTipService.ShowOnDisabled="True"
ToolTip="{Binding StopActionHint}" />
<CheckBox Grid.Column="9"
VerticalAlignment="Center"
Content="稳流"
IsChecked="{Binding IsFlowStabilizationEnabled, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding CanUseFlowStabilization}"
ToolTip="{Binding FlowStabilizationToggleHint}" />
<TextBlock Grid.Column="11"
VerticalAlignment="Center"
FontSize="11"
Foreground="{StaticResource MutedTextBrush}"
Text="{Binding FlowStabilizationStateText}"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding FlowStabilizationStateText}" />
</Grid>
</DataTemplate>
</Window.Resources> </Window.Resources>
<Grid Margin="12"> <Grid Margin="12">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -202,6 +281,16 @@
Cursor="Hand" Cursor="Hand"
PreviewMouseLeftButtonDown="PumpSetpointTextBox_OnPreviewMouseLeftButtonDown" PreviewMouseLeftButtonDown="PumpSetpointTextBox_OnPreviewMouseLeftButtonDown"
GotKeyboardFocus="PumpSetpointTextBox_OnGotKeyboardFocus" /> GotKeyboardFocus="PumpSetpointTextBox_OnGotKeyboardFocus" />
<CheckBox Margin="0,8,0,0"
Content="稳流"
IsChecked="{Binding IsFlowStabilizationEnabled, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding CanUseFlowStabilization}"
ToolTip="{Binding FlowStabilizationToggleHint}" />
<TextBlock Margin="0,4,0,0"
FontSize="11"
Foreground="{StaticResource MutedTextBrush}"
Text="{Binding FlowStabilizationStateText}"
TextWrapping="Wrap" />
<Border Margin="0,10,0,0" <Border Margin="0,10,0,0"
Padding="10,6" Padding="10,6"
Background="{Binding SetpointStatusBackground}" Background="{Binding SetpointStatusBackground}"
@@ -939,6 +1028,105 @@
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<TabItem x:Name="TightnessTestTab" Header="密合性">
<ScrollViewer Margin="0,6,0,0" VerticalScrollBarVisibility="Auto" CanContentScroll="False">
<StackPanel>
<Border Style="{StaticResource CardBorderStyle}" Padding="14">
<ItemsControl ItemsSource="{Binding PressureDropRs485FlowPumpControls}"
ItemTemplate="{StaticResource ProjectRs485PumpCardTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Border>
<Border Style="{StaticResource CardBorderStyle}" Padding="14">
<StackPanel IsEnabled="{Binding HasTightnessInspectionItem}">
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Style="{StaticResource SectionTitleStyle}"
Text="{Binding SelectedItemTitle}"
TextTrimming="CharacterEllipsis" />
<Button Grid.Column="2"
MinWidth="112"
MinHeight="32"
Padding="12,5"
Click="OpenProjectInfoDialogButton_OnClick"
Background="#FFE3F6EF"
Foreground="{StaticResource HeaderBrush}"
BorderBrush="#FF9CCBBF"
Content="查看项目说明" />
</Grid>
<TextBlock Style="{StaticResource CaptionStyle}" Text="结果记录" />
<TextBox Margin="0,4,0,0"
Text="{Binding ResultValue, UpdateSourceTrigger=PropertyChanged}"
MinHeight="118"
AcceptsReturn="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
IsReadOnly="{Binding SelectedItemUsesRealtimeValue}" />
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource CaptionStyle}" Text="判定" />
<ComboBox Grid.Row="1"
ItemsSource="{Binding ResultStatusOptions}"
SelectedItem="{Binding SelectedResultStatusText, Mode=TwoWay}" />
<TextBlock Grid.Column="2" Style="{StaticResource CaptionStyle}" Text="记录人" />
<TextBox Grid.Row="1" Grid.Column="2" Text="{Binding ResultOperator, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="2" Style="{StaticResource CaptionStyle}" Text="复核人" />
<TextBox Grid.Row="3" Text="{Binding ReviewerName, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="2" Grid.Column="2" Style="{StaticResource CaptionStyle}" Text="批准人" />
<TextBox Grid.Row="3" Grid.Column="2" Text="{Binding ApproverName, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
<TextBlock Margin="0,10,0,0" Style="{StaticResource CaptionStyle}" Text="复核备注" />
<TextBox Margin="0,4,0,0"
Text="{Binding ResultNote, UpdateSourceTrigger=PropertyChanged}"
MinHeight="96"
AcceptsReturn="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
IsReadOnly="{Binding SelectedItemUsesRealtimeValue}" />
<DockPanel Margin="0,12,0,0" LastChildFill="True">
<Button DockPanel.Dock="Right"
MinWidth="112"
Height="34"
Padding="12,4"
Command="{Binding ApplyResultCommand}"
Content="保存结果"
Background="#FF4D8C72"
IsEnabled="{Binding CanModifySession}" />
<TextBlock VerticalAlignment="Center"
Style="{StaticResource CaptionStyle}"
Text="{Binding LatestAction}"
TextWrapping="Wrap" />
</DockPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem x:Name="ManualSupplementTab" Header="项目补充"> <TabItem x:Name="ManualSupplementTab" Header="项目补充">
<ScrollViewer Margin="0,6,0,0" VerticalScrollBarVisibility="Auto" CanContentScroll="False"> <ScrollViewer Margin="0,6,0,0" VerticalScrollBarVisibility="Auto" CanContentScroll="False">
<StackPanel> <StackPanel>
@@ -1070,34 +1258,8 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</Border.Style> </Border.Style>
<Border.Resources>
<DataTemplate x:Key="KinkResistanceRs485PumpCardTemplate" DataType="{x:Type models:PumpControlChannel}">
<Border MinWidth="640"
Margin="0,0,10,10"
Padding="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="116" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontSize="14" FontWeight="Bold" Text="{Binding Name}" TextWrapping="Wrap" />
<TextBox Grid.Column="1" Width="108" Height="32" Margin="8,0,0,0" VerticalContentAlignment="Center" Text="{Binding PendingSetpointText, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True" Cursor="Hand" PreviewMouseLeftButtonDown="PumpSetpointTextBox_OnPreviewMouseLeftButtonDown" GotKeyboardFocus="PumpSetpointTextBox_OnGotKeyboardFocus" />
<Button Grid.Column="2" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StartSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="启动" IsEnabled="{Binding CanStartRs485Action}" Background="#FF2B8F6A" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StartActionHint}" />
<Button Grid.Column="3" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StopSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="停止" IsEnabled="{Binding CanStopRs485Action}" Background="#FFB85C38" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StopActionHint}" />
<TextBlock Grid.Column="4" Margin="10,0,0,0" VerticalAlignment="Center" FontSize="12" FontWeight="Bold" Foreground="{Binding SetpointStatusForeground}" Text="{Binding Rs485RunStateText}" TextWrapping="Wrap" />
</Grid>
</Border>
</DataTemplate>
</Border.Resources>
<StackPanel> <StackPanel>
<Grid Margin="0,8,0,0"> <Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="12" /> <ColumnDefinition Width="12" />
@@ -1105,7 +1267,7 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ItemsControl Grid.Column="0" <ItemsControl Grid.Column="0"
ItemsSource="{Binding KinkResistanceRs485FlowPumpControls}" ItemsSource="{Binding KinkResistanceRs485FlowPumpControls}"
ItemTemplate="{StaticResource KinkResistanceRs485PumpCardTemplate}"> ItemTemplate="{StaticResource ProjectRs485PumpCardTemplate}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel /> <WrapPanel />
@@ -1133,30 +1295,6 @@
</Style> </Style>
</Border.Style> </Border.Style>
<Border.Resources> <Border.Resources>
<DataTemplate x:Key="PressureDropRs485QuickPumpCardTemplate" DataType="{x:Type models:PumpControlChannel}">
<Border MinWidth="640"
Margin="0,0,10,10"
Padding="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="116" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontSize="14" FontWeight="Bold" Text="{Binding Name}" TextWrapping="Wrap" />
<TextBox Grid.Column="1" Width="108" Height="32" Margin="8,0,0,0" VerticalContentAlignment="Center" Text="{Binding PendingSetpointText, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True" Cursor="Hand" PreviewMouseLeftButtonDown="PumpSetpointTextBox_OnPreviewMouseLeftButtonDown" GotKeyboardFocus="PumpSetpointTextBox_OnGotKeyboardFocus" />
<Button Grid.Column="2" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StartSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="启动" IsEnabled="{Binding CanStartRs485Action}" Background="#FF2B8F6A" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StartActionHint}" />
<Button Grid.Column="3" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StopSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="停止" IsEnabled="{Binding CanStopRs485Action}" Background="#FFB85C38" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StopActionHint}" />
<TextBlock Grid.Column="4" Margin="10,0,0,0" VerticalAlignment="Center" FontSize="12" FontWeight="Bold" Foreground="{Binding SetpointStatusForeground}" Text="{Binding Rs485RunStateText}" TextWrapping="Wrap" />
</Grid>
</Border>
</DataTemplate>
<DataTemplate x:Key="PressureDropValveControlCardTemplate" DataType="{x:Type models:ValveControlChannel}"> <DataTemplate x:Key="PressureDropValveControlCardTemplate" DataType="{x:Type models:ValveControlChannel}">
<Button MinWidth="132" <Button MinWidth="132"
Height="34" Height="34"
@@ -1170,7 +1308,7 @@
ToolTip="{Binding StateText}" /> ToolTip="{Binding StateText}" />
</DataTemplate> </DataTemplate>
</Border.Resources> </Border.Resources>
<Grid Margin="0,8,0,0"> <Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="12" /> <ColumnDefinition Width="12" />
@@ -1178,7 +1316,7 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ItemsControl Grid.Column="0" <ItemsControl Grid.Column="0"
ItemsSource="{Binding PressureDropRs485FlowPumpControls}" ItemsSource="{Binding PressureDropRs485FlowPumpControls}"
ItemTemplate="{StaticResource PressureDropRs485QuickPumpCardTemplate}"> ItemTemplate="{StaticResource ProjectRs485PumpCardTemplate}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel /> <WrapPanel />
@@ -1224,70 +1362,38 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</Border.Style> </Border.Style>
<Border.Resources> <Grid Margin="0,4,0,0">
<DataTemplate x:Key="RecirculationRs485PumpCardTemplate" DataType="{x:Type models:PumpControlChannel}"> <Grid.ColumnDefinitions>
<Border MinWidth="640" <ColumnDefinition Width="*" />
Margin="0,0,10,10" <ColumnDefinition Width="12" />
Padding="0"> <ColumnDefinition Width="Auto" />
<Grid> </Grid.ColumnDefinitions>
<Grid.ColumnDefinitions> <ItemsControl Grid.Column="0"
<ColumnDefinition Width="150" /> ItemsSource="{Binding RecirculationRs485FlowPumpControls}"
<ColumnDefinition Width="116" /> ItemTemplate="{StaticResource ProjectRs485PumpCardTemplate}">
<ColumnDefinition Width="Auto" /> <ItemsControl.ItemsPanel>
<ColumnDefinition Width="Auto" /> <ItemsPanelTemplate>
<ColumnDefinition Width="*" /> <WrapPanel />
</Grid.ColumnDefinitions> </ItemsPanelTemplate>
<Grid.RowDefinitions> </ItemsControl.ItemsPanel>
<RowDefinition Height="Auto" /> </ItemsControl>
<RowDefinition Height="Auto" /> <WrapPanel Grid.Column="2" VerticalAlignment="Top">
</Grid.RowDefinitions> <Button MinWidth="112"
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontSize="14" FontWeight="Bold" Text="{Binding Name}" TextWrapping="Wrap" /> Height="34"
<TextBox Grid.Column="1" Width="108" Height="32" Margin="8,0,0,0" VerticalContentAlignment="Center" Text="{Binding PendingSetpointText, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True" Cursor="Hand" PreviewMouseLeftButtonDown="PumpSetpointTextBox_OnPreviewMouseLeftButtonDown" GotKeyboardFocus="PumpSetpointTextBox_OnGotKeyboardFocus" /> Padding="10,4"
<Button Grid.Column="2" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StartSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="启动" IsEnabled="{Binding CanStartRs485Action}" Background="#FF2B8F6A" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StartActionHint}" /> Margin="0,0,8,6"
<Button Grid.Column="3" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StopSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="停止" IsEnabled="{Binding CanStopRs485Action}" Background="#FFB85C38" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StopActionHint}" /> Command="{Binding StartRecirculationRs485PumpsCommand}"
<TextBlock Grid.Column="4" Margin="10,0,0,0" VerticalAlignment="Center" FontSize="12" FontWeight="Bold" Foreground="{Binding SetpointStatusForeground}" Text="{Binding Rs485RunStateText}" TextWrapping="Wrap" /> Content="统一启动"
</Grid> Background="#FF2B8F6A" />
</Border> <Button MinWidth="112"
</DataTemplate> Height="34"
</Border.Resources> Padding="10,4"
<StackPanel> Margin="0,0,0,6"
<DockPanel LastChildFill="False"> Command="{Binding StopRecirculationRs485PumpsCommand}"
<StackPanel DockPanel.Dock="Left"> Content="统一停止"
</StackPanel> Background="#FFB85C38" />
<WrapPanel DockPanel.Dock="Right" Margin="12,0,0,0"> </WrapPanel>
<Button MinWidth="120" </Grid>
Height="34"
Padding="12,4"
Margin="0,0,8,8"
Command="{Binding StartRecirculationRs485PumpsCommand}"
Content="统一启动三泵"
Background="#FF2B8F6A" />
<Button MinWidth="120"
Height="34"
Padding="12,4"
Margin="0,0,8,8"
Command="{Binding StopRecirculationRs485PumpsCommand}"
Content="统一停止三泵"
Background="#FFB85C38" />
</WrapPanel>
</DockPanel>
<Grid Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ItemsControl Grid.Column="0"
ItemsSource="{Binding RecirculationRs485FlowPumpControls}"
ItemTemplate="{StaticResource RecirculationRs485PumpCardTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</StackPanel>
</Border> </Border>
<Border> <Border>
<Border.Style> <Border.Style>
@@ -1312,30 +1418,6 @@
</Style> </Style>
</Border.Style> </Border.Style>
<Border.Resources> <Border.Resources>
<DataTemplate x:Key="AntiCollapseRs485QuickPumpCardTemplate" DataType="{x:Type models:PumpControlChannel}">
<Border MinWidth="640"
Margin="0,0,10,10"
Padding="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="116" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontSize="14" FontWeight="Bold" Text="{Binding Name}" TextWrapping="Wrap" />
<TextBox Grid.Column="1" Width="108" Height="32" Margin="8,0,0,0" VerticalContentAlignment="Center" Text="{Binding PendingSetpointText, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True" Cursor="Hand" PreviewMouseLeftButtonDown="PumpSetpointTextBox_OnPreviewMouseLeftButtonDown" GotKeyboardFocus="PumpSetpointTextBox_OnGotKeyboardFocus" />
<Button Grid.Column="2" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StartSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="启动" IsEnabled="{Binding CanStartRs485Action}" Background="#FF2B8F6A" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StartActionHint}" />
<Button Grid.Column="3" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StopSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="停止" IsEnabled="{Binding CanStopRs485Action}" Background="#FFB85C38" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StopActionHint}" />
<TextBlock Grid.Column="4" Margin="10,0,0,0" VerticalAlignment="Center" FontSize="12" FontWeight="Bold" Foreground="{Binding SetpointStatusForeground}" Text="{Binding Rs485RunStateText}" TextWrapping="Wrap" />
</Grid>
</Border>
</DataTemplate>
<DataTemplate x:Key="AntiCollapsePumpControlCardTemplate" DataType="{x:Type models:PumpControlChannel}"> <DataTemplate x:Key="AntiCollapsePumpControlCardTemplate" DataType="{x:Type models:PumpControlChannel}">
<Button MinWidth="132" <Button MinWidth="132"
Height="34" Height="34"
@@ -1362,9 +1444,9 @@
</DataTemplate> </DataTemplate>
</Border.Resources> </Border.Resources>
<StackPanel> <StackPanel>
<WrapPanel Margin="0,8,0,0"> <WrapPanel Margin="0,4,0,0">
<ItemsControl ItemsSource="{Binding PressureDropRs485FlowPumpControls}" <ItemsControl ItemsSource="{Binding PressureDropRs485FlowPumpControls}"
ItemTemplate="{StaticResource AntiCollapseRs485QuickPumpCardTemplate}"> ItemTemplate="{StaticResource ProjectRs485PumpCardTemplate}">
<ItemsControl.Style> <ItemsControl.Style>
<Style TargetType="ItemsControl"> <Style TargetType="ItemsControl">
<Setter Property="Visibility" Value="Visible" /> <Setter Property="Visibility" Value="Visible" />
@@ -1377,7 +1459,7 @@
</ItemsControl.Style> </ItemsControl.Style>
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" /> <WrapPanel />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
</ItemsControl> </ItemsControl>
@@ -1437,34 +1519,8 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</Border.Style> </Border.Style>
<Border.Resources>
<DataTemplate x:Key="HemolysisRs485PumpCardTemplate" DataType="{x:Type models:PumpControlChannel}">
<Border MinWidth="640"
Margin="0,0,10,10"
Padding="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="116" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontSize="14" FontWeight="Bold" Text="{Binding Name}" TextWrapping="Wrap" />
<TextBox Grid.Column="1" Width="108" Height="32" Margin="8,0,0,0" VerticalContentAlignment="Center" Text="{Binding PendingSetpointText, UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True" Cursor="Hand" PreviewMouseLeftButtonDown="PumpSetpointTextBox_OnPreviewMouseLeftButtonDown" GotKeyboardFocus="PumpSetpointTextBox_OnGotKeyboardFocus" />
<Button Grid.Column="2" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StartSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="启动" IsEnabled="{Binding CanStartRs485Action}" Background="#FF2B8F6A" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StartActionHint}" />
<Button Grid.Column="3" MinWidth="66" Height="32" Margin="8,0,0,0" Padding="8,2" Command="{Binding DataContext.StopSingleRs485PumpCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Content="停止" IsEnabled="{Binding CanStopRs485Action}" Background="#FFB85C38" ToolTipService.ShowOnDisabled="True" ToolTip="{Binding StopActionHint}" />
<TextBlock Grid.Column="4" Margin="10,0,0,0" VerticalAlignment="Center" FontSize="12" FontWeight="Bold" Foreground="{Binding SetpointStatusForeground}" Text="{Binding Rs485RunStateText}" TextWrapping="Wrap" />
</Grid>
</Border>
</DataTemplate>
</Border.Resources>
<StackPanel> <StackPanel>
<Grid Margin="0,8,0,0"> <Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="12" /> <ColumnDefinition Width="12" />
@@ -1472,7 +1528,7 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ItemsControl Grid.Column="0" <ItemsControl Grid.Column="0"
ItemsSource="{Binding HemolysisRs485FlowPumpControls}" ItemsSource="{Binding HemolysisRs485FlowPumpControls}"
ItemTemplate="{StaticResource HemolysisRs485PumpCardTemplate}"> ItemTemplate="{StaticResource ProjectRs485PumpCardTemplate}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel /> <WrapPanel />
@@ -1495,18 +1551,27 @@
</Style> </Style>
</Border.Style> </Border.Style>
<StackPanel> <StackPanel>
<DockPanel LastChildFill="False"> <Grid Margin="0,0,0,8">
<Button DockPanel.Dock="Right" <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Style="{StaticResource SectionTitleStyle}"
Text="{Binding SelectedItemTitle}"
TextTrimming="CharacterEllipsis" />
<Button Grid.Column="2"
MinWidth="112" MinWidth="112"
MinHeight="32" MinHeight="32"
Padding="12,5" Padding="12,5"
Margin="12,0,0,8"
Click="OpenProjectInfoDialogButton_OnClick" Click="OpenProjectInfoDialogButton_OnClick"
Background="#FFE3F6EF" Background="#FFE3F6EF"
Foreground="{StaticResource HeaderBrush}" Foreground="{StaticResource HeaderBrush}"
BorderBrush="#FF9CCBBF" BorderBrush="#FF9CCBBF"
Content="查看项目说明" /> Content="查看项目说明" />
</DockPanel> </Grid>
<Grid Margin="0,2,0,0"> <Grid Margin="0,2,0,0">
<StackPanel Margin="0,0,0,4" IsEnabled="{Binding CanModifySession}"> <StackPanel Margin="0,0,0,4" IsEnabled="{Binding CanModifySession}">
<Border Margin="0,0,0,16" Padding="0"> <Border Margin="0,0,0,16" Padding="0">
@@ -1983,13 +2048,21 @@
</Border> </Border>
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid Margin="0,8,0,0"> <Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="12" /> <ColumnDefinition Width="12" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center" Style="{StaticResource CaptionStyle}" Text="{Binding LatestAction}" TextWrapping="Wrap" /> <TextBlock Grid.Column="0" VerticalAlignment="Center" Style="{StaticResource CaptionStyle}" Text="{Binding LatestAction}" TextWrapping="Wrap" />
<Button Grid.Column="2"
MinWidth="112"
Height="34"
Padding="12,4"
Command="{Binding ApplyResultCommand}"
Content="保存结果"
Background="#FF4D8C72"
IsEnabled="{Binding CanModifySession}" />
</Grid> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>

View File

@@ -21,6 +21,7 @@ public partial class MainWindow : Window
private readonly List<Button> _projectInspectionButtons = []; private readonly List<Button> _projectInspectionButtons = [];
private object? _projectInspectionContent; private object? _projectInspectionContent;
private object? _realtimeContent; private object? _realtimeContent;
private object? _tightnessTestContent;
private object? _manualSupplementContent; private object? _manualSupplementContent;
private object? _configurationContent; private object? _configurationContent;
private object? _traceContent; private object? _traceContent;
@@ -85,6 +86,7 @@ public partial class MainWindow : Window
{ {
_realtimeContent = ExtractTabContent(RealtimeDataTab); _realtimeContent = ExtractTabContent(RealtimeDataTab);
_projectInspectionContent = ExtractTabContent(ProjectListTab); _projectInspectionContent = ExtractTabContent(ProjectListTab);
_tightnessTestContent = ExtractTabContent(TightnessTestTab);
_manualSupplementContent = ExtractTabContent(ManualSupplementTab); _manualSupplementContent = ExtractTabContent(ManualSupplementTab);
_configurationContent = ExtractTabContent(ConfigurationTab); _configurationContent = ExtractTabContent(ConfigurationTab);
_traceContent = ExtractTabContent(TraceTab); _traceContent = ExtractTabContent(TraceTab);
@@ -126,6 +128,7 @@ public partial class MainWindow : Window
_projectInspectionButtons.Add(button); _projectInspectionButtons.Add(button);
} }
AddNavigationButton("密合性", new NavigationTarget(NavigationPage.TightnessTest, null));
AddNavigationButton("项目补充", new NavigationTarget(NavigationPage.ManualSupplement, null)); AddNavigationButton("项目补充", new NavigationTarget(NavigationPage.ManualSupplement, null));
AddNavigationButton("配置", new NavigationTarget(NavigationPage.Configuration, null)); AddNavigationButton("配置", new NavigationTarget(NavigationPage.Configuration, null));
AddNavigationButton("追溯", new NavigationTarget(NavigationPage.Trace, null)); AddNavigationButton("追溯", new NavigationTarget(NavigationPage.Trace, null));
@@ -188,9 +191,19 @@ public partial class MainWindow : Window
{ {
_currentPage = page; _currentPage = page;
_currentProjectItem = null; _currentProjectItem = null;
if (page == NavigationPage.TightnessTest
&& DataContext is MainViewModel viewModel
&& viewModel.TightnessInspectionItem is not null
&& !ReferenceEquals(viewModel.SelectedItem, viewModel.TightnessInspectionItem))
{
viewModel.SelectedItem = viewModel.TightnessInspectionItem;
}
MainContentHost.Content = page switch MainContentHost.Content = page switch
{ {
NavigationPage.RealtimeData => _realtimeContent, NavigationPage.RealtimeData => _realtimeContent,
NavigationPage.TightnessTest => _tightnessTestContent,
NavigationPage.ManualSupplement => _manualSupplementContent, NavigationPage.ManualSupplement => _manualSupplementContent,
NavigationPage.Configuration => _configurationContent, NavigationPage.Configuration => _configurationContent,
NavigationPage.Trace => _traceContent, NavigationPage.Trace => _traceContent,
@@ -254,6 +267,7 @@ public partial class MainWindow : Window
{ {
RealtimeData, RealtimeData,
ProjectItem, ProjectItem,
TightnessTest,
ManualSupplement, ManualSupplement,
Configuration, Configuration,
Trace Trace

View File

@@ -109,6 +109,18 @@ public partial class PumpControlChannel : ObservableObject
[ObservableProperty] [ObservableProperty]
private bool isBatchSelected; private bool isBatchSelected;
[ObservableProperty]
private bool isFlowStabilizationEnabled;
[ObservableProperty]
private DateTime lastFlowStabilizationAdjustmentUtc = DateTime.MinValue;
[ObservableProperty]
private int flowStabilizationRawSetpoint;
[ObservableProperty]
private string flowStabilizationStatusText = "稳流未启用";
public string StartAddressDisplay => $"M{StartAddress}"; public string StartAddressDisplay => $"M{StartAddress}";
public string FlowAddressDisplay => FlowAddress.HasValue ? $"D{FlowAddress.Value}" : "-"; public string FlowAddressDisplay => FlowAddress.HasValue ? $"D{FlowAddress.Value}" : "-";
public bool HasFlowTelemetry => FlowAddress.HasValue; public bool HasFlowTelemetry => FlowAddress.HasValue;
@@ -224,6 +236,15 @@ public partial class PumpControlChannel : ObservableObject
: "未配置流量换算系数"; : "未配置流量换算系数";
public string Rs485ReadActionText => IsRs485Busy ? "处理中" : "读取"; public string Rs485ReadActionText => IsRs485Busy ? "处理中" : "读取";
public string Rs485WriteActionText => IsRs485Busy ? "处理中" : "写入"; public string Rs485WriteActionText => IsRs485Busy ? "处理中" : "写入";
public bool CanUseFlowStabilization => SupportsRs485DirectControl && Key != "KinkResistancePump";
public string FlowStabilizationToggleHint => CanUseFlowStabilization
? "按实际流量小步调节泵速"
: "抗扭结采样要求保持固定转速";
public string FlowStabilizationStateText => !CanUseFlowStabilization
? "固定转速"
: IsFlowStabilizationEnabled
? FlowStabilizationStatusText
: "稳流未启用";
public string SetpointStatusForeground => ResolveSetpointStatusForeground(); public string SetpointStatusForeground => ResolveSetpointStatusForeground();
public string SetpointStatusBackground => ResolveSetpointStatusBackground(); public string SetpointStatusBackground => ResolveSetpointStatusBackground();
@@ -241,6 +262,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(CanToggleRs485Action)); OnPropertyChanged(nameof(CanToggleRs485Action));
OnPropertyChanged(nameof(ToggleActionHint)); OnPropertyChanged(nameof(ToggleActionHint));
OnPropertyChanged(nameof(CardPrimaryDisplay)); OnPropertyChanged(nameof(CardPrimaryDisplay));
OnPropertyChanged(nameof(FlowStabilizationStateText));
} }
partial void OnFlowValueChanged(double value) partial void OnFlowValueChanged(double value)
@@ -251,6 +273,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateHint)); OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(IndicatorColor)); OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(CardPrimaryDisplay)); OnPropertyChanged(nameof(CardPrimaryDisplay));
OnPropertyChanged(nameof(FlowStabilizationStateText));
} }
partial void OnStateAvailableChanged(bool value) partial void OnStateAvailableChanged(bool value)
@@ -260,6 +283,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(IndicatorColor)); OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(ToggleButtonText)); OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CardPrimaryDisplay)); OnPropertyChanged(nameof(CardPrimaryDisplay));
OnPropertyChanged(nameof(FlowStabilizationStateText));
} }
partial void OnFlowAvailableChanged(bool value) partial void OnFlowAvailableChanged(bool value)
@@ -270,6 +294,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateHint)); OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(IndicatorColor)); OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(CardPrimaryDisplay)); OnPropertyChanged(nameof(CardPrimaryDisplay));
OnPropertyChanged(nameof(FlowStabilizationStateText));
} }
partial void OnRs485EnabledChanged(bool value) partial void OnRs485EnabledChanged(bool value)
@@ -280,11 +305,33 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(CalibrationStatusText)); OnPropertyChanged(nameof(CalibrationStatusText));
OnPropertyChanged(nameof(SetpointReadbackDisplay)); OnPropertyChanged(nameof(SetpointReadbackDisplay));
OnPropertyChanged(nameof(RawSetpointDisplay)); OnPropertyChanged(nameof(RawSetpointDisplay));
OnPropertyChanged(nameof(CanUseFlowStabilization));
OnPropertyChanged(nameof(FlowStabilizationToggleHint));
OnPropertyChanged(nameof(FlowStabilizationStateText));
} }
partial void OnRs485SlaveAddressChanged(byte value) => OnPropertyChanged(nameof(Rs485SlaveAddressDisplay)); partial void OnRs485SlaveAddressChanged(byte value) => OnPropertyChanged(nameof(Rs485SlaveAddressDisplay));
partial void OnRs485MotorControlRegisterChanged(ushort value) => OnPropertyChanged(nameof(SupportsRs485DirectControl)); partial void OnRs485MotorControlRegisterChanged(ushort value)
{
OnPropertyChanged(nameof(SupportsRs485DirectControl));
OnPropertyChanged(nameof(CanUseFlowStabilization));
OnPropertyChanged(nameof(FlowStabilizationToggleHint));
OnPropertyChanged(nameof(FlowStabilizationStateText));
}
partial void OnIsFlowStabilizationEnabledChanged(bool value)
{
if (!value)
{
FlowStabilizationStatusText = "稳流未启用";
}
OnPropertyChanged(nameof(FlowStabilizationStateText));
}
partial void OnFlowStabilizationStatusTextChanged(string value) =>
OnPropertyChanged(nameof(FlowStabilizationStateText));
partial void OnRs485RawPerLitrePerMinuteChanged(double value) partial void OnRs485RawPerLitrePerMinuteChanged(double value)
{ {
@@ -327,6 +374,7 @@ public partial class PumpControlChannel : ObservableObject
ConfirmedSetpointAvailable = false; ConfirmedSetpointAvailable = false;
ConfirmedRawSetpointValue = 0; ConfirmedRawSetpointValue = 0;
ConfirmedSetpointFlowValue = 0; ConfirmedSetpointFlowValue = 0;
FlowStabilizationRawSetpoint = 0;
} }
partial void OnSetpointAvailableChanged(bool value) partial void OnSetpointAvailableChanged(bool value)

View File

@@ -16,5 +16,5 @@ public sealed class Rs485PumpBindingSettings
public double RawPerLitrePerMinute { get; set; } public double RawPerLitrePerMinute { get; set; }
public double RawOffset { get; set; } public double RawOffset { get; set; }
public double MinFlowLpm { get; set; } public double MinFlowLpm { get; set; }
public double MaxFlowLpm { get; set; } = 7.0; public double MaxFlowLpm { get; set; } = 20.0;
} }

View File

@@ -8,6 +8,7 @@ public interface IRs485PumpFlowService
Rs485PumpFlowOperationResult ReadPumpPreset(Rs485PumpFlowRequest request); Rs485PumpFlowOperationResult ReadPumpPreset(Rs485PumpFlowRequest request);
IReadOnlyList<Rs485PumpRuntimeSnapshot> ReadPumpRuntimeStates(IReadOnlyList<Rs485PumpFlowRequest> requests); IReadOnlyList<Rs485PumpRuntimeSnapshot> ReadPumpRuntimeStates(IReadOnlyList<Rs485PumpFlowRequest> requests);
Rs485PumpFlowOperationResult WritePumpPreset(Rs485PumpFlowRequest request, ushort rawForwardSpeed); Rs485PumpFlowOperationResult WritePumpPreset(Rs485PumpFlowRequest request, ushort rawForwardSpeed);
Rs485PumpFlowOperationResult WritePumpMotorCommand(Rs485PumpFlowRequest request, short rawMotorSpeed);
Rs485PumpFlowOperationResult StartPump(Rs485PumpFlowRequest request, short rawMotorSpeed); Rs485PumpFlowOperationResult StartPump(Rs485PumpFlowRequest request, short rawMotorSpeed);
Rs485PumpFlowOperationResult StopPump(Rs485PumpFlowRequest request); Rs485PumpFlowOperationResult StopPump(Rs485PumpFlowRequest request);
} }

View File

@@ -278,6 +278,48 @@ public sealed class Rs485PumpFlowService : IRs485PumpFlowService
}); });
} }
public Rs485PumpFlowOperationResult WritePumpMotorCommand(Rs485PumpFlowRequest request, short rawMotorSpeed)
{
return Execute(request, master =>
{
var slave = request.PumpSettings.SlaveAddress;
if (rawMotorSpeed <= 0)
{
return Failure("RS485 稳流调节失败:控制值必须大于 0");
}
if (!TryWriteSignedRegister(master, slave, request.PumpSettings.MotorControlRegister, rawMotorSpeed, out var writeError))
{
return Failure($"RS485 稳流调节失败:{writeError}");
}
if (PostCommandVerifyDelayMs > 0)
{
Thread.Sleep(PostCommandVerifyDelayMs);
}
if (TryReadRegister(master, slave, request.PumpSettings.MotorControlRegister, out var motorCommandReadback, out _)
&& motorCommandReadback != unchecked((ushort)rawMotorSpeed))
{
return Failure($"RS485 稳流调节失败:控制寄存器回读 {motorCommandReadback},目标 {rawMotorSpeed}");
}
ushort? runStatus = null;
if (TryReadRegister(master, slave, request.PumpSettings.RunStatusRegister, out var runStatusValue, out _))
{
runStatus = runStatusValue;
}
return new Rs485PumpFlowOperationResult
{
Success = true,
Message = $"RS485 稳流调节成功,控制值 {rawMotorSpeed}",
RunStatus = runStatus,
RawForwardSpeed = (ushort)rawMotorSpeed
};
});
}
public Rs485PumpFlowOperationResult StopPump(Rs485PumpFlowRequest request) public Rs485PumpFlowOperationResult StopPump(Rs485PumpFlowRequest request)
{ {
return Execute(request, master => return Execute(request, master =>

View File

@@ -41,8 +41,12 @@ public partial class MainViewModel
private const double DefaultRs485RawPerLitrePerMinute = 100d; private const double DefaultRs485RawPerLitrePerMinute = 100d;
private const double DefaultRs485RawOffset = 0d; private const double DefaultRs485RawOffset = 0d;
private const int MaxRs485MotorCommand = short.MaxValue; private const int MaxRs485MotorCommand = short.MaxValue;
private const double FlowStabilizationDeadbandLpm = 0.05d;
private const int FlowStabilizationMaxRawStep = 5;
private const double FlowStabilizationMaxRelativeTrim = 0.20d;
private const string PressureDropRs485PumpKey = "PressureDropPump"; private const string PressureDropRs485PumpKey = "PressureDropPump";
private const string KinkResistanceRs485PumpKey = "KinkResistancePump"; private const string KinkResistanceRs485PumpKey = "KinkResistancePump";
private static readonly TimeSpan FlowStabilizationAdjustmentInterval = TimeSpan.FromSeconds(2);
private static readonly string[] HemolysisRs485PumpKeys = private static readonly string[] HemolysisRs485PumpKeys =
[ [
"HemolysisDrainageSinglePump", "HemolysisDrainageSinglePump",
@@ -220,7 +224,7 @@ public partial class MainViewModel
RawPerLitrePerMinute = DefaultRs485RawPerLitrePerMinute, RawPerLitrePerMinute = DefaultRs485RawPerLitrePerMinute,
RawOffset = DefaultRs485RawOffset, RawOffset = DefaultRs485RawOffset,
MinFlowLpm = 0, MinFlowLpm = 0,
MaxFlowLpm = 7.0 MaxFlowLpm = 20.0
}); });
} }
@@ -249,7 +253,7 @@ public partial class MainViewModel
? binding.RawOffset ? binding.RawOffset
: DefaultRs485RawOffset; : DefaultRs485RawOffset;
pump.Rs485MinFlowLpm = binding.MinFlowLpm; pump.Rs485MinFlowLpm = binding.MinFlowLpm;
pump.Rs485MaxFlowLpm = binding.MaxFlowLpm <= 0 ? Math.Max(RatedMaxFlow, 7.0) : binding.MaxFlowLpm; pump.Rs485MaxFlowLpm = binding.MaxFlowLpm <= 0 ? Math.Max(RatedMaxFlow, 20.0) : binding.MaxFlowLpm;
if (string.IsNullOrWhiteSpace(pump.PendingSetpointText)) if (string.IsNullOrWhiteSpace(pump.PendingSetpointText))
{ {
@@ -552,6 +556,132 @@ public partial class MainViewModel
} }
} }
private async Task MaintainRs485FlowStabilizationAsync()
{
var adjustablePumps = ActiveRs485FlowPumpControls
.Where(ShouldMaintainFlowStabilization)
.OrderBy(item => item.Rs485SlaveAddress)
.ToList();
foreach (var pump in adjustablePumps)
{
await TryAdjustRs485FlowStabilizationAsync(pump);
}
}
private bool ShouldMaintainFlowStabilization(PumpControlChannel pump)
{
if (!pump.IsFlowStabilizationEnabled)
{
return false;
}
if (!pump.CanUseFlowStabilization)
{
pump.FlowStabilizationStatusText = "当前项目要求固定转速";
return false;
}
if (pump.IsRs485Busy)
{
pump.FlowStabilizationStatusText = "等待当前 RS485 操作完成";
return false;
}
if (!pump.IsRunning || pump.PendingRs485RunningState == false)
{
pump.FlowStabilizationStatusText = "等待泵运行";
return false;
}
if (!pump.FlowAvailable)
{
pump.FlowStabilizationStatusText = "等待流量反馈";
return false;
}
if (!pump.ConfirmedSetpointAvailable || pump.ConfirmedSetpointFlowValue <= 0)
{
pump.FlowStabilizationStatusText = "等待确认目标流量";
return false;
}
if (DateTime.UtcNow - pump.LastFlowStabilizationAdjustmentUtc < FlowStabilizationAdjustmentInterval)
{
return false;
}
return true;
}
private async Task TryAdjustRs485FlowStabilizationAsync(PumpControlChannel pump)
{
var targetFlow = pump.ConfirmedSetpointFlowValue;
var flowError = targetFlow - pump.FlowValue;
if (Math.Abs(flowError) <= FlowStabilizationDeadbandLpm)
{
pump.FlowStabilizationStatusText = $"稳流保持:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
return;
}
var targetRaw = pump.ConfirmedRawSetpointValue > 0
? pump.ConfirmedRawSetpointValue
: ConvertFlowToRawSpeed(pump, targetFlow);
if (targetRaw <= 0)
{
pump.FlowStabilizationStatusText = "目标流量换算值无效";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
return;
}
var currentRaw = pump.FlowStabilizationRawSetpoint > 0
? pump.FlowStabilizationRawSetpoint
: pump.RawSetpointValue > 0
? pump.RawSetpointValue
: targetRaw;
var maxTrim = Math.Max(FlowStabilizationMaxRawStep, (int)Math.Round(targetRaw * FlowStabilizationMaxRelativeTrim, MidpointRounding.AwayFromZero));
var minRaw = Math.Max(1, targetRaw - maxTrim);
var maxRaw = Math.Min(MaxRs485MotorCommand, targetRaw + maxTrim);
var rawStep = Math.Sign(flowError) * FlowStabilizationMaxRawStep;
var nextRaw = Math.Clamp(currentRaw + rawStep, minRaw, maxRaw);
if (nextRaw == currentRaw)
{
pump.FlowStabilizationStatusText = $"稳流已到调节限幅:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
return;
}
if (!TryBeginRs485PumpOperation(pump, "稳流调节"))
{
return;
}
try
{
var request = BuildRs485Request(pump);
var result = await Task.Run(() => _rs485PumpFlowService.WritePumpMotorCommand(request, (short)nextRaw));
if (!result.Success)
{
pump.FlowStabilizationStatusText = result.Message;
Rs485StatusText = result.Message;
TraceEvents.Insert(0, NewTrace("RS485 稳流调节失败", $"{pump.Name} / {result.Message}"));
return;
}
pump.FlowStabilizationRawSetpoint = nextRaw;
CacheResolvedRs485Setpoint(pump, nextRaw);
ApplyPostCommandPumpState(pump, result, expectedRunning: true);
pump.FlowStabilizationStatusText = $"稳流调节:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min / 控制值 {nextRaw}";
}
finally
{
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
EndRs485PumpOperation(pump);
}
}
private void RaiseRs485CalibrationSummaryChanges() private void RaiseRs485CalibrationSummaryChanges()
{ {
OnPropertyChanged(nameof(Rs485EnabledPumpCount)); OnPropertyChanged(nameof(Rs485EnabledPumpCount));
@@ -1349,6 +1479,7 @@ public partial class MainViewModel
pump.ConfirmedSetpointAvailable = true; pump.ConfirmedSetpointAvailable = true;
pump.ConfirmedRawSetpointValue = rawMotorSpeed; pump.ConfirmedRawSetpointValue = rawMotorSpeed;
pump.ConfirmedSetpointFlowValue = flowLpm; pump.ConfirmedSetpointFlowValue = flowLpm;
pump.FlowStabilizationRawSetpoint = rawMotorSpeed;
} }
private bool TryBeginRs485PumpOperation(PumpControlChannel pump, string operationName) private bool TryBeginRs485PumpOperation(PumpControlChannel pump, string operationName)

View File

@@ -423,6 +423,8 @@ public partial class MainViewModel : ObservableObject, IDisposable
public bool HasFilteredItems => !FilteredItemsView.IsEmpty; public bool HasFilteredItems => !FilteredItemsView.IsEmpty;
public bool HasManualSupplementItems => !ManualSupplementItemsView.IsEmpty; public bool HasManualSupplementItems => !ManualSupplementItemsView.IsEmpty;
public bool HasSelectedItem => SelectedItem is not null; public bool HasSelectedItem => SelectedItem is not null;
public InspectionItem? TightnessInspectionItem => InspectionItems.FirstOrDefault(IsTightnessInspectionItem);
public bool HasTightnessInspectionItem => TightnessInspectionItem is not null;
public IEnumerable<DeviceChannel> FlowSensorChannels => Channels.Where(IsFlowSensorChannel); public IEnumerable<DeviceChannel> FlowSensorChannels => Channels.Where(IsFlowSensorChannel);
public IEnumerable<DeviceChannel> OtherChannels => Channels.Where(channel => !IsFlowSensorChannel(channel)); public IEnumerable<DeviceChannel> OtherChannels => Channels.Where(channel => !IsFlowSensorChannel(channel));
public bool IsTelemetryOnline => _isTelemetryOnline; public bool IsTelemetryOnline => _isTelemetryOnline;
@@ -544,7 +546,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
public bool HasItemSearchText => !string.IsNullOrWhiteSpace(ItemSearchText); public bool HasItemSearchText => !string.IsNullOrWhiteSpace(ItemSearchText);
public int RealtimeMonitorCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.RealtimeMonitor); public int RealtimeMonitorCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.RealtimeMonitor);
public int RealtimeAssistCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.RealtimeAssist); public int RealtimeAssistCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.RealtimeAssist);
public int ManualEntryCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.ManualEntry); public int ManualEntryCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.ManualEntry && !IsTightnessInspectionItem(item));
public string SelectedItemCaptureModeText => SelectedItem?.CaptureModeText ?? "未选择"; public string SelectedItemCaptureModeText => SelectedItem?.CaptureModeText ?? "未选择";
public string SelectedItemMeasurementSource => SelectedItem?.MeasurementSource ?? "-"; public string SelectedItemMeasurementSource => SelectedItem?.MeasurementSource ?? "-";
public bool SelectedItemUsesRealtimeValue => SelectedItem?.CaptureMode == InspectionItemCaptureMode.RealtimeMonitor; public bool SelectedItemUsesRealtimeValue => SelectedItem?.CaptureMode == InspectionItemCaptureMode.RealtimeMonitor;
@@ -566,6 +568,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
public string KinkResistanceMandrelDiameterDisplay => $"圆角模板直径:{Math.Max(KinkResistanceOuterDiameter, 0d) * 4d:F1} mm外径 {KinkResistanceOuterDiameter:F1} mm × 4"; public string KinkResistanceMandrelDiameterDisplay => $"圆角模板直径:{Math.Max(KinkResistanceOuterDiameter, 0d) * 4d:F1} mm外径 {KinkResistanceOuterDiameter:F1} mm × 4";
public bool IsPressureDropSelected => SelectedItem?.Clause == "4.3.1"; public bool IsPressureDropSelected => SelectedItem?.Clause == "4.3.1";
public string PressureDropSamplingSummary => BuildPressureDropSamplingSummary(); public string PressureDropSamplingSummary => BuildPressureDropSamplingSummary();
public bool IsTightnessSelected => SelectedItem is not null && IsTightnessInspectionItem(SelectedItem);
public bool IsKinkResistanceSelected => SelectedItem?.Clause == "4.2.3"; public bool IsKinkResistanceSelected => SelectedItem?.Clause == "4.2.3";
public string KinkResistanceSamplingSummary => BuildKinkResistanceSamplingSummary(); public string KinkResistanceSamplingSummary => BuildKinkResistanceSamplingSummary();
public bool IsAntiCollapseSelected => SelectedItem?.Clause == "4.3.2"; public bool IsAntiCollapseSelected => SelectedItem?.Clause == "4.3.2";
@@ -692,6 +695,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
OnPropertyChanged(nameof(SelectedItemUsesRealtimeValue)); OnPropertyChanged(nameof(SelectedItemUsesRealtimeValue));
OnPropertyChanged(nameof(IsPressureDropSelected)); OnPropertyChanged(nameof(IsPressureDropSelected));
OnPropertyChanged(nameof(PressureDropSamplingSummary)); OnPropertyChanged(nameof(PressureDropSamplingSummary));
OnPropertyChanged(nameof(IsTightnessSelected));
OnPropertyChanged(nameof(IsKinkResistanceSelected)); OnPropertyChanged(nameof(IsKinkResistanceSelected));
OnPropertyChanged(nameof(KinkResistanceFlowPointDisplay)); OnPropertyChanged(nameof(KinkResistanceFlowPointDisplay));
OnPropertyChanged(nameof(KinkResistanceMandrelDiameterDisplay)); OnPropertyChanged(nameof(KinkResistanceMandrelDiameterDisplay));
@@ -1408,6 +1412,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
var snapshot = await Task.Run(_telemetryService.UpdateChannels); var snapshot = await Task.Run(_telemetryService.UpdateChannels);
ApplyTelemetrySnapshot(snapshot); ApplyTelemetrySnapshot(snapshot);
await RefreshRs485RuntimeStateSilentlyAsync(); await RefreshRs485RuntimeStateSilentlyAsync();
await MaintainRs485FlowStabilizationAsync();
_lastTelemetryRefreshFailureMessage = string.Empty; _lastTelemetryRefreshFailureMessage = string.Empty;
RefreshTelemetryPanel(); RefreshTelemetryPanel();
RefreshDeviceStatus(); RefreshDeviceStatus();
@@ -1635,7 +1640,8 @@ public partial class MainViewModel : ObservableObject, IDisposable
var projectCheckItems = FilteredItemsView.Cast<InspectionItem>().ToList(); var projectCheckItems = FilteredItemsView.Cast<InspectionItem>().ToList();
var manualSupplementItems = ManualSupplementItemsView.Cast<InspectionItem>().ToList(); var manualSupplementItems = ManualSupplementItemsView.Cast<InspectionItem>().ToList();
var selectedItemStillVisible = SelectedItem is not null var selectedItemStillVisible = SelectedItem is not null
&& ((SelectedItem.CaptureMode == InspectionItemCaptureMode.ManualEntry && manualSupplementItems.Contains(SelectedItem)) && ((IsTightnessInspectionItem(SelectedItem))
|| (SelectedItem.CaptureMode == InspectionItemCaptureMode.ManualEntry && manualSupplementItems.Contains(SelectedItem))
|| (SelectedItem.CaptureMode != InspectionItemCaptureMode.ManualEntry && projectCheckItems.Contains(SelectedItem))); || (SelectedItem.CaptureMode != InspectionItemCaptureMode.ManualEntry && projectCheckItems.Contains(SelectedItem)));
if (selectedItemStillVisible) if (selectedItemStillVisible)
@@ -2132,8 +2138,13 @@ public partial class MainViewModel : ObservableObject, IDisposable
private bool MatchesManualSupplementItem(object item) => private bool MatchesManualSupplementItem(object item) =>
item is InspectionItem inspectionItem item is InspectionItem inspectionItem
&& inspectionItem.CaptureMode == InspectionItemCaptureMode.ManualEntry && inspectionItem.CaptureMode == InspectionItemCaptureMode.ManualEntry
&& !IsTightnessInspectionItem(inspectionItem)
&& MatchesItemSearch(inspectionItem); && MatchesItemSearch(inspectionItem);
private static bool IsTightnessInspectionItem(InspectionItem item) =>
string.Equals(item.Clause, "4.2.1", StringComparison.Ordinal)
&& string.Equals(item.Item, "血液通道密合性", StringComparison.Ordinal);
private bool MatchesItemSearch(InspectionItem item) private bool MatchesItemSearch(InspectionItem item)
{ {
if (string.IsNullOrWhiteSpace(ItemSearchText)) if (string.IsNullOrWhiteSpace(ItemSearchText))

View File

@@ -37,7 +37,7 @@
"RawPerLitrePerMinute": 100, "RawPerLitrePerMinute": 100,
"RawOffset": 0, "RawOffset": 0,
"MinFlowLpm": 0, "MinFlowLpm": 0,
"MaxFlowLpm": 7 "MaxFlowLpm": 20
}, },
{ {
"PumpKey": "RecirculationMainPump", "PumpKey": "RecirculationMainPump",
@@ -54,7 +54,7 @@
"RawPerLitrePerMinute": 100, "RawPerLitrePerMinute": 100,
"RawOffset": 0, "RawOffset": 0,
"MinFlowLpm": 0, "MinFlowLpm": 0,
"MaxFlowLpm": 7 "MaxFlowLpm": 20
}, },
{ {
"PumpKey": "RecirculationReturnPump", "PumpKey": "RecirculationReturnPump",
@@ -71,7 +71,7 @@
"RawPerLitrePerMinute": 100, "RawPerLitrePerMinute": 100,
"RawOffset": 0, "RawOffset": 0,
"MinFlowLpm": 0, "MinFlowLpm": 0,
"MaxFlowLpm": 7 "MaxFlowLpm": 20
}, },
{ {
"PumpKey": "RecirculationDrainagePump", "PumpKey": "RecirculationDrainagePump",
@@ -88,7 +88,7 @@
"RawPerLitrePerMinute": 100, "RawPerLitrePerMinute": 100,
"RawOffset": 0, "RawOffset": 0,
"MinFlowLpm": 0, "MinFlowLpm": 0,
"MaxFlowLpm": 7 "MaxFlowLpm": 20
}, },
{ {
"PumpKey": "KinkResistancePump", "PumpKey": "KinkResistancePump",
@@ -105,7 +105,7 @@
"RawPerLitrePerMinute": 100, "RawPerLitrePerMinute": 100,
"RawOffset": 0, "RawOffset": 0,
"MinFlowLpm": 0, "MinFlowLpm": 0,
"MaxFlowLpm": 7 "MaxFlowLpm": 20
}, },
{ {
"PumpKey": "HemolysisDrainageSinglePump", "PumpKey": "HemolysisDrainageSinglePump",
@@ -122,7 +122,7 @@
"RawPerLitrePerMinute": 100, "RawPerLitrePerMinute": 100,
"RawOffset": 0, "RawOffset": 0,
"MinFlowLpm": 0, "MinFlowLpm": 0,
"MaxFlowLpm": 7 "MaxFlowLpm": 20
}, },
{ {
"PumpKey": "HemolysisReturnSinglePump", "PumpKey": "HemolysisReturnSinglePump",
@@ -139,7 +139,7 @@
"RawPerLitrePerMinute": 100, "RawPerLitrePerMinute": 100,
"RawOffset": 0, "RawOffset": 0,
"MinFlowLpm": 0, "MinFlowLpm": 0,
"MaxFlowLpm": 7 "MaxFlowLpm": 20
}, },
{ {
"PumpKey": "HemolysisDualLumenPump", "PumpKey": "HemolysisDualLumenPump",
@@ -156,7 +156,7 @@
"RawPerLitrePerMinute": 100, "RawPerLitrePerMinute": 100,
"RawOffset": 0, "RawOffset": 0,
"MinFlowLpm": 0, "MinFlowLpm": 0,
"MaxFlowLpm": 7 "MaxFlowLpm": 20
} }
] ]
} }