Add the session, radio control, bandmap and cluster
Nonemm.Session holds what the operator is typing, what the log says about it and what happens on Enter, with no UI toolkit behind it. Nonemm.Rig talks to hamlib's rigctld and reconnects on its own. Nonemm.Spotting reads DX cluster lines into a bandmap that drops spots after an hour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
77
src/Nonemm.Spotting/SpotLine.cs
Normal file
77
src/Nonemm.Spotting/SpotLine.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Globalization;
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Spotting;
|
||||
|
||||
/// Reads the `DX de` lines a cluster node sends.
|
||||
public static class SpotLine
|
||||
{
|
||||
private const string Marker = "DX de ";
|
||||
|
||||
/// Null for any other traffic from the node, which the packet window shows
|
||||
/// as it arrived.
|
||||
public static Spot? Parse(string line, DateTime nowUtc)
|
||||
{
|
||||
int start = line.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (start < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string body = line[(start + Marker.Length)..];
|
||||
int colon = body.IndexOf(':');
|
||||
if (colon < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string spotter = body[..colon].Trim();
|
||||
string[] parts = body[(colon + 1)..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (!double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double kilohertz))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string comment = string.Join(' ', parts.Skip(2)).Trim();
|
||||
return new Spot(
|
||||
Callsign.Parse(parts[1]),
|
||||
Frequency.FromKilohertz(kilohertz),
|
||||
ParseTime(comment, nowUtc),
|
||||
SpotSource.Cluster,
|
||||
spotter,
|
||||
TrimTime(comment));
|
||||
}
|
||||
|
||||
/// The node puts the spot's time at the end as `1234Z`.
|
||||
private static DateTime ParseTime(string comment, DateTime nowUtc)
|
||||
{
|
||||
string? stamp = TimeStamp(comment);
|
||||
if (stamp is null ||
|
||||
!int.TryParse(stamp[..2], out int hour) ||
|
||||
!int.TryParse(stamp[2..4], out int minute))
|
||||
{
|
||||
return nowUtc;
|
||||
}
|
||||
DateTime at = new(nowUtc.Year, nowUtc.Month, nowUtc.Day, hour, minute, 0, DateTimeKind.Utc);
|
||||
// a spot timed later than now came in just before midnight
|
||||
return at > nowUtc.AddMinutes(5) ? at.AddDays(-1) : at;
|
||||
}
|
||||
|
||||
private static string TrimTime(string comment)
|
||||
{
|
||||
string? stamp = TimeStamp(comment);
|
||||
return stamp is null ? comment : comment[..^stamp.Length].TrimEnd();
|
||||
}
|
||||
|
||||
private static string? TimeStamp(string comment)
|
||||
{
|
||||
string trimmed = comment.TrimEnd();
|
||||
if (trimmed.Length < 5 || char.ToUpperInvariant(trimmed[^1]) != 'Z')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string candidate = trimmed[^5..];
|
||||
return candidate[..4].All(char.IsAsciiDigit) ? candidate : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user