Skip to content

Commit 240ff97

Browse files
authored
Merge pull request #10 from masarray/fix/command-dispatch-icon-compact
Make command dispatch deterministic and compact IED Explorer
2 parents 2260c1d + ca4f710 commit 240ff97

4 files changed

Lines changed: 101 additions & 107 deletions

File tree

MainWindow.xaml

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@
111111
<TabItem Header="IEC 61850 Explorer">
112112
<Grid>
113113
<Grid.ColumnDefinitions>
114-
<ColumnDefinition Width="292"/>
115-
<ColumnDefinition Width="14"/>
114+
<ColumnDefinition Width="250"/>
115+
<ColumnDefinition Width="12"/>
116116
<ColumnDefinition Width="*"/>
117117
</Grid.ColumnDefinitions>
118118

119-
<Border Grid.Column="0" Style="{StaticResource Card}" Padding="12">
119+
<Border Grid.Column="0" Style="{StaticResource Card}" Padding="10">
120120
<DockPanel>
121121
<Grid DockPanel.Dock="Top" Margin="0,0,0,10">
122122
<StackPanel>
@@ -192,41 +192,26 @@
192192
</ListBox.ItemContainerStyle>
193193
<ListBox.ItemTemplate>
194194
<DataTemplate>
195-
<Grid MinHeight="82">
195+
<Grid MinHeight="86">
196196
<Grid x:Name="CardContent">
197197
<Grid.RowDefinitions>
198198
<RowDefinition Height="Auto"/>
199199
<RowDefinition Height="Auto"/>
200200
</Grid.RowDefinitions>
201201
<Grid.ColumnDefinitions>
202-
<ColumnDefinition Width="44"/>
202+
<ColumnDefinition Width="64"/>
203203
<ColumnDefinition Width="*"/>
204204
</Grid.ColumnDefinitions>
205+
<!-- Large borderless relay / BCU icon; icon color is the connection state. -->
206+
<Grid Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Width="58" Height="58" Margin="0,-2,6,0" VerticalAlignment="Top" Panel.ZIndex="2">
207+
<Viewbox Width="55" Height="55" HorizontalAlignment="Center" VerticalAlignment="Center">
208+
<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">
209+
<Path.Effect><DropShadowEffect BlurRadius="14" ShadowDepth="0" Opacity="0.58" Color="#EF4444"/></Path.Effect>
210+
</Path>
211+
</Viewbox>
212+
</Grid>
205213

206-
<!-- Protection relay / BCU silhouette supplied by the user. -->
207-
<Grid Grid.Row="0" Grid.RowSpan="2" Grid.Column="0"
208-
Width="38" Height="38" Margin="0,1,8,0"
209-
VerticalAlignment="Top" Panel.ZIndex="2">
210-
<Border Width="34" Height="34" CornerRadius="7"
211-
Background="#EEF4FF" BorderBrush="#CFE0FF" BorderThickness="1"
212-
HorizontalAlignment="Center" VerticalAlignment="Center">
213-
<Viewbox Width="22" Height="22" Margin="5">
214-
<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"
215-
Fill="#2563EB" Stretch="Uniform"/>
216-
</Viewbox>
217-
</Border>
218-
<Ellipse x:Name="ConnectionDot" Width="9" Height="9" Fill="#EF4444"
219-
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,0,0,0">
220-
<Ellipse.Effect><DropShadowEffect BlurRadius="6" ShadowDepth="0" Opacity="0.28" Color="#EF4444"/></Ellipse.Effect>
221-
</Ellipse>
222-
<Ellipse x:Name="TxDot" Width="7" Height="7" Fill="#86EFAC" Visibility="Collapsed"
223-
HorizontalAlignment="Right" VerticalAlignment="Bottom" RenderTransformOrigin="0.5,0.5" Opacity="0.72">
224-
<Ellipse.RenderTransform><ScaleTransform x:Name="TxPulseScale" ScaleX="1" ScaleY="1"/></Ellipse.RenderTransform>
225-
<Ellipse.Effect><DropShadowEffect BlurRadius="8" ShadowDepth="0" Opacity="0.8" Color="#4ADE80"/></Ellipse.Effect>
226-
</Ellipse>
227-
</Grid>
228-
229-
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Top">
214+
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Top">
230215
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
231216
Foreground="{StaticResource Ink}" TextTrimming="CharacterEllipsis"/>
232217
<TextBlock Text="{Binding EndpointText}" FontSize="11.6"
@@ -263,7 +248,7 @@
263248

264249
<Border x:Name="BusyOverlay" Visibility="Collapsed" Background="#F2F7FDFA" CornerRadius="12"
265250
BorderBrush="#B8CEF3" BorderThickness="1" Padding="12" Panel.ZIndex="20">
266-
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="238">
251+
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="190">
267252
<!-- Eight identical rounded dashes recreate the original loading circle
268253
without the asymmetric final dash produced by Ellipse.StrokeDashArray. -->
269254
<Grid Width="32" Height="32" HorizontalAlignment="Center" RenderTransformOrigin="0.5,0.5">
@@ -291,10 +276,10 @@
291276
<TextBlock Text="{Binding BusyTitle}" FontSize="12.2" FontWeight="SemiBold" Foreground="{StaticResource Ink}"
292277
HorizontalAlignment="Center" Margin="0,7,0,0"/>
293278
<TextBlock Text="{Binding BusyStage}" FontSize="10.8" Foreground="{StaticResource Muted}" TextAlignment="Center"
294-
TextWrapping="Wrap" Margin="0,3,0,0" MinHeight="28" MaxWidth="224"/>
279+
TextWrapping="Wrap" Margin="0,3,0,0" MinHeight="28" MaxWidth="184"/>
295280
<ProgressBar Value="{Binding DiscoveryProgressPercent, Mode=OneWay}" Minimum="0" Maximum="100"
296-
Style="{StaticResource DiscoveryProgressBar}" Width="218" Height="8" Margin="0,8,0,0"/>
297-
<Grid Width="218" Margin="0,5,0,0">
281+
Style="{StaticResource DiscoveryProgressBar}" Width="178" Height="8" Margin="0,8,0,0"/>
282+
<Grid Width="178" Margin="0,5,0,0">
298283
<TextBlock Text="{Binding DiscoveryProgressStepText}" FontSize="10.2" Foreground="#5D6B82"/>
299284
<TextBlock Text="{Binding DiscoveryProgressPercentText}" FontSize="10.4" FontWeight="SemiBold"
300285
Foreground="{StaticResource Accent}" HorizontalAlignment="Right"/>
@@ -304,24 +289,10 @@
304289
</Grid>
305290
<DataTemplate.Triggers>
306291
<DataTrigger Binding="{Binding IsConnected}" Value="True">
307-
<Setter TargetName="ConnectionDot" Property="Fill" Value="#22C55E"/>
308-
</DataTrigger>
309-
<DataTrigger Binding="{Binding HasReportStream}" Value="True">
310-
<Setter TargetName="TxDot" Property="Visibility" Value="Visible"/>
311-
</DataTrigger>
312-
<DataTrigger Binding="{Binding ReportPulseActive}" Value="True">
313-
<Setter TargetName="TxDot" Property="Fill" Value="#BBF7D0"/>
314-
<Setter TargetName="TxDot" Property="Opacity" Value="1"/>
315-
<DataTrigger.EnterActions>
316-
<BeginStoryboard>
317-
<Storyboard>
318-
<DoubleAnimation Storyboard.TargetName="TxPulseScale" Storyboard.TargetProperty="ScaleX" From="1" To="1.6" AutoReverse="True" Duration="0:0:0.22"/>
319-
<DoubleAnimation Storyboard.TargetName="TxPulseScale" Storyboard.TargetProperty="ScaleY" From="1" To="1.6" AutoReverse="True" Duration="0:0:0.22"/>
320-
</Storyboard>
321-
</BeginStoryboard>
322-
</DataTrigger.EnterActions>
323-
</DataTrigger>
324-
<DataTrigger Binding="{Binding IsBusy}" Value="True">
292+
<Setter TargetName="RelayDeviceIcon" Property="Fill" Value="#16A34A"/>
293+
<Setter TargetName="RelayDeviceIcon" Property="Effect"><Setter.Value><DropShadowEffect BlurRadius="15" ShadowDepth="0" Opacity="0.64" Color="#16A34A"/></Setter.Value></Setter>
294+
</DataTrigger>
295+
<DataTrigger Binding="{Binding IsBusy}" Value="True">
325296
<Setter TargetName="CardContent" Property="Opacity" Value="0.22"/>
326297
<Setter TargetName="CardContent" Property="IsHitTestVisible" Value="False"/>
327298
<Setter TargetName="BusyOverlay" Property="Visibility" Value="Visible"/>

MainWindow.xaml.cs

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -766,77 +766,85 @@ private async void ControlQuickAction_Click(object sender, RoutedEventArgs e)
766766

767767
private async Task ExecuteQuickControlAsync(SignalDefinition signal, string requestedValue)
768768
{
769-
var device = SelectedDevice;
770-
if (device == null || signal.ControlIsBusy)
771-
return;
769+
var device = _signalOwners.TryGetValue(signal, out var owner) ? owner : SelectedDevice;
770+
if (device == null)
771+
return;
772772

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

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

790786
signal.ControlIsBusy = true;
791-
signal.ControlLastResult = $"Sending {requestedValue}…";
787+
signal.ControlLastResult = $"Dispatching {requestedValue}…";
788+
SetStatus($"{device.Name}: dispatching {signal.Name} = {requestedValue}…");
789+
await Dispatcher.Yield(DispatcherPriority.Render);
790+
792791
try
793792
{
794-
if (signal.ControlModelText == "Auto-detect" || signal.ControlCurrentValue == "-")
795-
{
796-
var capabilities = await _runtime.InspectControlAsync(
797-
device.DeviceId,
798-
signal,
799-
_applicationCancellation.Token);
800-
signal.ControlCurrentValue = capabilities.CurrentValue;
801-
device.RefreshCommandSignalProjection();
802-
RebuildControlFeedbackIndex(device);
803-
}
804-
805-
var result = await _runtime.ExecuteControlAsync(
806-
device.DeviceId,
807-
new Iec61850ControlCommandRequest
808-
{
809-
Signal = signal,
810-
ValueText = requestedValue,
811-
InterlockCheck = CommandInterlockCheck,
812-
SynchroCheck = CommandSynchroCheck,
813-
TestMode = CommandTestMode,
814-
FeedbackTimeoutMs = signal.IsPositionControl ? 12000 :
815-
(signal.IsRaiseOnlyControl || signal.IsLowerOnlyControl || signal.IsRaiseLowerControl) ? 15000 : 8000,
816-
CommandTerminationTimeoutMs = 10000,
817-
OriginCategory = "Maintenance"
818-
},
819-
_applicationCancellation.Token);
820-
821-
if (!string.IsNullOrWhiteSpace(result.FeedbackValue) && result.FeedbackValue != "-")
822-
signal.ControlCurrentValue = result.FeedbackValue;
823-
824-
signal.ControlLastResult = BuildQuickControlResult(result);
825-
SetStatus($"{device.Name}: {signal.Name}{signal.ControlLastResult}");
793+
if (!device.IsConnected)
794+
{
795+
SetStatus($"{device.Name}: connecting before control…");
796+
var connected = device.HasDiscoveryCache && device.Signals.Count > 0
797+
? await ConnectUsingSavedModelAsync(device)
798+
: await ConnectAndConfigureDeviceAsync(device, openWizard: false);
799+
if (!connected)
800+
return;
801+
}
802+
803+
if (signal.ControlModelText == "Auto-detect" || signal.ControlCurrentValue == "-")
804+
{
805+
var capabilities = await _runtime.InspectControlAsync(device.DeviceId, signal, _applicationCancellation.Token);
806+
signal.ControlCurrentValue = capabilities.CurrentValue;
807+
device.RefreshCommandSignalProjection();
808+
RebuildControlFeedbackIndex(device);
809+
}
810+
811+
var result = await _runtime.ExecuteControlAsync(
812+
device.DeviceId,
813+
new Iec61850ControlCommandRequest
814+
{
815+
Signal = signal,
816+
ValueText = requestedValue,
817+
InterlockCheck = CommandInterlockCheck,
818+
SynchroCheck = CommandSynchroCheck,
819+
TestMode = CommandTestMode,
820+
FeedbackTimeoutMs = signal.IsPositionControl ? 12000 :
821+
(signal.IsRaiseOnlyControl || signal.IsLowerOnlyControl || signal.IsRaiseLowerControl) ? 15000 : 8000,
822+
CommandTerminationTimeoutMs = 10000,
823+
OriginCategory = "Maintenance"
824+
},
825+
_applicationCancellation.Token);
826+
827+
if (!string.IsNullOrWhiteSpace(result.FeedbackValue) && result.FeedbackValue != "-")
828+
signal.ControlCurrentValue = result.FeedbackValue;
829+
830+
signal.ControlLastResult = BuildQuickControlResult(result);
831+
SetStatus($"{device.Name}: {signal.Name}{signal.ControlLastResult}");
826832
}
827833
catch (OperationCanceledException)
828834
{
829-
signal.ControlLastResult = "Command cancelled.";
835+
signal.ControlLastResult = "Command cancelled.";
836+
SetStatus($"{device.Name}: {signal.Name} command cancelled.");
830837
}
831838
catch (Exception ex)
832839
{
833-
signal.ControlLastResult = $"Command failed: {ex.Message}";
834-
AddLog("ERROR", device.Name, $"Quick control failed for {signal.ObjectReference}: {ex}");
835-
MarkDiagnosticAlert();
840+
signal.ControlLastResult = $"Command failed: {ex.Message}";
841+
AddLog("ERROR", device.Name, $"Quick control failed for {signal.ObjectReference}: {ex}");
842+
SetStatus($"{device.Name}: {signal.Name} command failed — {ex.Message}");
843+
MarkDiagnosticAlert();
836844
}
837845
finally
838846
{
839-
signal.ControlIsBusy = false;
847+
signal.ControlIsBusy = false;
840848
}
841849
}
842850

0 commit comments

Comments
 (0)