From db3572fd04e7839973d54e68df6517427f33ab44 Mon Sep 17 00:00:00 2001 From: KC3PIB Date: Sun, 10 Jul 2022 21:56:09 -0400 Subject: [PATCH] Add QSO logging, better highlight logic, migrate to QSO parser in WsjtxUtils.Messages relates #1 --- README.md | 22 +- .../LoggedQsoManager.cs | 179 +++++++++++++ .../PskReporter/PskReporterUtils.cs | 9 +- .../ReceptionReportState.cs | 17 +- .../ReceptionReporter.cs | 62 +++-- .../Searchlight.cs | 236 +++++++++--------- .../SearchlightClientState.cs | 33 ++- .../Settings/ColorSettings.cs | 10 +- .../Settings/LoggedQsosSettings.cs | 42 ++++ .../Settings/SearchlightSettings.cs | 21 +- src/WsjtxUtils.Searchlight.Common/Utils.cs | 49 +++- .../Wsjtx/QsoState.cs | 43 ---- .../Wsjtx/WsjtxQso.cs | 117 --------- .../Wsjtx/WsjtxQsoParser.cs | 108 -------- .../WsjtxUtils.Searchlight.Common.csproj | 4 +- .../WsjtxUtils.Searchlight.Console.csproj | 1 - .../config.json | 15 +- 17 files changed, 518 insertions(+), 450 deletions(-) create mode 100644 src/WsjtxUtils.Searchlight.Common/LoggedQsoManager.cs create mode 100644 src/WsjtxUtils.Searchlight.Common/Settings/LoggedQsosSettings.cs delete mode 100644 src/WsjtxUtils.Searchlight.Common/Wsjtx/QsoState.cs delete mode 100644 src/WsjtxUtils.Searchlight.Common/Wsjtx/WsjtxQso.cs delete mode 100644 src/WsjtxUtils.Searchlight.Common/Wsjtx/WsjtxQsoParser.cs diff --git a/README.md b/README.md index 9044516..5feb320 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A .NET 6 console application that highlights callsigns within [WSJT-X](https://p WjtxUtils.Searchlight will download reception reports from PSK Reporter for the callsigns of any connected WSJT-X client. These reception reports are then correlated with recent decodes and highlighted within WSJT-X. The background color of the highlighted callsign is mapped to the range of received reception report SNR values and used to indicate the relative strength of the received signal. -When a logged QSO occurs, that callsign will be highlighted for the duration of the application. There is no persistence of logged QSOs, and all highlights are cleared on application exit. +When a logged QSO occurs, that callsign will be highlighted for the duration of the application and can be optionally logged to a file to allow for tracking logged QSOs between sessions. There is a countdown period when the application starts until requesting the first reception report, which defaults to 5 minutes. This period is an excellent time to call CQ for a while to seed the initial reception report. @@ -52,13 +52,14 @@ To use 3rd party software ([GridTracker](https://gridtracker.org/grid-tracker/)) "Port": 2237 } ``` -The color options used for reception reports and logged QSOs are altered through the Palette section. ```ReceptionReportBackgroundColors``` is a list of colors, [a gradient](https://colordesigner.io/gradient-generator), used as the background color for reception reports. The first color in the list represents the weakest SNR report values and the last the strongest SNR values. ```ReceptionReportForegroundColor``` controls the color used for highlighted text (callsigns). Both ```ContactedBackgroundColor``` and ```ContactedForegroundColor``` are used for logged QSOs. +The color options used for reception reports and logged QSOs are altered through the Palette section. ```ReceptionReportBackgroundColors``` is a list of colors, [a gradient](https://colordesigner.io/gradient-generator), used as the background color for reception reports. The first color in the list represents the weakest SNR report values and the last the strongest SNR values. ```ReceptionReportForegroundColor``` controls the color used for highlighted text (callsigns). Both ```ContactedBackgroundColor``` and ```ContactedForegroundColor``` are used for logged QSOs. ```HighlightCallsignsPeriodSeconds``` is the period at which callsigns will be correlated and highlighted based on the current reception report. ```json "Palette": { "ReceptionReportBackgroundColors": [ "#114397", "#453f99", "#653897", "#812e91", "#991f87", "#ae027a", "#be006a", "#cb0058", "#d30044", "#d7002e" ], "ReceptionReportForegroundColor": "#ffff00", "ContactedBackgroundColor": "#000000", - "ContactedForegroundColor": "#ffff00" + "ContactedForegroundColor": "#ffff00", + "HighlightCallsignsPeriodSeconds": 5 } ``` Reception report options are altered through the ```PskReporter``` section. ```ReportWindowSeconds``` is a negative number in seconds to indicate how much data to retrieve. This value cannot be more than 24 hours and defaults to -900 seconds or the previous 15 minutes, which should be enough data for current band conditions. ```ReportRetrievalPeriodSeconds``` controls how often reception reports are retrieved. Philip from [PSK Reporter](https://pskreporter.info/) has asked to limit requests to once every five minutes. IMHO Philip does a considerable service to the ham radio community with this data, don't abuse it. @@ -68,6 +69,13 @@ Reception report options are altered through the ```PskReporter``` section. ```R "ReportRetrievalPeriodSeconds": 300 } ``` +The ```LoggedQsos``` section allows adding an optional log file to maintain logged QSOs across searchlight sessions. Set a ```LogFilePath``` to enable QSO logging. ```QsoManagerBehavior``` controls how logged QSOs are highlighted, once per band or once per band and mode. +```json +"LoggedQsos": { + "LogFilePath": "qsolog.txt", + "QsoManagerBehavior": "OncePerBand" + } +``` Console and file logging output is controlled through the ```Serilog``` section. Please see the [Serilog documentation](https://github.com/serilog/serilog-settings-configuration) for details. ```json "Serilog": { @@ -79,14 +87,6 @@ Console and file logging output is controlled through the ```Serilog``` section. "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {NewLine}{Exception}" } - }, - { - "Name": "File", - "Args": { - "path": "searchlight-log.txt", - "rollingInterval": "Day", - "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {NewLine}{Exception}" - } } ] } diff --git a/src/WsjtxUtils.Searchlight.Common/LoggedQsoManager.cs b/src/WsjtxUtils.Searchlight.Common/LoggedQsoManager.cs new file mode 100644 index 0000000..9952b78 --- /dev/null +++ b/src/WsjtxUtils.Searchlight.Common/LoggedQsoManager.cs @@ -0,0 +1,179 @@ +using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; +using WsjtxUtils.Searchlight.Common.Settings; +using WsjtxUtils.WsjtxMessages.Messages; +using WsjtxUtils.WsjtxMessages.QsoParsing; + +namespace WsjtxUtils.Searchlight.Common +{ + /// + /// Logged QSO manager behavior + /// + public enum QsoManagerBehavior + { + OncePerBand, + OncePerBandAndMode, + } + + public class LoggedQsoManager + { + /// + /// Semaphore to manage log access + /// + private readonly SemaphoreSlim _qsoLogSemaphore = new SemaphoreSlim(1, 1); + + /// + /// Settings that alter behavior + /// + private readonly LoggedQsoManagerSettings _settings; + + /// + /// Platform specific newline characters + /// + private readonly ReadOnlyMemory _newlineBuffer; + + /// + /// Logged QSO's key by band + /// + private readonly ConcurrentDictionary> _loggedQso = new ConcurrentDictionary>(); + + /// + /// + /// + /// + public LoggedQsoManager(LoggedQsoManagerSettings loggedQsoManagerSettings) + { + _settings = loggedQsoManagerSettings; + _newlineBuffer = new ReadOnlyMemory(Encoding.UTF8.GetBytes(Environment.NewLine)); + } + + /// + /// Provides a list of previsouly contacted stations that are not highlighted already + /// + /// + /// + /// + public IEnumerable GetPreviouslyContactedStationsNotHiglighted(SearchlightClientState clientState) + { + if (clientState.Status == null) + throw new ArgumentNullException(nameof(clientState.Status)); + + string client = clientState.Id; + string callsign = clientState.Status.DECall; + string mode = clientState.Status.Mode; + ulong band = Utils.ApproximateBandFromFrequency(clientState.Status.DialFrequencyInHz); + + return clientState.DecodedStations + .WherePreviouslyContacted(GetQsosLogged(band, callsign, mode)) + .WhereNotPreviouslyHiglighted(clientState) + .Select(s => s.Value); + } + + /// + /// Get all logged QSOs for the band and mode and specified callsign + /// + /// + /// + /// + /// + public IEnumerable GetQsosLogged(ulong band, string callsign, string mode) + { + if (!_loggedQso.ContainsKey(band)) + return new List(); + + var qsosForCallsign = _loggedQso[band].WhereDECallsign(callsign); + + if (_settings.QsoManagerBehavior == QsoManagerBehavior.OncePerBand) + return qsosForCallsign; + + return qsosForCallsign.WhereMode(mode); + } + + /// + /// Log a message to the log file + /// + /// + /// + /// + public async Task WriteQsoToLogFileAsync(QsoLogged qsoLogged, CancellationToken cancellationToken = default) + { + // log the message to the local cache + QsosLoggedForBand(qsoLogged.TXFrequencyInHz).Add(qsoLogged); + + // check for a log file + if (string.IsNullOrEmpty(_settings.LogFilePath)) + return; + + // log the message to disk + await _qsoLogSemaphore.WaitAsync(); + try + { + using(var stream = File.Open(_settings.LogFilePath, FileMode.Append)) + { + // write the object + await JsonSerializer.SerializeAsync(stream, + qsoLogged, + new JsonSerializerOptions { WriteIndented = false }, + cancellationToken); + + // write a newline + await stream.WriteAsync(_newlineBuffer, cancellationToken); + } + } + finally + { + _qsoLogSemaphore.Release(); + } + } + + /// + /// Load all logged QSOs from the log file + /// + /// + /// + /// + public async Task ReadAllQsosFromLogFileAsync(CancellationToken cancellationToken = default) + { + await _qsoLogSemaphore.WaitAsync(); + try + { + using (TextReader r = new StreamReader(_settings.LogFilePath)) + { + string? line; + while ((line = await r.ReadLineAsync()) != null) + { + QsoLogged ? qso = JsonSerializer.Deserialize(line); + if (qso == null) + continue; + + QsosLoggedForBand(qso.TXFrequencyInHz).Add(qso); + } + } + } + finally + { + _qsoLogSemaphore.Release(); + } + } + + /// + /// Fetch the list of qso's for a given band + /// + /// + /// + private ConcurrentBag QsosLoggedForBand(ulong frequencyInHertz) + { + var band = Utils.ApproximateBandFromFrequency(frequencyInHertz); + + return _loggedQso.AddOrUpdate(band, (qsoBag) => + { + return new ConcurrentBag(); + }, + (band, qsoBag) => + { + return qsoBag; + }); + } + } +} diff --git a/src/WsjtxUtils.Searchlight.Common/PskReporter/PskReporterUtils.cs b/src/WsjtxUtils.Searchlight.Common/PskReporter/PskReporterUtils.cs index ad51bfb..62fc937 100644 --- a/src/WsjtxUtils.Searchlight.Common/PskReporter/PskReporterUtils.cs +++ b/src/WsjtxUtils.Searchlight.Common/PskReporter/PskReporterUtils.cs @@ -22,11 +22,10 @@ public static class PskReporterUtils Log.Debug("HTTP Get {url}.", url); var response = await httpClient.GetAsync(url, cancellationToken); - Log.Debug("Request complete: {url} {code} {headers}", url, response.StatusCode, response.Headers); + Log.Debug("Request complete: {url} {code}", url, response.StatusCode); response.EnsureSuccessStatusCode(); - Log.Debug("Deserialize reception reports"); var serializer = new XmlSerializer(typeof(PskReceptionReports)); using var reader = new XmlTextReader(await response.Content.ReadAsStreamAsync(cancellationToken)); return (PskReceptionReports?)serializer.Deserialize(reader); @@ -46,11 +45,7 @@ private static HttpClient CreateHttpClientWithDecompressionSupport() if (handler.SupportsAutomaticDecompression) handler.AutomaticDecompression = DecompressionMethods.All; - var client = new HttpClient(handler); - - //client.DefaultRequestHeaders.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("searchlight","0.1")); - - return client; + return new HttpClient(handler); } } } diff --git a/src/WsjtxUtils.Searchlight.Common/ReceptionReportState.cs b/src/WsjtxUtils.Searchlight.Common/ReceptionReportState.cs index 4a9b4e9..0c513a1 100644 --- a/src/WsjtxUtils.Searchlight.Common/ReceptionReportState.cs +++ b/src/WsjtxUtils.Searchlight.Common/ReceptionReportState.cs @@ -3,7 +3,7 @@ namespace WsjtxUtils.Searchlight.Common { /// - /// Recpetion report state + /// Reception report state /// public class ReceptionReportState { @@ -58,9 +58,24 @@ public ReceptionReportState(DateTime timestamp, PskReceptionReports? receptionRe /// public int Retry { get; set; } + /// + /// Time in seconds for exponential backoff + /// + public double Backoff { get; set; } + /// /// Has the report been updated before a refresh? /// public bool IsDirty { get; set; } + + /// + /// Handle exponential backoff on retry + /// + /// + public void RetryWithExponentialBackoff(double seconds = 30) + { + Retry++; + Backoff = seconds * Math.Pow(2, Retry + Random.Shared.NextDouble()); // simple backoff with jitter + } } } diff --git a/src/WsjtxUtils.Searchlight.Common/ReceptionReporter.cs b/src/WsjtxUtils.Searchlight.Common/ReceptionReporter.cs index 3965ad4..fcdf449 100644 --- a/src/WsjtxUtils.Searchlight.Common/ReceptionReporter.cs +++ b/src/WsjtxUtils.Searchlight.Common/ReceptionReporter.cs @@ -11,6 +11,8 @@ public class ReceptionReporter private const int ExponentialBackoffSeconds = 30; private const int MaxRetries = 3; + private readonly SemaphoreSlim _reportRequestSemaphore = new SemaphoreSlim(1, 1); + /// /// Global dictionary for reception reports keyed by DE callsign /// @@ -36,7 +38,7 @@ public ReceptionReporter(PskReporterSettings pskReporterSettings) /// /// /// - public bool TryGetStateForCallsign(string callsign, out ReceptionReportState? receptionReportState) + public bool TryGetReceptionReportState(string callsign, out ReceptionReportState? receptionReportState) { if (_receptionReports.ContainsKey(callsign)) { @@ -77,27 +79,43 @@ public bool TryRemoveCallsign(string callsign) /// Gather reception reports for all monitored callsigns /// /// - public async Task GatherReceptionReportsAsync(CancellationToken cancellationToken = default) + public async Task PeriodicallyGatherReceptionReportsAsync(CancellationToken cancellationToken = default) { - foreach (var callsign in _receptionReports.Keys) - { - // check if a new report need to be queried - if (!ShouldQueryReport(callsign, _receptionReports[callsign])) - continue; - - try + using (var timer = new PeriodicTimer(TimeSpan.FromSeconds(1))) + while (await timer.WaitForNextTickAsync(cancellationToken)) { - await GatherReceptionReportsForCallsignAsync(callsign, cancellationToken); + foreach (var callsign in _receptionReports.Keys) + { + var receptionReportsForCallsignState = _receptionReports[callsign]; + + // check if a new report needs to be queried + if (!ShouldQueryReport(callsign, receptionReportsForCallsignState) || _reportRequestSemaphore.CurrentCount < 1) + continue; + + await _reportRequestSemaphore.WaitAsync(); + try + { + await GatherReceptionReportsForCallsignAsync(callsign, cancellationToken); + } + catch (HttpRequestException htttpRequestException) + { + Log.Warning("Report for {callsign} failed with {code} {message}, retry {retry}", callsign, htttpRequestException.StatusCode, htttpRequestException.Message, receptionReportsForCallsignState.Retry); + + if (htttpRequestException.StatusCode != HttpStatusCode.ServiceUnavailable && receptionReportsForCallsignState.Retry > MaxRetries) + throw; + + receptionReportsForCallsignState.RetryWithExponentialBackoff(ExponentialBackoffSeconds); + } + catch(Exception exception) + { + Log.Error(exception, "An error ocurred."); + } + finally + { + _reportRequestSemaphore.Release(); + } + } } - catch (HttpRequestException htttpRequestException) - { - if (htttpRequestException.StatusCode != HttpStatusCode.ServiceUnavailable && _receptionReports[callsign].Retry > MaxRetries) - throw; - - Log.Warning("Report for {callsign} failed with {code} {message}, retry {retry}", callsign, htttpRequestException.StatusCode, htttpRequestException.Message, _receptionReports[callsign].Retry); - _receptionReports[callsign].Retry++; - } - } } /// @@ -108,8 +126,7 @@ public async Task GatherReceptionReportsAsync(CancellationToken cancellationToke private bool ShouldQueryReport(string callsign, ReceptionReportState state) { var secondsElasped = (DateTime.UtcNow - state.ReportTimestamp).TotalSeconds; - var backoffInSeconds = (state.Retry > 0) ? ExponentialBackoffSeconds * Math.Pow(2, state.Retry + Random.Shared.NextDouble()) : 0; - var intervalInSeconds = _pskReporterSettings.ReportRetrievalPeriodSeconds + backoffInSeconds; + var intervalInSeconds = _pskReporterSettings.ReportRetrievalPeriodSeconds + state.Backoff; if (secondsElasped > intervalInSeconds) { @@ -117,7 +134,7 @@ private bool ShouldQueryReport(string callsign, ReceptionReportState state) return true; } - Log.Information("Reception report update for {callsign} in {seconds} seconds", callsign, Convert.ToInt32(intervalInSeconds - secondsElasped)); + Log.Verbose("Reception report update for {callsign} in {seconds} seconds", callsign, Convert.ToInt32(intervalInSeconds - secondsElasped)); return false; } @@ -156,6 +173,7 @@ private async Task GatherReceptionReportsForCallsignAsync(string callsign, Cance reportState.ReceptionReports = report; reportState.ReportTimestamp = DateTime.UtcNow; reportState.Retry = 0; + reportState.Backoff = 0; reportState.IsDirty = true; return reportState; }); diff --git a/src/WsjtxUtils.Searchlight.Common/Searchlight.cs b/src/WsjtxUtils.Searchlight.Common/Searchlight.cs index 66ad8b2..3b27155 100644 --- a/src/WsjtxUtils.Searchlight.Common/Searchlight.cs +++ b/src/WsjtxUtils.Searchlight.Common/Searchlight.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Drawing; using System.Net; +using System.Text.Json; using WsjtxUtils.Searchlight.Common.Settings; using WsjtxUtils.WsjtxMessages.Messages; using WsjtxUtils.WsjtxUdpServer; @@ -21,11 +22,6 @@ public class Searchlight : WsjtxUdpServerBaseAsyncMessageHandler /// private readonly ConcurrentDictionary _searchlightClients = new ConcurrentDictionary(); - /// - /// Logged QSO's key by band - /// - private readonly ConcurrentDictionary> _loggedQso = new ConcurrentDictionary>(); - /// /// Reception reports /// @@ -36,6 +32,11 @@ public class Searchlight : WsjtxUdpServerBaseAsyncMessageHandler /// private readonly WsjtxUdpServer.WsjtxUdpServer _server; + /// + /// QSO Logger + /// + private readonly LoggedQsoManager _loggedQsos; + /// /// Create a searchlight /// @@ -45,6 +46,14 @@ public Searchlight(SearchlightSettings settings) _settings = settings; _receptionReporter = new ReceptionReporter(settings.PskReporter); _server = new WsjtxUdpServer.WsjtxUdpServer(this, IPAddress.Parse(_settings.Server.Address), _settings.Server.Port); + _loggedQsos = new LoggedQsoManager(_settings.LoggedQsos); + + // load previouslt logged Qsos + Task.Run(async () => await _loggedQsos.ReadAllQsosFromLogFileAsync()); + + ClientConnectedCallback = (client) => { Log.Information("Client {server} connected", client.ClientId); return Task.CompletedTask; }; + ClientClosedCallback = (client) => { Log.Information("Client {server} disconnected", client.ClientId); return Task.CompletedTask; }; + ClientExpiredCallback = (client) => { Log.Information("Client {server} expired", client.ClientId); return Task.CompletedTask; }; } /// @@ -59,32 +68,26 @@ public async Task RunAsync(CancellationTokenSource cancellationTokenSource) try { - while (!cancellationTokenSource.IsCancellationRequested) - { - cancellationTokenSource.Token.ThrowIfCancellationRequested(); - - // gather reception reports - await _receptionReporter.GatherReceptionReportsAsync(cancellationTokenSource.Token); - - // highlight callsigns - await HighlightBasedOnReceptionReportAsync(_server, cancellationTokenSource.Token); - - // delay - await Task.Delay(5000, cancellationTokenSource.Token); - } + Task.WaitAll(_receptionReporter.PeriodicallyGatherReceptionReportsAsync(cancellationTokenSource.Token), + HighlightBasedOnReceptionReportAsync(_server, cancellationTokenSource.Token)); } - catch (OperationCanceledException) + catch (AggregateException aggregateException) { - } - catch (Exception ex) - { - Log.Error(ex, "An exception occured."); + aggregateException.Handle((innerException) => + { + if (innerException is not TaskCanceledException) + { + Log.Error(innerException, "An exception occured."); + return false; + } + return true; + }); } finally { // clear highlights on connected clients foreach (var client in ConnectedClients.Keys) - await ClearAllHighlightedCallsignsForClientAsync(_server, client); + await ClearAllHighlightedCallsignsForClientAsync(client); Log.Information("Stopping server at {server}:{port}", _settings.Server.Address, _settings.Server.Port); _server.Stop(); @@ -104,10 +107,11 @@ public override async Task HandleQsoLoggedMessageAsync(WsjtxUdpServer.WsjtxUdpSe { await base.HandleQsoLoggedMessageAsync(server, message, endPoint, cancellationToken); - QsosLoggedListForBand(message.TXFrequencyInHz).Add(message); + // log the QSO + await _loggedQsos.WriteQsoToLogFileAsync(message); // send highlight message for the DX callsign - await HighlightCallsignAsync(server, message.Id, message.DXCall, _settings.Palette.ContactedBackgroundColor, _settings.Palette.ContactedForegroundColor); + await HighlightCallsignAsync(message.Id, message.DXCall, _settings.Palette.ContactedBackgroundColor, _settings.Palette.ContactedForegroundColor); } /// @@ -138,6 +142,10 @@ public override async Task HandleStatusMessageAsync(WsjtxUdpServer.WsjtxUdpServe { await base.HandleStatusMessageAsync(server, message, endPoint, cancellationToken); + // set the status for the specified WSJT-X client + ClientStateFor(message.Id).Status = message; + + // add the WSJT-X client DE callsign to the reception reporter _receptionReporter.TryAddCallsign(message.DECall); } #endregion @@ -153,109 +161,106 @@ private SearchlightClientState ClientStateFor(string clientId) // get the correct list for this client return _searchlightClients.AddOrUpdate(clientId, (wsjtxClient) => { - return new SearchlightClientState(ConnectedClients[clientId].Status); + return new SearchlightClientState(clientId); }, (wsjtxClient, searchlightClient) => { - searchlightClient.Status = _searchlightClients[clientId].Status; return searchlightClient; }); } /// - /// Fetch the list of qso's for a given band + /// Highlight callsigns based on reception reports /// - /// + /// + /// /// - private ConcurrentBag QsosLoggedListForBand(ulong frequencyInHertz) + private async Task HighlightBasedOnReceptionReportAsync(WsjtxUdpServer.WsjtxUdpServer server, CancellationToken cancellationToken = default) { - var band = Utils.ApproximateBandFromFrequency(frequencyInHertz); - - return _loggedQso.AddOrUpdate(band, (qsoBag) => - { - return new ConcurrentBag(); - }, - (band, qsoBag) => - { - return qsoBag; - }); + using (var timer = new PeriodicTimer(TimeSpan.FromSeconds(_settings.Palette.HighlightCallsignsPeriodSeconds))) + while (await timer.WaitForNextTickAsync(cancellationToken)) + { + // get all clients that have a status + foreach (var kvp in ConnectedClients.WhereValidStatus()) + { + // determine the callsign being used for this client and check if there are reception + // report for this callsign, current band, and mode + string client = kvp.Key; + string callsign = kvp.Value.Status!.DECall; + string mode = kvp.Value.Status.Mode; + ulong band = Utils.ApproximateBandFromFrequency(kvp.Value.Status.DialFrequencyInHz); + + // check for any logged QSO's that are being decoded but haven't been highlighted + await CheckAndHighlightLoggedQsosAsync(ClientStateFor(client), cancellationToken); + + Log.Verbose("Checking for reception reports for {callsign} with WSJT-X client '{client}' on {band} {mode}", callsign, client, kvp.Value.Status.DialFrequencyInHz, mode); + if (!_receptionReporter.TryGetReceptionReportState(callsign, out ReceptionReportState? receptionReportState) || + receptionReportState == null || + receptionReportState.ReceptionReports == null || + receptionReportState.ReceptionReports.Reports == null || + !receptionReportState.ReceptionReports.Reports.WhereBand(band).WhereMode(mode).Any()) + { + Log.Verbose("No reception reports for {callsign} with WSJT-X client '{client}' on {band} {mode}", callsign, client, kvp.Value.Status.DialFrequencyInHz, mode); + continue; + } + + var reportsForBandAndMode = receptionReportState.ReceptionReports.Reports.WhereBand(band).WhereMode(mode); + var reportSnrMin = reportsForBandAndMode.Min(r => r.SNR); + var reportSnrMax = reportsForBandAndMode.Max(r => r.SNR); + + // if this is a new report, cleanup previous highlights + if (receptionReportState.IsDirty) + { + receptionReportState.IsDirty = false; + + // clear all previous highlights + await ClearAllHighlightedCallsignsForClientAsync(client, cancellationToken); + + // highlight logged qsos + await CheckAndHighlightLoggedQsosAsync(ClientStateFor(client), cancellationToken); + } + + Log.Verbose("Checking {count} reports for {callsign} on '{client}' {band} {mode}", reportsForBandAndMode.Count(), callsign, client, kvp.Value.Status.DialFrequencyInHz, mode); + + // find all heard stations that haven't already been contacted/higlighted and pull the reception reports for that station if it exists. + foreach (var heardStation in ClientStateFor(client).DecodedStations + .WhereNotPreviouslyContacted(_loggedQsos.GetQsosLogged(band, callsign, mode)) + .WhereNotPreviouslyHiglighted(ClientStateFor(client)) + .Select(s => s.Key)) + { + // check for a reception report for the heard station + var receptionReport = receptionReportState.ReceptionReports.Reports + .WhereReceiverCallsign(heardStation) + .WhereBand(band) + .WhereMode(mode) + .FirstOrDefault(); + + if (receptionReport == null) + continue; + + // colorize based on the signal report + int colorIndex = Utils.Scale(receptionReport.SNR, 1, _settings.Palette.ReceptionReportBackgroundColors.Count(), reportSnrMin, reportSnrMax) - 1; + Log.Information("Highlighting {station} {snr} with color {index} on WSJT-X client '{client}' - {mode}", heardStation, receptionReport.SNR, colorIndex, client, mode); + await HighlightCallsignAsync(client, heardStation, _settings.Palette.ReceptionReportBackgroundColors[colorIndex], _settings.Palette.ReceptionReportForegroundColor, cancellationToken); + } + } + } } /// - /// Highlight callsigns based on reception reports + /// Check and highlight any logged QSO's /// /// + /// + /// /// /// - private async Task HighlightBasedOnReceptionReportAsync(WsjtxUdpServer.WsjtxUdpServer server, CancellationToken cancellationToken = default) + private async Task CheckAndHighlightLoggedQsosAsync(SearchlightClientState clientState, CancellationToken cancellationToken) { - // get all clients that have a status - foreach (var kvp in ConnectedClients.WhereValidStatus()) + foreach (var decode in _loggedQsos.GetPreviouslyContactedStationsNotHiglighted(clientState)) { - // determine the callsign being used for this client and check if there are reception - // report for this callsign, current band, and mode - string client = kvp.Key; - string callsign = kvp.Value.Status!.DECall; - string mode = kvp.Value.Status.Mode; - ulong band = Utils.ApproximateBandFromFrequency(kvp.Value.Status.DialFrequencyInHz); - - Log.Verbose("Checking for reception reports for {callsign} with WSJT-X client '{client}' on {band} {mode}", callsign, client, kvp.Value.Status.DialFrequencyInHz, mode); - - if (!_receptionReporter.TryGetStateForCallsign(callsign, out ReceptionReportState? receptionReportState) || - receptionReportState == null || - receptionReportState.ReceptionReports == null || - receptionReportState.ReceptionReports.Reports == null || - !receptionReportState.ReceptionReports.Reports.WhereBand(band).WhereMode(mode).Any()) - { - Log.Verbose("No reception reports for {callsign} with WSJT-X client '{client}' on {band} {mode}", callsign, client, kvp.Value.Status.DialFrequencyInHz, mode); - continue; - } - - var reportsForBandAndMode = receptionReportState.ReceptionReports.Reports.WhereBand(band).WhereMode(mode); - var reportSnrMin = reportsForBandAndMode.Min(r => r.SNR); - var reportSnrMax = reportsForBandAndMode.Max(r => r.SNR); - - // get a list of qso's for the current callsign, band, and mode - IEnumerable loggedQso = _loggedQso.ContainsKey(band) ? _loggedQso[band].WhereDECallsign(callsign).WhereMode(mode).ToList() : new List(); - - // if this is a new report, clear previous highlights - if (receptionReportState.IsDirty) - { - Log.Verbose("Dirty reception report"); - receptionReportState.IsDirty = false; - - // clear previous highlights - await ClearAllHighlightedCallsignsForClientAsync(_server, client, cancellationToken); - - // highlight logged qsos - Log.Verbose("Highlight previous QSO's"); - foreach (var qso in loggedQso) - await HighlightCallsignAsync(server, client, qso.DXCall, _settings.Palette.ContactedBackgroundColor, _settings.Palette.ContactedForegroundColor, cancellationToken); - } - - Log.Information("Checking {count} reports for {callsign} on '{client}' {band} {mode}", reportsForBandAndMode.Count(), callsign, client, kvp.Value.Status.DialFrequencyInHz, mode); - - // find all heard stations that haven't already been contacted/higlighted and pull the reception reports for that station if it exists. - foreach (var heardStation in ClientStateFor(client).DecodedStations - .WhereNotPreviouslyContacted(loggedQso) - .WhereNotPreviouslyHiglighted(ClientStateFor(client).HighlightedCallsigns.Keys) - .Select(s => s.Key)) - { - // check for a reception report for the heard station - var receptionReport = receptionReportState.ReceptionReports.Reports - .WhereReceiverCallsign(heardStation) - .WhereBand(band) - .WhereMode(mode) - .FirstOrDefault(); - - if (receptionReport == null) - continue; - - // colorize based on the signal report - int colorIndex = Utils.Scale(receptionReport.SNR, 1, _settings.Palette.ReceptionReportBackgroundColors.Count(), reportSnrMin, reportSnrMax) - 1; - Log.Information("Highlighting {station} {snr} with color {index} on WSJT-X client '{client}'", heardStation, receptionReport.SNR, colorIndex, client); - await HighlightCallsignAsync(server, client, heardStation, _settings.Palette.ReceptionReportBackgroundColors[colorIndex], _settings.Palette.ReceptionReportForegroundColor, cancellationToken); - } + Log.Information("Highlight logged QSO with {callsign} on WSJT-X client '{client}'", decode.DECallsign, clientState.Id); + await HighlightCallsignAsync(clientState.Id, decode.DECallsign, _settings.Palette.ContactedBackgroundColor, _settings.Palette.ContactedForegroundColor, cancellationToken); } } @@ -265,11 +270,10 @@ private async Task HighlightBasedOnReceptionReportAsync(WsjtxUdpServer.WsjtxUdpS /// /// /// - private async Task ClearAllHighlightedCallsignsForClientAsync(WsjtxUdpServer.WsjtxUdpServer server, string client, CancellationToken cancellationToken = default) + private async Task ClearAllHighlightedCallsignsForClientAsync(string client, CancellationToken cancellationToken = default) { - Log.Information("Clearing highlighted callsigns on WSJT-X client '{client}'", client); foreach (var callsign in ClientStateFor(client).HighlightedCallsigns.Keys) - await ClearHighlightedCallsignAsync(server, client, callsign, cancellationToken); + await ClearHighlightedCallsignAsync(client, callsign, cancellationToken); } /// @@ -279,9 +283,9 @@ private async Task ClearAllHighlightedCallsignsForClientAsync(WsjtxUdpServer.Wsj /// /// /// - private async Task ClearHighlightedCallsignAsync(WsjtxUdpServer.WsjtxUdpServer server, string client, string callsign, CancellationToken cancellationToken = default) + private async Task ClearHighlightedCallsignAsync(string client, string callsign, CancellationToken cancellationToken = default) { - await HighlightCallsignAsync(server, client, callsign, Color.Empty, Color.Empty, cancellationToken); + await HighlightCallsignAsync(client, callsign, Color.Empty, Color.Empty, cancellationToken); } /// @@ -293,7 +297,7 @@ private async Task ClearHighlightedCallsignAsync(WsjtxUdpServer.WsjtxUdpServer s /// /// /// - private async Task HighlightCallsignAsync(WsjtxUdpServer.WsjtxUdpServer server, string client, string callsign, Color background, Color foreground, CancellationToken cancellationToken = default) + private async Task HighlightCallsignAsync(string client, string callsign, Color background, Color foreground, CancellationToken cancellationToken = default) { HighlightCallsign message = new HighlightCallsign(client, callsign, background, foreground); @@ -302,7 +306,7 @@ private async Task HighlightCallsignAsync(WsjtxUdpServer.WsjtxUdpServer server, else ClientStateFor(client).AddHighlightedCallsign(message); - await server.SendMessageToAsync(ConnectedClients[client].Endpoint, message, cancellationToken); + await _server.SendMessageToAsync(ConnectedClients[client].Endpoint, message, cancellationToken); } #endregion } diff --git a/src/WsjtxUtils.Searchlight.Common/SearchlightClientState.cs b/src/WsjtxUtils.Searchlight.Common/SearchlightClientState.cs index a40a9af..44cca41 100644 --- a/src/WsjtxUtils.Searchlight.Common/SearchlightClientState.cs +++ b/src/WsjtxUtils.Searchlight.Common/SearchlightClientState.cs @@ -1,6 +1,6 @@ using System.Collections.Concurrent; -using WsjtxUtils.Searchlight.Common.Wsjtx; using WsjtxUtils.WsjtxMessages.Messages; +using WsjtxUtils.WsjtxMessages.QsoParsing; namespace WsjtxUtils.Searchlight.Common { @@ -9,17 +9,35 @@ namespace WsjtxUtils.Searchlight.Common /// public class SearchlightClientState { - public SearchlightClientState(Status? status) : this(status, new ConcurrentDictionary(), new ConcurrentDictionary()) + /// + /// Construct a searchlight client state + /// + /// + /// + public SearchlightClientState(string id, Status? status = null) : this(id, status, new ConcurrentDictionary(), new ConcurrentDictionary()) { } - public SearchlightClientState(Status? status, ConcurrentDictionary decodedStations, ConcurrentDictionary highlightedCallsigns) + /// + /// Construct a searchlight client state + /// + /// + /// + /// + /// + public SearchlightClientState(string id, Status? status, ConcurrentDictionary decodedStations, ConcurrentDictionary highlightedCallsigns) { + Id = id; Status = status; DecodedStations = decodedStations; HighlightedCallsigns = highlightedCallsigns; } + /// + /// WSJT-X client id + /// + public string Id { get; set; } + /// /// Status for the client /// @@ -57,11 +75,14 @@ public void RemoveHighlightedCallsign(HighlightCallsign message) /// /// Add a station that was decoded to the list /// - /// + /// /// - public void AddDecodedStation(Decode HighlightCallsign, int expiryInSeconds = 1800) + public void AddDecodedStation(Decode decode, int expiryInSeconds = 1800) { - var qso = WsjtxQsoParser.ParseDecode(HighlightCallsign); + if (Status == null || Status.Mode == string.Empty) + return; + + var qso = WsjtxQsoParser.ParseDecode(Status.Mode, decode); DecodedStations.AddOrUpdate(qso.DECallsign, (deCallsign) => { diff --git a/src/WsjtxUtils.Searchlight.Common/Settings/ColorSettings.cs b/src/WsjtxUtils.Searchlight.Common/Settings/ColorSettings.cs index aaf71f6..f54b14a 100644 --- a/src/WsjtxUtils.Searchlight.Common/Settings/ColorSettings.cs +++ b/src/WsjtxUtils.Searchlight.Common/Settings/ColorSettings.cs @@ -11,7 +11,7 @@ public class ColorSettings /// /// Searchlight color settings /// - public ColorSettings() : this(new List(), Color.Empty, Color.Empty, Color.Empty) + public ColorSettings() : this(new List(), Color.Empty, Color.Empty, Color.Empty, 5) { } @@ -22,12 +22,13 @@ public ColorSettings() : this(new List(), Color.Empty, Color.Empty, Color /// /// /// - public ColorSettings(List receptionReportBackgroundColors, Color receptionReportForegroundColor, Color contactedBackgroundColor, Color contactedForegroundColor) + public ColorSettings(List receptionReportBackgroundColors, Color receptionReportForegroundColor, Color contactedBackgroundColor, Color contactedForegroundColor, int updateHighlightedCallsignsSeconds = 5) { ReceptionReportBackgroundColors = receptionReportBackgroundColors; ReceptionReportForegroundColor = receptionReportForegroundColor; ContactedBackgroundColor = contactedBackgroundColor; ContactedForegroundColor = contactedForegroundColor; + HighlightCallsignsPeriodSeconds = updateHighlightedCallsignsSeconds; } /// @@ -53,5 +54,10 @@ public ColorSettings(List receptionReportBackgroundColors, Color receptio /// [JsonConverter(typeof(HexadecimalColorJsonConverter))] public Color ContactedForegroundColor { get; set; } + + /// + /// The period to update highlighted callsigns in seconds + /// + public int HighlightCallsignsPeriodSeconds { get; set; } } } diff --git a/src/WsjtxUtils.Searchlight.Common/Settings/LoggedQsosSettings.cs b/src/WsjtxUtils.Searchlight.Common/Settings/LoggedQsosSettings.cs new file mode 100644 index 0000000..3e78c8c --- /dev/null +++ b/src/WsjtxUtils.Searchlight.Common/Settings/LoggedQsosSettings.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WsjtxUtils.Searchlight.Common.Settings +{ + /// + /// Logged QSO manager settings + /// + public class LoggedQsoManagerSettings + { + /// + /// Constructs a logged QSO manager settings object + /// + public LoggedQsoManagerSettings() : this(string.Empty, QsoManagerBehavior.OncePerBand) + { + } + + /// + /// Constructs a logged QSO manager settings object + /// + /// + /// + public LoggedQsoManagerSettings(string logFilePath, QsoManagerBehavior qsoManagerBehavior) + { + LogFilePath = logFilePath; + QsoManagerBehavior = qsoManagerBehavior; + } + + /// + /// Path to the logged QSO file + /// + public string LogFilePath { get; set; } + + /// + /// The behavior of the QSO manager + /// + public QsoManagerBehavior QsoManagerBehavior { get; set; } + } +} diff --git a/src/WsjtxUtils.Searchlight.Common/Settings/SearchlightSettings.cs b/src/WsjtxUtils.Searchlight.Common/Settings/SearchlightSettings.cs index 65724ad..2c7d74b 100644 --- a/src/WsjtxUtils.Searchlight.Common/Settings/SearchlightSettings.cs +++ b/src/WsjtxUtils.Searchlight.Common/Settings/SearchlightSettings.cs @@ -5,16 +5,26 @@ /// public class SearchlightSettings { - public SearchlightSettings() : this(new WsjtxServer(), new ColorSettings(), new PskReporterSettings()) + /// + /// Constructs a searchlight settings object + /// + public SearchlightSettings() : this(new WsjtxServer(), new ColorSettings(), new PskReporterSettings(), new LoggedQsoManagerSettings()) { - } - public SearchlightSettings(WsjtxServer server, ColorSettings palette, PskReporterSettings pskReporter) + /// + /// Constructs a searchlight settings object + /// + /// + /// + /// + /// + public SearchlightSettings(WsjtxServer server, ColorSettings palette, PskReporterSettings pskReporter, LoggedQsoManagerSettings loggedQsos) { Server = server; Palette = palette; PskReporter = pskReporter; + LoggedQsos = loggedQsos; } /// @@ -31,5 +41,10 @@ public SearchlightSettings(WsjtxServer server, ColorSettings palette, PskReporte /// PSKReporter settings /// public PskReporterSettings PskReporter { get; set; } + + /// + /// Logged QSO settings + /// + public LoggedQsoManagerSettings LoggedQsos { get; set; } } } diff --git a/src/WsjtxUtils.Searchlight.Common/Utils.cs b/src/WsjtxUtils.Searchlight.Common/Utils.cs index 2738ba6..c6fd436 100644 --- a/src/WsjtxUtils.Searchlight.Common/Utils.cs +++ b/src/WsjtxUtils.Searchlight.Common/Utils.cs @@ -1,5 +1,6 @@ using WsjtxUtils.Searchlight.Common.PskReporter; using WsjtxUtils.WsjtxMessages.Messages; +using WsjtxUtils.WsjtxMessages.QsoParsing; using WsjtxUtils.WsjtxUdpServer; namespace WsjtxUtils.Searchlight.Common @@ -74,6 +75,17 @@ public static IEnumerable WhereBand(this IEnumerable report.Frequency != 0 && Utils.ApproximateBandFromFrequency(report.Frequency) == band); } + /// + /// Where logged qso band is + /// + /// + /// + /// + public static IEnumerable WhereBand(this IEnumerable enumerable, ulong band) + { + return enumerable.Where(qso => Utils.ApproximateBandFromFrequency(qso.TXFrequencyInHz) == band); + } + /// /// Where reception report mode /// @@ -101,20 +113,53 @@ public static IEnumerable> WhereValid /// /// /// - public static IEnumerable> WhereNotPreviouslyContacted(this IEnumerable> enumerable, IEnumerable previousQsos) + public static IEnumerable> WhereNotPreviouslyContacted(this IEnumerable> enumerable, IEnumerable previousQsos) { return enumerable.Where(kvp => !previousQsos.Any(logged => kvp.Key == logged.DXCall)); } + /// + /// Where previously contacted + /// + /// + /// + /// + public static IEnumerable> WherePreviouslyContacted(this IEnumerable> enumerable, IEnumerable previousQsos) + { + return enumerable.Where(kvp => previousQsos.Any(logged => kvp.Key == logged.DXCall)); + } + /// /// Where not previously highlighted /// /// /// /// - public static IEnumerable> WhereNotPreviouslyHiglighted(this IEnumerable> enumerable, IEnumerable highlightedCalls) + public static IEnumerable> WhereNotPreviouslyHiglighted(this IEnumerable> enumerable, IEnumerable highlightedCalls) { return enumerable.Where(kvp => !highlightedCalls.Any(call => call == kvp.Value.DECallsign)); } + + /// + /// Where not previously highlighted + /// + /// + /// + /// + public static IEnumerable> WhereNotPreviouslyHiglighted(this IEnumerable> enumerable, SearchlightClientState clientState) + { + return enumerable.WhereNotPreviouslyHiglighted(clientState.HighlightedCallsigns.Keys); + } + + /// + /// Where not previously highlighted + /// + /// + /// + /// + public static IEnumerable WhereNotPreviouslyHiglighted(this IEnumerable enumerable, IEnumerable highlightedCalls) + { + return enumerable.Where(qso => !highlightedCalls.Any(call => call == qso.DXCall)); + } } } diff --git a/src/WsjtxUtils.Searchlight.Common/Wsjtx/QsoState.cs b/src/WsjtxUtils.Searchlight.Common/Wsjtx/QsoState.cs deleted file mode 100644 index 893f61f..0000000 --- a/src/WsjtxUtils.Searchlight.Common/Wsjtx/QsoState.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace WsjtxUtils.Searchlight.Common.Wsjtx -{ - /// - /// The state of the currently decoded QSO - /// - public enum QsoState - { - /// - /// The QSO is in an unknown state - /// - Unknown = 0, - /// - /// Reply to a station's CQ - /// - /// Corresponds with TX 1 - CallingStation = 1, - /// - /// Sending a signal report - /// - /// Corresponds with TX 2 - Report = 2, - /// - /// Sending a roger signal report - /// - /// Corresponds with TX 3 - RogerReport = 3, - /// - /// Sending a roger signal report - /// - /// Corresponds with TX 4 - Rogers = 4, - /// - /// Sending 73 - /// - /// Corresponds with TX 5 - Signoff = 5, - /// - /// Calling CQ - /// - /// Corresponds with TX 6 - CallingCq = 6 - } -} diff --git a/src/WsjtxUtils.Searchlight.Common/Wsjtx/WsjtxQso.cs b/src/WsjtxUtils.Searchlight.Common/Wsjtx/WsjtxQso.cs deleted file mode 100644 index f8d2a19..0000000 --- a/src/WsjtxUtils.Searchlight.Common/Wsjtx/WsjtxQso.cs +++ /dev/null @@ -1,117 +0,0 @@ -using WsjtxUtils.WsjtxMessages.Messages; - -namespace WsjtxUtils.Searchlight.Common.Wsjtx -{ - /// - /// A decoded WSJT-X QSO - /// - public class WsjtxQso - { - /// - /// Decoded WSJT-X QSO - /// - /// - public WsjtxQso(Decode decode) : this(decode, string.Empty, string.Empty, string.Empty, string.Empty) - { - - } - - /// - /// Decoded WSJT-X QSO - /// - /// - /// - /// - /// - /// - public WsjtxQso(Decode decode, string callingModifier, string dxCallsign, string deCallsign, string gridSquare) - { - RawMessage = decode.Message; - Time = DateTime.UtcNow.Date.AddSeconds(decode.Time / 1000); - QsoState = QsoState.Unknown; - Mode = decode.Mode; - Snr = decode.Snr; - OffsetTimeSeconds = decode.OffsetTimeSeconds; - OffsetFrequencyHz = decode.OffsetFrequencyHz; - LowConfidence = decode.LowConfidence; - CallingModifier = callingModifier; - DXCallsign = dxCallsign; - DECallsign = deCallsign; - GridSquare = gridSquare; - } - - /// - /// The date and time the message was received by the local station - /// - public DateTime Time { get; set; } - - /// - /// The WSJT-X mode - /// - public string Mode { get; set; } - - /// - /// The signal to noise from the remote to local station - /// - public int Snr { get; set; } - - /// - /// The time difference from the remote to local station - /// - public float OffsetTimeSeconds { get; set; } - - /// - /// The frequency delta of the remote station - /// - public uint OffsetFrequencyHz { get; set; } - - /// - /// Low confidence decodes are flagged in protocols where the decoder - /// has knows that a decode has a higher than normal probability - /// of being false, they should not be reported on publicly - /// accessible services without some attached warning or further validation. - /// - public bool LowConfidence { get; set; } - - /// - /// State of the current QSO - /// - public QsoState QsoState { get; set; } - - /// - /// Was priori information used to complete this call - /// - /// https://www.physics.princeton.edu/pulsar/K1JT/wsjtx-doc/wsjtx-main-2.5.4.html#_ap_decoding - public bool UsedPriori { get; set; } - - /// - /// Is the station calling CQ - /// - public bool IsCallingCQ { get => QsoState == QsoState.CallingCq; } - - /// - /// The CQ calling modifier - /// - public string CallingModifier { get; set; } - - /// - /// The gridsquare of the calling station - /// - public string GridSquare { get; set; } - - /// - /// The local callsign - /// - public string DECallsign { get; set; } - - /// - /// The remote callsign - /// - public string DXCallsign { get; set; } - - /// - /// The raw decode message - /// - public string RawMessage { get; set; } - } -} diff --git a/src/WsjtxUtils.Searchlight.Common/Wsjtx/WsjtxQsoParser.cs b/src/WsjtxUtils.Searchlight.Common/Wsjtx/WsjtxQsoParser.cs deleted file mode 100644 index 2d37709..0000000 --- a/src/WsjtxUtils.Searchlight.Common/Wsjtx/WsjtxQsoParser.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Text.RegularExpressions; -using WsjtxUtils.WsjtxMessages.Messages; - -namespace WsjtxUtils.Searchlight.Common.Wsjtx -{ - /// - /// WSJT-X decode message QSO parser - /// - public static class WsjtxQsoParser - { - /// - /// Attempt to parse as much information as possible about the OSO of a WSJT-X decode packet - /// - /// - /// - public static WsjtxQso ParseDecode(Decode decode) - { - // initialize the result object and extract the 'parts' - // of the raw message and normalize for parsing - WsjtxQso result = new WsjtxQso(decode); - var parts = decode.Message - .Split(' ', StringSplitOptions.RemoveEmptyEntries) - .Where(part => part != "?") - .ToArray(); - var length = parts.Length; - - // if there is less than two parts, it's a message in a - // unknown state that can't be parsed - if (length < 2) - return result; - - // setup the values and objects needed to decode the message - var deCallSignIndex = 1; - var dxCallSignIndex = 0; - var gridSquareIndex = -1; - string callingModifier = string.Empty; - bool usesPriori = Regex.Match(parts.Last(), "a[1-6]").Success; - - // Check if the station CQ - if (parts[0] == "CQ" || parts[0] == "QRZ") - { - result.QsoState = QsoState.CallingCq; - dxCallSignIndex = -1; - - // Check if the CQ call has a modifer (DX, POTA, TEST, etc.) - if (length == 4 && !usesPriori || length == 5 && usesPriori) - { - callingModifier = parts[1]; - deCallSignIndex++; - } - - // Find the grid if included with the CQ call - var offset = (usesPriori) ? 2 : 1; - if (deCallSignIndex + offset < length) - gridSquareIndex = deCallSignIndex + 1; - } - // not calling CQ and more than two or three parts - else if (length > (usesPriori ? 3 : 2)) - { - var lastPartIndex = length - (usesPriori ? 2 : 1); - var targetPart = parts[lastPartIndex]; - - if (Regex.Match(targetPart, @"R[+-][\d]+").Success) - { - result.QsoState = QsoState.RogerReport; - } - else if (Regex.Match(targetPart, @"[+-][\d]+").Success) - { - result.QsoState = QsoState.Report; - } - else if (targetPart == "RRR" || targetPart == "RR73") - { - result.QsoState = QsoState.Rogers; - } - else if (targetPart == "73") - { - result.QsoState = QsoState.Signoff; - } - else if (Regex.Match(targetPart, @"[A-Z]{2}[\d]{2}([A-Za-z]{2})?").Success) - { - result.QsoState = QsoState.CallingStation; - gridSquareIndex = lastPartIndex; - } - else - { - deCallSignIndex = dxCallSignIndex = -1; - } - } - // Grab the callsign and gridsquare - result.CallingModifier = callingModifier; - result.UsedPriori = usesPriori; - - result.DXCallsign = (dxCallSignIndex == -1) - ? string.Empty - : parts[dxCallSignIndex].Replace("<", "").Replace(">", ""); - - result.DECallsign = (deCallSignIndex == -1) - ? string.Empty - : parts[deCallSignIndex].Replace("<", "").Replace(">", ""); - - result.GridSquare = (gridSquareIndex == -1) - ? string.Empty - : parts[gridSquareIndex]; - - return result; - } - } -} diff --git a/src/WsjtxUtils.Searchlight.Common/WsjtxUtils.Searchlight.Common.csproj b/src/WsjtxUtils.Searchlight.Common/WsjtxUtils.Searchlight.Common.csproj index 27f6f85..dfa3661 100644 --- a/src/WsjtxUtils.Searchlight.Common/WsjtxUtils.Searchlight.Common.csproj +++ b/src/WsjtxUtils.Searchlight.Common/WsjtxUtils.Searchlight.Common.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/WsjtxUtils.Searchlight.Console/WsjtxUtils.Searchlight.Console.csproj b/src/WsjtxUtils.Searchlight.Console/WsjtxUtils.Searchlight.Console.csproj index 751df5e..d06f9ac 100644 --- a/src/WsjtxUtils.Searchlight.Console/WsjtxUtils.Searchlight.Console.csproj +++ b/src/WsjtxUtils.Searchlight.Console/WsjtxUtils.Searchlight.Console.csproj @@ -20,7 +20,6 @@ - diff --git a/src/WsjtxUtils.Searchlight.Console/config.json b/src/WsjtxUtils.Searchlight.Console/config.json index 3ca0018..1c151cf 100644 --- a/src/WsjtxUtils.Searchlight.Console/config.json +++ b/src/WsjtxUtils.Searchlight.Console/config.json @@ -7,12 +7,17 @@ "ReceptionReportBackgroundColors": [ "#114397", "#453f99", "#653897", "#812e91", "#991f87", "#ae027a", "#be006a", "#cb0058", "#d30044", "#d7002e" ], "ReceptionReportForegroundColor": "#ffff00", "ContactedBackgroundColor": "#000000", - "ContactedForegroundColor": "#ffff00" + "ContactedForegroundColor": "#ffff00", + "HighlightCallsignsPeriodSeconds": 5 }, "PskReporter": { "ReportWindowSeconds": -900, "ReportRetrievalPeriodSeconds": 300 }, + "LoggedQsos": { + "LogFilePath": "searchlight-qso.log", + "QsoManagerBehavior": "OncePerBand" + }, "Serilog": { "MinimumLevel": "Information", "WriteTo": [ @@ -22,14 +27,6 @@ "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {NewLine}{Exception}" } - }, - { - "Name": "File", - "Args": { - "path": "searchlight-log.txt", - "rollingInterval": "Day", - "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {NewLine}{Exception}" - } } ] }