Add the Avalonia application and the station network
The entry window types contacts and colours them as they are typed; the log, check, bandmap, score and packet windows read the same session. Contacts are shared with the other stations of a multi-operator entry in N1MM's contact message, so an N1MM station on the same network sees them too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
105
src/Nonemm.Network/StationNetwork.cs
Normal file
105
src/Nonemm.Network/StationNetwork.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Network;
|
||||
|
||||
/// Keeps the stations of a multi-operator entry in step. Each contact is sent
|
||||
/// to every peer as it is logged, and contacts that arrive from a peer are
|
||||
/// handed to whoever owns the log.
|
||||
public sealed class StationNetwork : IDisposable
|
||||
{
|
||||
private readonly int port;
|
||||
private readonly string stationName;
|
||||
private readonly IReadOnlyList<IPEndPoint> peers;
|
||||
private readonly CancellationTokenSource stopping = new();
|
||||
private readonly UdpClient listener;
|
||||
private readonly UdpClient sender = new();
|
||||
private Task? loop;
|
||||
|
||||
public StationNetwork(int port, string stationName, IEnumerable<string> peerAddresses)
|
||||
{
|
||||
this.port = port;
|
||||
this.stationName = stationName;
|
||||
peers = peerAddresses.Select(a => Endpoint(a, port)).OfType<IPEndPoint>().ToList();
|
||||
listener = new UdpClient(new IPEndPoint(IPAddress.Any, port));
|
||||
sender.EnableBroadcast = true;
|
||||
}
|
||||
|
||||
public string StationName => stationName;
|
||||
|
||||
public IReadOnlyList<IPEndPoint> Peers => peers;
|
||||
|
||||
/// A contact another station logged. It arrives already scored; the log
|
||||
/// works its points and multipliers out again from the rules.
|
||||
public event EventHandler<Qso>? ContactArrived;
|
||||
|
||||
public event EventHandler<string>? Failed;
|
||||
|
||||
public void Start() => loop ??= Task.Run(() => ListenAsync(stopping.Token));
|
||||
|
||||
public async Task SendAsync(Qso qso, string myCallsign, CancellationToken cancellation = default)
|
||||
{
|
||||
byte[] message = Encoding.UTF8.GetBytes(ContactMessage.Write(qso, myCallsign, stationName));
|
||||
foreach (IPEndPoint peer in Destinations())
|
||||
{
|
||||
try
|
||||
{
|
||||
await sender.SendAsync(message, peer, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Failed?.Invoke(this, $"could not reach {peer}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
stopping.Cancel();
|
||||
listener.Dispose();
|
||||
sender.Dispose();
|
||||
stopping.Dispose();
|
||||
}
|
||||
|
||||
/// With no peers named, the contact goes out as a broadcast, which is how a
|
||||
/// station that has just joined is found without configuring anything.
|
||||
private IEnumerable<IPEndPoint> Destinations() =>
|
||||
peers.Count > 0 ? peers : [new IPEndPoint(IPAddress.Broadcast, port)];
|
||||
|
||||
private async Task ListenAsync(CancellationToken cancellation)
|
||||
{
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
UdpReceiveResult received = await listener.ReceiveAsync(cancellation).ConfigureAwait(false);
|
||||
Qso? qso = ContactMessage.Read(Encoding.UTF8.GetString(received.Buffer));
|
||||
if (qso is not null && qso.StationName != stationName)
|
||||
{
|
||||
ContactArrived?.Invoke(this, qso);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Failed?.Invoke(this, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IPEndPoint? Endpoint(string address, int defaultPort)
|
||||
{
|
||||
string[] parts = address.Split(':');
|
||||
if (!IPAddress.TryParse(parts[0].Trim(), out IPAddress? host))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
int port = parts.Length > 1 && int.TryParse(parts[1], out int given) ? given : defaultPort;
|
||||
return new IPEndPoint(host, port);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user