Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 22 additions & 51 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@
<TabItem Header="IEC 61850 Explorer">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="292"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<Border Grid.Column="0" Style="{StaticResource Card}" Padding="12">
<Border Grid.Column="0" Style="{StaticResource Card}" Padding="10">
<DockPanel>
<Grid DockPanel.Dock="Top" Margin="0,0,0,10">
<StackPanel>
Expand Down Expand Up @@ -192,41 +192,26 @@
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid MinHeight="82">
<Grid MinHeight="86">
<Grid x:Name="CardContent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="44"/>
<ColumnDefinition Width="64"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Large borderless relay / BCU icon; icon color is the connection state. -->
<Grid Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Width="58" Height="58" Margin="0,-2,6,0" VerticalAlignment="Top" Panel.ZIndex="2">
<Viewbox Width="55" Height="55" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path x:Name="RelayDeviceIcon" Data="M 2 0 L 2 20 L 18 20 L 18 0 L 2 0 z M 3 1 L 17 1 L 17 19 L 3 19 L 3 1 z M 5 3 L 5 7 L 15 7 L 15 3 L 5 3 z M 6 4 L 14 4 L 14 6 L 6 6 L 6 4 z M 5 10 L 5 11 L 7 11 L 7 10 L 5 10 z M 9 10 L 9 11 L 11 11 L 11 10 L 9 10 z M 13 10 L 13 11 L 15 11 L 15 10 L 13 10 z M 5 13 L 5 14 L 7 14 L 7 13 L 5 13 z M 9 13 L 9 14 L 11 14 L 11 13 L 9 13 z M 13 13 L 13 14 L 15 14 L 15 13 L 13 13 z M 5 16 L 5 17 L 7 17 L 7 16 L 5 16 z M 9 16 L 9 17 L 11 17 L 11 16 L 9 16 z M 13 16 L 13 17 L 15 17 L 15 16 L 13 16 z" Fill="#EF4444" Stretch="Uniform">
<Path.Effect><DropShadowEffect BlurRadius="14" ShadowDepth="0" Opacity="0.58" Color="#EF4444"/></Path.Effect>
</Path>
</Viewbox>
</Grid>

<!-- Protection relay / BCU silhouette supplied by the user. -->
<Grid Grid.Row="0" Grid.RowSpan="2" Grid.Column="0"
Width="38" Height="38" Margin="0,1,8,0"
VerticalAlignment="Top" Panel.ZIndex="2">
<Border Width="34" Height="34" CornerRadius="7"
Background="#EEF4FF" BorderBrush="#CFE0FF" BorderThickness="1"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Viewbox Width="22" Height="22" Margin="5">
<Path Data="M 2 0 L 2 20 L 18 20 L 18 0 L 2 0 z M 3 1 L 17 1 L 17 19 L 3 19 L 3 1 z M 5 3 L 5 7 L 15 7 L 15 3 L 5 3 z M 6 4 L 14 4 L 14 6 L 6 6 L 6 4 z M 5 10 L 5 11 L 7 11 L 7 10 L 5 10 z M 9 10 L 9 11 L 11 11 L 11 10 L 9 10 z M 13 10 L 13 11 L 15 11 L 15 10 L 13 10 z M 5 13 L 5 14 L 7 14 L 7 13 L 5 13 z M 9 13 L 9 14 L 11 14 L 11 13 L 9 13 z M 13 13 L 13 14 L 15 14 L 15 13 L 13 13 z M 5 16 L 5 17 L 7 17 L 7 16 L 5 16 z M 9 16 L 9 17 L 11 17 L 11 16 L 9 16 z M 13 16 L 13 17 L 15 17 L 15 16 L 13 16 z"
Fill="#2563EB" Stretch="Uniform"/>
</Viewbox>
</Border>
<Ellipse x:Name="ConnectionDot" Width="9" Height="9" Fill="#EF4444"
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,0,0,0">
<Ellipse.Effect><DropShadowEffect BlurRadius="6" ShadowDepth="0" Opacity="0.28" Color="#EF4444"/></Ellipse.Effect>
</Ellipse>
<Ellipse x:Name="TxDot" Width="7" Height="7" Fill="#86EFAC" Visibility="Collapsed"
HorizontalAlignment="Right" VerticalAlignment="Bottom" RenderTransformOrigin="0.5,0.5" Opacity="0.72">
<Ellipse.RenderTransform><ScaleTransform x:Name="TxPulseScale" ScaleX="1" ScaleY="1"/></Ellipse.RenderTransform>
<Ellipse.Effect><DropShadowEffect BlurRadius="8" ShadowDepth="0" Opacity="0.8" Color="#4ADE80"/></Ellipse.Effect>
</Ellipse>
</Grid>

<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Top">
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Top">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
Foreground="{StaticResource Ink}" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding EndpointText}" FontSize="11.6"
Expand Down Expand Up @@ -263,7 +248,7 @@

<Border x:Name="BusyOverlay" Visibility="Collapsed" Background="#F2F7FDFA" CornerRadius="12"
BorderBrush="#B8CEF3" BorderThickness="1" Padding="12" Panel.ZIndex="20">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="238">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="190">
<!-- Eight identical rounded dashes recreate the original loading circle
without the asymmetric final dash produced by Ellipse.StrokeDashArray. -->
<Grid Width="32" Height="32" HorizontalAlignment="Center" RenderTransformOrigin="0.5,0.5">
Expand Down Expand Up @@ -291,10 +276,10 @@
<TextBlock Text="{Binding BusyTitle}" FontSize="12.2" FontWeight="SemiBold" Foreground="{StaticResource Ink}"
HorizontalAlignment="Center" Margin="0,7,0,0"/>
<TextBlock Text="{Binding BusyStage}" FontSize="10.8" Foreground="{StaticResource Muted}" TextAlignment="Center"
TextWrapping="Wrap" Margin="0,3,0,0" MinHeight="28" MaxWidth="224"/>
TextWrapping="Wrap" Margin="0,3,0,0" MinHeight="28" MaxWidth="184"/>
<ProgressBar Value="{Binding DiscoveryProgressPercent, Mode=OneWay}" Minimum="0" Maximum="100"
Style="{StaticResource DiscoveryProgressBar}" Width="218" Height="8" Margin="0,8,0,0"/>
<Grid Width="218" Margin="0,5,0,0">
Style="{StaticResource DiscoveryProgressBar}" Width="178" Height="8" Margin="0,8,0,0"/>
<Grid Width="178" Margin="0,5,0,0">
<TextBlock Text="{Binding DiscoveryProgressStepText}" FontSize="10.2" Foreground="#5D6B82"/>
<TextBlock Text="{Binding DiscoveryProgressPercentText}" FontSize="10.4" FontWeight="SemiBold"
Foreground="{StaticResource Accent}" HorizontalAlignment="Right"/>
Expand All @@ -304,24 +289,10 @@
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter TargetName="ConnectionDot" Property="Fill" Value="#22C55E"/>
</DataTrigger>
<DataTrigger Binding="{Binding HasReportStream}" Value="True">
<Setter TargetName="TxDot" Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding ReportPulseActive}" Value="True">
<Setter TargetName="TxDot" Property="Fill" Value="#BBF7D0"/>
<Setter TargetName="TxDot" Property="Opacity" Value="1"/>
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="TxPulseScale" Storyboard.TargetProperty="ScaleX" From="1" To="1.6" AutoReverse="True" Duration="0:0:0.22"/>
<DoubleAnimation Storyboard.TargetName="TxPulseScale" Storyboard.TargetProperty="ScaleY" From="1" To="1.6" AutoReverse="True" Duration="0:0:0.22"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter TargetName="RelayDeviceIcon" Property="Fill" Value="#16A34A"/>
<Setter TargetName="RelayDeviceIcon" Property="Effect"><Setter.Value><DropShadowEffect BlurRadius="15" ShadowDepth="0" Opacity="0.64" Color="#16A34A"/></Setter.Value></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter TargetName="CardContent" Property="Opacity" Value="0.22"/>
<Setter TargetName="CardContent" Property="IsHitTestVisible" Value="False"/>
<Setter TargetName="BusyOverlay" Property="Visibility" Value="Visible"/>
Expand Down
112 changes: 60 additions & 52 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -766,77 +766,85 @@ private async void ControlQuickAction_Click(object sender, RoutedEventArgs e)

private async Task ExecuteQuickControlAsync(SignalDefinition signal, string requestedValue)
{
var device = SelectedDevice;
if (device == null || signal.ControlIsBusy)
return;
var device = _signalOwners.TryGetValue(signal, out var owner) ? owner : SelectedDevice;
if (device == null)
return;

if (!CommandTestMode && !LiveControlArmed)
if (signal.ControlIsBusy)
{
signal.ControlLastResult = "Enable Live control armed before sending a command.";
SetStatus("Live control is not armed. Review the selected IED and enable the Command Panel safety switch.");
return;
SetStatus($"{device.Name}: {signal.Name} command is already in progress.");
return;
}

if (!device.IsConnected)
if (!CommandTestMode && !LiveControlArmed)
{
SetStatus($"{device.Name}: connecting before control…");
var connected = device.HasDiscoveryCache && device.Signals.Count > 0
? await ConnectUsingSavedModelAsync(device)
: await ConnectAndConfigureDeviceAsync(device, openWizard: false);
if (!connected)
return;
signal.ControlLastResult = "Enable Live control armed before sending a command.";
SetStatus("Live control is not armed. Review the selected IED and enable the Command Panel safety switch.");
return;
}

signal.ControlIsBusy = true;
signal.ControlLastResult = $"Sending {requestedValue}…";
signal.ControlLastResult = $"Dispatching {requestedValue}…";
SetStatus($"{device.Name}: dispatching {signal.Name} = {requestedValue}…");
await Dispatcher.Yield(DispatcherPriority.Render);

try
{
if (signal.ControlModelText == "Auto-detect" || signal.ControlCurrentValue == "-")
{
var capabilities = await _runtime.InspectControlAsync(
device.DeviceId,
signal,
_applicationCancellation.Token);
signal.ControlCurrentValue = capabilities.CurrentValue;
device.RefreshCommandSignalProjection();
RebuildControlFeedbackIndex(device);
}

var result = await _runtime.ExecuteControlAsync(
device.DeviceId,
new Iec61850ControlCommandRequest
{
Signal = signal,
ValueText = requestedValue,
InterlockCheck = CommandInterlockCheck,
SynchroCheck = CommandSynchroCheck,
TestMode = CommandTestMode,
FeedbackTimeoutMs = signal.IsPositionControl ? 12000 :
(signal.IsRaiseOnlyControl || signal.IsLowerOnlyControl || signal.IsRaiseLowerControl) ? 15000 : 8000,
CommandTerminationTimeoutMs = 10000,
OriginCategory = "Maintenance"
},
_applicationCancellation.Token);

if (!string.IsNullOrWhiteSpace(result.FeedbackValue) && result.FeedbackValue != "-")
signal.ControlCurrentValue = result.FeedbackValue;

signal.ControlLastResult = BuildQuickControlResult(result);
SetStatus($"{device.Name}: {signal.Name} — {signal.ControlLastResult}");
if (!device.IsConnected)
{
SetStatus($"{device.Name}: connecting before control…");
var connected = device.HasDiscoveryCache && device.Signals.Count > 0
? await ConnectUsingSavedModelAsync(device)
: await ConnectAndConfigureDeviceAsync(device, openWizard: false);
if (!connected)
return;
}

if (signal.ControlModelText == "Auto-detect" || signal.ControlCurrentValue == "-")
{
var capabilities = await _runtime.InspectControlAsync(device.DeviceId, signal, _applicationCancellation.Token);
signal.ControlCurrentValue = capabilities.CurrentValue;
device.RefreshCommandSignalProjection();
RebuildControlFeedbackIndex(device);
}

var result = await _runtime.ExecuteControlAsync(
device.DeviceId,
new Iec61850ControlCommandRequest
{
Signal = signal,
ValueText = requestedValue,
InterlockCheck = CommandInterlockCheck,
SynchroCheck = CommandSynchroCheck,
TestMode = CommandTestMode,
FeedbackTimeoutMs = signal.IsPositionControl ? 12000 :
(signal.IsRaiseOnlyControl || signal.IsLowerOnlyControl || signal.IsRaiseLowerControl) ? 15000 : 8000,
CommandTerminationTimeoutMs = 10000,
OriginCategory = "Maintenance"
},
_applicationCancellation.Token);

if (!string.IsNullOrWhiteSpace(result.FeedbackValue) && result.FeedbackValue != "-")
signal.ControlCurrentValue = result.FeedbackValue;

signal.ControlLastResult = BuildQuickControlResult(result);
SetStatus($"{device.Name}: {signal.Name} — {signal.ControlLastResult}");
}
catch (OperationCanceledException)
{
signal.ControlLastResult = "Command cancelled.";
signal.ControlLastResult = "Command cancelled.";
SetStatus($"{device.Name}: {signal.Name} command cancelled.");
}
catch (Exception ex)
{
signal.ControlLastResult = $"Command failed: {ex.Message}";
AddLog("ERROR", device.Name, $"Quick control failed for {signal.ObjectReference}: {ex}");
MarkDiagnosticAlert();
signal.ControlLastResult = $"Command failed: {ex.Message}";
AddLog("ERROR", device.Name, $"Quick control failed for {signal.ObjectReference}: {ex}");
SetStatus($"{device.Name}: {signal.Name} command failed — {ex.Message}");
MarkDiagnosticAlert();
}
finally
{
signal.ControlIsBusy = false;
signal.ControlIsBusy = false;
}
}

Expand Down
Loading
Loading