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
50 changes: 50 additions & 0 deletions source/CPDLCPlugin/Messages/OpenConnectionRequestWindowRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using CPDLCPlugin.Configuration;
using CPDLCPlugin.ViewModels;
using CPDLCPlugin.Windows;
using MediatR;

namespace CPDLCPlugin.Messages;

public record OpenConnectionRequestWindowRequest(string Callsign) : IRequest;

public class OpenConnectionRequestWindowRequestHandler(
WindowManager windowManager,
PluginConfiguration pluginConfiguration,
IMediator mediator,
Plugin plugin,
IErrorReporter errorReporter)
: IRequestHandler<OpenConnectionRequestWindowRequest>
{
public Task Handle(OpenConnectionRequestWindowRequest request, CancellationToken cancellationToken)
{
// Each window is specific to a callsign, so we always want a fresh window
windowManager.TryRemoveWindow(WindowKeys.ConnectionRequest);

windowManager.FocusOrCreateWindow(
WindowKeys.ConnectionRequest,
"CPDLC Connection Request",
windowHandle =>
{
var currentStationIdentifier = plugin.ConnectionManager?.StationIdentifier;
var viewModel = new ConnectionRequestViewModel(
mediator,
errorReporter,
windowHandle,
request.Callsign,
pluginConfiguration.Stations,
string.IsNullOrEmpty(currentStationIdentifier) ? pluginConfiguration.Stations.First() : currentStationIdentifier!);

var control = new ConnectionRequestView(viewModel);
return control;
},
configureForm: form =>
{
form.HasMinimizeButton = false;
form.MinimizeBox = false;
form.HasCloseButton = false;
form.HasSendBackButton = false;
});

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,21 @@ public async Task Handle(RebuildCpdlcStatusLabelItemsRequest request, Cancellati
if (isOnAcarsNetwork && connection is null)
{
text = ".";
// TODO: Left click will initiate a manual connection
onMouseClick = args =>
{
try
{
if (args.Button != CustomLabelItemMouseButton.Left)
return;

mediator.Send(new OpenConnectionRequestWindowRequest(callsign));
args.Handled = true;
}
catch (Exception ex)
{
errorReporter.ReportError(ex, "Error opening CPDLC connection request window");
}
};
}
else if (connection is not null && connection.DataAuthorityState == DataAuthorityState.NextDataAuthority)
{
Expand Down
26 changes: 26 additions & 0 deletions source/CPDLCPlugin/Messages/SendConnectionRequestRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using MediatR;
using Serilog;

namespace CPDLCPlugin.Messages;

public record SendConnectionRequestRequest(string Callsign, string StationId) : IRequest;

public class SendConnectionRequestRequestHandler(Plugin plugin, ILogger logger)
: IRequestHandler<SendConnectionRequestRequest>
{
public async Task Handle(SendConnectionRequestRequest request, CancellationToken cancellationToken)
{
logger.Information("Sending connection request for {StationId} to {Callsign}", request.StationId, request.Callsign);

if (plugin.ConnectionManager is null || !plugin.ConnectionManager.IsConnected)
{
logger.Warning("Not connected to server");
return;
}

await plugin.ConnectionManager.RequestConnection(
request.Callsign,
request.StationId,
cancellationToken);
}
}
10 changes: 10 additions & 0 deletions source/CPDLCPlugin/Server/SignalRConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@ await connection.InvokeAsync<UplinkMessageDto>(
cancellationToken: cancellationToken);
}

public async Task RequestConnection(string callsign, string stationId, CancellationToken cancellationToken)
{
var connection = GetConnectedOrThrow();
await connection.InvokeAsync(
"RequestConnection",
callsign,
stationId,
cancellationToken);
}

public async Task<AircraftConnectionDto[]> GetConnectedAircraft(CancellationToken cancellationToken)
{
var connection = GetConnectedOrThrow();
Expand Down
61 changes: 61 additions & 0 deletions source/CPDLCPlugin/ViewModels/ConnectionRequestViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CPDLCPlugin.Messages;
using MediatR;

namespace CPDLCPlugin.ViewModels;

public partial class ConnectionRequestViewModel : ObservableObject
{
readonly IMediator _mediator;
readonly IErrorReporter _errorReporter;
readonly IWindowHandle _windowHandle;

[ObservableProperty] string callsign;
[ObservableProperty] string[] availableStationIdentifiers;
[ObservableProperty] string selectedStationIdentifier;

// Example data for the XAML designer. Do not use at runtime.
public ConnectionRequestViewModel()
{
Callsign = "QFA123AB";
AvailableStationIdentifiers = ["YBBB", "YMMM"];
SelectedStationIdentifier = "YBBB";
}

public ConnectionRequestViewModel(
IMediator mediator,
IErrorReporter errorReporter,
IWindowHandle windowHandle,
string callsign,
string[] availableStationIdentifiers,
string selectedStationIdentifier)
{
_mediator = mediator;
_errorReporter = errorReporter;
_windowHandle = windowHandle;
Callsign = callsign;
AvailableStationIdentifiers = availableStationIdentifiers;
SelectedStationIdentifier = selectedStationIdentifier;
}

[RelayCommand]
async Task Confirm()
{
try
{
await _mediator.Send(new SendConnectionRequestRequest(Callsign, SelectedStationIdentifier));
_windowHandle.Close();
}
catch (Exception e)
{
_errorReporter.ReportError(e);
}
}

[RelayCommand]
void Cancel()
{
_windowHandle.Close();
}
}
75 changes: 75 additions & 0 deletions source/CPDLCPlugin/Windows/ConnectionRequestView.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<UserControl x:Class="CPDLCPlugin.Windows.ConnectionRequestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cpdlcPlugin="clr-namespace:CPDLCPlugin"
xmlns:viewModels="clr-namespace:CPDLCPlugin.ViewModels"
Background="{x:Static cpdlcPlugin:Theme.BackgroundColor}"
d:DataContext="{d:DesignInstance Type=viewModels:ConnectionRequestViewModel}"
mc:Ignorable="d"
Width="400">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/CPDLCPlugin;component/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>

<Style TargetType="Button" BasedOn="{StaticResource ButtonBaseStyle}">
<Setter Property="Height" Value="32"/>
<Setter Property="MinWidth" Value="80"/>
</Style>

<Style TargetType="TextBlock" BasedOn="{StaticResource GenericTextStyle}" />
</ResourceDictionary>
</UserControl.Resources>

<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<TextBlock Grid.Row="0" Margin="0,0,0,8" TextWrapping="Wrap">
<Run Text="Send connection request to "/>
<Run Text="{Binding Callsign, Mode=OneWay}"/>
</TextBlock>

<!-- ATSU -->
<Grid Grid.Row="1" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<TextBlock Grid.Column="0" Text="ATSU" Margin="0,0,8,0"/>
<ComboBox Grid.Column="1"
ItemsSource="{Binding AvailableStationIdentifiers}"
SelectedItem="{Binding SelectedStationIdentifier}"/>
</Grid>

<!-- Buttons -->
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<Button
Grid.Column="0"
Content="Send"
Command="{Binding ConfirmCommand}"
HorizontalAlignment="Stretch"
Margin="0,0,4,0"/>

<Button
Grid.Column="1"
Content="Cancel"
Command="{Binding CancelCommand}"
HorizontalAlignment="Stretch"
Margin="4,0,0,0"/>
</Grid>
</Grid>
</UserControl>
13 changes: 13 additions & 0 deletions source/CPDLCPlugin/Windows/ConnectionRequestView.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Windows.Controls;
using CPDLCPlugin.ViewModels;

namespace CPDLCPlugin.Windows;

public partial class ConnectionRequestView : UserControl
{
public ConnectionRequestView(ConnectionRequestViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
1 change: 1 addition & 0 deletions source/CPDLCPlugin/Windows/WindowKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public static class WindowKeys
public const string CurrentMessages = nameof(CurrentMessages);
public const string History = nameof(History);
public const string ADSContract = nameof(ADSContract);
public const string ConnectionRequest = nameof(ConnectionRequest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using CPDLCServer.Handlers;
using CPDLCServer.Messages;
using CPDLCServer.Model;
using CPDLCServer.Tests.Mocks;
using Serilog.Core;

namespace CPDLCServer.Tests.Handlers;

public class SendConnectionRequestCommandHandlerTests
{
[Fact]
public async Task Handle_SendsConnectionRequestUplink()
{
// Arrange
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();
messageIdProvider.SetNextId(5);
var clock = new TestClock();

var handler = new SendConnectionRequestCommandHandler(clientManager, messageIdProvider, clock, Logger.None);

var command = new SendConnectionRequestCommand("UAL123", "YBBB");

// Act
await handler.Handle(command, CancellationToken.None);

// Assert
var client = (TestAcarsClient)await clientManager.GetAcarsClient("hoppies-ybbb", CancellationToken.None);
var uplink = Assert.Single(client.SentMessages);
Assert.Equal("UAL123", uplink.Recipient);
Assert.Equal("CONNECTION REQUESTED @YBBB@", uplink.Content);
Assert.Equal(5, uplink.MessageId);
Assert.Null(uplink.MessageReference);
Assert.Equal(CpdlcUplinkResponseType.NoResponse, uplink.ResponseType);
Assert.Equal(clock.UtcNow(), uplink.Sent);
}

[Fact]
public async Task Handle_SendsUplinkViaClientForSelectedStation()
{
// The uplink must go through the ACARS client of the selected ATSU, not another client.
// Arrange
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();
var clock = new TestClock();

var handler = new SendConnectionRequestCommandHandler(clientManager, messageIdProvider, clock, Logger.None);

var command = new SendConnectionRequestCommand("UAL123", "YMMM");

// Act
await handler.Handle(command, CancellationToken.None);

// Assert
var ymmmClient = (TestAcarsClient)await clientManager.GetAcarsClient("hoppies-ymmm", CancellationToken.None);
var ybbbClient = (TestAcarsClient)await clientManager.GetAcarsClient("hoppies-ybbb", CancellationToken.None);

var uplink = Assert.Single(ymmmClient.SentMessages);
Assert.Equal("CONNECTION REQUESTED @YMMM@", uplink.Content);
Assert.Empty(ybbbClient.SentMessages);
}

[Fact]
public async Task Handle_MatchesStationIdIgnoringCase()
{
// Arrange
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();
var clock = new TestClock();

var handler = new SendConnectionRequestCommandHandler(clientManager, messageIdProvider, clock, Logger.None);

var command = new SendConnectionRequestCommand("UAL123", "ybbb");

// Act
await handler.Handle(command, CancellationToken.None);

// Assert
var client = (TestAcarsClient)await clientManager.GetAcarsClient("hoppies-ybbb", CancellationToken.None);
Assert.Single(client.SentMessages);
}

[Fact]
public async Task Handle_ThrowsWhenNoClientForStation()
{
// Arrange
var clientManager = new TestClientManager();
var messageIdProvider = new TestMessageIdProvider();
var clock = new TestClock();

var handler = new SendConnectionRequestCommandHandler(clientManager, messageIdProvider, clock, Logger.None);

var command = new SendConnectionRequestCommand("UAL123", "YPPP");

// Act & Assert
await Assert.ThrowsAnyAsync<Exception>(() => handler.Handle(command, CancellationToken.None));

var ybbbClient = (TestAcarsClient)await clientManager.GetAcarsClient("hoppies-ybbb", CancellationToken.None);
var ymmmClient = (TestAcarsClient)await clientManager.GetAcarsClient("hoppies-ymmm", CancellationToken.None);
Assert.Empty(ybbbClient.SentMessages);
Assert.Empty(ymmmClient.SentMessages);
}
}
10 changes: 10 additions & 0 deletions source/CPDLCServer.Tests/Mocks/TestClientManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ public Task<AcarsConfiguration> GetConfigurationForStation(string stationId, Can
return Task.FromResult(configuration);
}

public Task<string?> FindAcarsClientId(string stationId, CancellationToken cancellationToken)
{
var acarsClientId = _clients
.Where(c => string.Equals(c.Value.StationId, stationId, StringComparison.OrdinalIgnoreCase))
.Select(c => c.Key)
.FirstOrDefault();

return Task.FromResult(acarsClientId);
}

public bool ClientExists(string acarsClientId)
{
return _clients.ContainsKey(acarsClientId);
Expand Down
Loading
Loading