Add project files.

master
Tom 2019-07-21 18:59:51 +01:00
rodzic 32816f40af
commit 3bd300635a
5 zmienionych plików z 302880 dodań i 0 usunięć

25
ft8spotter.sln 100644
Wyświetl plik

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ft8spotter", "ft8spotter\ft8spotter.csproj", "{C85CCBC8-5FA8-44A7-8AC0-C31CF99DDAAD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C85CCBC8-5FA8-44A7-8AC0-C31CF99DDAAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C85CCBC8-5FA8-44A7-8AC0-C31CF99DDAAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C85CCBC8-5FA8-44A7-8AC0-C31CF99DDAAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C85CCBC8-5FA8-44A7-8AC0-C31CF99DDAAD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {422B7EC7-DA35-43ED-9699-670971F0ACDF}
EndGlobalSection
EndGlobal

Wyświetl plik

@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace ft8spotter
{
public class ClublogCtyXml
{
internal const string ClublogNamespace = "https://clublog.org/cty/v1.2";
public DateTime Updated { get; set; }
public Entity[] Entities { get; set; }
public ExceptionRecord[] Exceptions { get; set; }
public Prefix[] Prefixes { get; set; }
public InvalidOperation[] InvalidOperations { get; set; }
public ZoneException[] ZoneExceptions { get; set; }
public static ClublogCtyXml Parse(string xml)
{
var result = new ClublogCtyXml();
XDocument xDocument = XDocument.Parse(xml);
result.Entities = Fetch(xDocument, "entities", "entity", (xe) => Entity.Parse(xe));
result.Exceptions = Fetch(xDocument, "exceptions", "exception", (xe) => ExceptionRecord.Parse(xe));
result.InvalidOperations = Fetch(xDocument, "invalid_operations", "invalid", (xe) => InvalidOperation.Parse(xe));
result.Prefixes = Fetch(xDocument, "prefixes", "prefix", (xe) => Prefix.Parse(xe));
result.ZoneExceptions = Fetch(xDocument, "zone_exceptions", "zone_exception", (xe) => ZoneException.Parse(xe));
result.Updated = DateTime.Parse(xDocument.Root.Attribute("date").Value);
return result;
}
private static T[] Fetch<T>(XDocument xDocument, string parent, string elementName, Func<XElement, T> parse) => xDocument.Element(XName.Get("clublog", ClublogNamespace))
.Descendants(XName.Get(parent, ClublogNamespace))
.Descendants(XName.Get(elementName, ClublogNamespace))
.Select(parse)
.ToArray();
internal static bool? GetNullableBool(XElement xe, string v)
{
var s = GetString(xe, v);
if (s == null)
{
return null;
}
if (s == "FALSE")
return false;
if (s == "TRUE")
return true;
throw new NotImplementedException(v);
}
internal static DateTime? GetNullableDateTime(XElement xe, string v)
{
var dt = GetString(xe, v);
if (dt == null)
{
return null;
}
return DateTime.Parse(dt);
}
internal static bool GetBool(XElement xe, string v)
{
string s = GetString(xe, v);
if (s == "FALSE")
return false;
if (s == "TRUE")
return true;
throw new NotImplementedException(v);
}
internal static string GetString(XElement xe, string elementName)
{
return xe.Element(XName.Get(elementName, ClublogCtyXml.ClublogNamespace))?.Value;
}
internal static int? GetNullableInt(XElement xe, string v)
{
var s = GetString(xe, v);
if (s == null)
{
return null;
}
return int.Parse(s);
}
internal static double? GetNullableDouble(XElement xe, string v)
{
var s = GetString(xe, v);
if (s == null)
{
return null;
}
return double.Parse(s);
}
}
public class ZoneException
{
public int Record { get; set; }
public string Call { get; set; }
public int Zone { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
internal static ZoneException Parse(XElement xe)
{
var result = new ZoneException
{
Record = int.Parse(xe.Attribute("record").Value),
Call = ClublogCtyXml.GetString(xe, "call"),
Zone = int.Parse(ClublogCtyXml.GetString(xe, "zone")),
End = ClublogCtyXml.GetNullableDateTime(xe, "end"),
Start = ClublogCtyXml.GetNullableDateTime(xe, "start"),
};
return result;
}
}
public class InvalidOperation
{
public int Record { get; set; }
public string Call { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
internal static InvalidOperation Parse(XElement xe)
{
var result = new InvalidOperation
{
Record = int.Parse(xe.Attribute("record").Value),
Call = ClublogCtyXml.GetString(xe, "call"),
End = ClublogCtyXml.GetNullableDateTime(xe, "end"),
Start = ClublogCtyXml.GetNullableDateTime(xe, "start"),
};
return result;
}
}
public class Entity
{
public int Adif { get; set; }
public string Name { get; set; }
public string Prefix { get; set; }
public bool Deleted { get; set; }
public int CqZone { get; set; }
public string Continent { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
public bool? Whitelist { get; set; }
public DateTime? WhitelistStart { get; set; }
public DateTime? WhitelistEnd { get; set; }
internal static Entity Parse(XElement xe)
{
var result = new Entity
{
Adif = int.Parse(ClublogCtyXml.GetString(xe, "adif")),
Continent = ClublogCtyXml.GetString(xe, "cont"),
CqZone = int.Parse(ClublogCtyXml.GetString(xe, "cqz")),
Deleted = ClublogCtyXml.GetBool(xe, "deleted"),
End = ClublogCtyXml.GetNullableDateTime(xe, "end"),
Start = ClublogCtyXml.GetNullableDateTime(xe, "start"),
Latitude = double.Parse(ClublogCtyXml.GetString(xe, "lat")),
Longitude = double.Parse(ClublogCtyXml.GetString(xe, "long")),
Name = ClublogCtyXml.GetString(xe, "name"),
Prefix = ClublogCtyXml.GetString(xe, "prefix"),
Whitelist = ClublogCtyXml.GetNullableBool(xe, "whitelist"),
WhitelistEnd = ClublogCtyXml.GetNullableDateTime(xe, "whitelist_end"),
WhitelistStart = ClublogCtyXml.GetNullableDateTime(xe, "whitelist_start"),
};
return result;
}
}
public class ExceptionRecord
{
public int Record { get; set; }
public string Call { get; set; }
public string Entity { get; set; }
public int Adif { get; set; }
public int CqZone { get; set; }
public string Continent { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
internal static ExceptionRecord Parse(XElement xe)
{
return new ExceptionRecord
{
Record = int.Parse(xe.Attribute("record").Value),
Call = ClublogCtyXml.GetString(xe, "call"),
End = ClublogCtyXml.GetNullableDateTime(xe, "end"),
Start = ClublogCtyXml.GetNullableDateTime(xe, "start"),
Adif = int.Parse(ClublogCtyXml.GetString(xe, "adif")),
Continent = ClublogCtyXml.GetString(xe, "cont"),
CqZone = int.Parse(ClublogCtyXml.GetString(xe, "cqz")),
Entity = ClublogCtyXml.GetString(xe, "entity"),
Latitude = double.Parse(ClublogCtyXml.GetString(xe, "lat")),
Longitude = double.Parse(ClublogCtyXml.GetString(xe, "long")),
};
}
}
public class Prefix
{
public int Record { get; set; }
public string Call { get; set; }
public string Entity { get; set; }
public int? Adif { get; set; }
public int? CqZone { get; set; }
public string Continent { get; set; }
public double? Longitude { get; set; }
public double? Latitude { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
internal static Prefix Parse(XElement xe)
{
return new Prefix
{
Record = int.Parse(xe.Attribute("record").Value),
Call = ClublogCtyXml.GetString(xe, "call"),
End = ClublogCtyXml.GetNullableDateTime(xe, "end"),
Start = ClublogCtyXml.GetNullableDateTime(xe, "start"),
Adif = ClublogCtyXml.GetNullableInt(xe, "adif"),
Continent = ClublogCtyXml.GetString(xe, "cont"),
CqZone = ClublogCtyXml.GetNullableInt(xe, "cqz"),
Entity = ClublogCtyXml.GetString(xe, "entity"),
Latitude = ClublogCtyXml.GetNullableDouble(xe, "lat"),
Longitude = ClublogCtyXml.GetNullableDouble(xe, "long"),
};
}
}
}

Wyświetl plik

@ -0,0 +1,331 @@
using Dapper;
using MySql.Data.MySqlClient;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ft8spotter
{
class Program
{
private const string configKey = "cloudlog_connection_string";
static ClublogCtyXml ctyXml;
static void Main(string[] args)
{
if (args.Any(a => a == "--help" || a == "-h" || a == "/?"))
{
Console.WriteLine(@"A work in progress, that listens to udp://localhost:2237 for WSJT-X, works out the DXCC entity of every call
heard using Clublog's cty.xml, then queries a Cloudlog MySQL database directly (because there's no API yet)
to see if it's a needed slot. If it is, it highlights the call in red in the console window.");
return;
}
if (!File.Exists(configFile))
{
Console.WriteLine("Cloudlog connection string?");
string cs = Console.ReadLine();
File.WriteAllText(configFile, $"{configKey}={cs}");
}
ctyXml = ClublogCtyXml.Parse(File.ReadAllText("cty.xml"));
using (var client = new UdpClient(2237, AddressFamily.InterNetwork))
{
var sw = Stopwatch.StartNew();
while (true)
{
var ipep = new IPEndPoint(IPAddress.Loopback, 0);
byte[] msg = client.Receive(ref ipep);
if (msg[11] == 0x02)
{
string heardCall = GetHeardCall(msg);
if (heardCall == null)
continue;
var entity = GetEntity(heardCall);
if (sw.Elapsed > TimeSpan.FromSeconds(5))
{
Console.WriteLine("------------------------------------------------------");
sw.Restart();
}
bool needed = entity == null ? false : GetNeeded(20, entity.Adif);
var colBefore = Console.ForegroundColor;
if (needed)
{
Console.ForegroundColor = ConsoleColor.Red;
}
Console.WriteLine($"{heardCall} - {entity?.Entity ?? "unknown"}");
Console.ForegroundColor = colBefore;
}
}
}
}
static string configFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ft8spotter", "config");
static string cs;
static IDbConnection GetConnection()
{
if (cs == null)
{
var configSettings = File.ReadAllLines(configFile)
.Where(line => line.Contains("="))
.Select(line => new { key = line.Substring(0, line.IndexOf("=")), value = line.Substring(line.IndexOf("=") + 1) })
.Where(kvp => !String.IsNullOrWhiteSpace(kvp.key) && !String.IsNullOrWhiteSpace(kvp.value))
.ToDictionary(line => line.key, line => line.value);
cs = configSettings[configKey];
}
return new MySqlConnection(cs);
}
private static bool GetNeeded(int band, int? adif)
{
if (!adif.HasValue)
return false;
int dxcc = adif.Value;
using (var conn = GetConnection())
{
conn.Open();
// and col_mode = @mode
return 0 == conn.ExecuteScalar<int>("select count(1) from TABLE_HRD_CONTACTS_V01 where col_dxcc=@dxcc and COL_BAND = @band", new { mode = "FT8", band = $"{band}M", dxcc });
}
}
private static MatchingEntity GetEntity(string heardCall)
{
var possibleExceptions = ctyXml.Exceptions.Where(c => String.Equals(heardCall, c.Call, StringComparison.OrdinalIgnoreCase));
foreach (var possibleException in possibleExceptions)
{
if (possibleException.Start.HasValue)
{
if (possibleException.Start.Value.ToUniversalTime() > DateTime.UtcNow)
{
continue;
}
else
{
// matches
}
}
if (possibleException.End.HasValue)
{
if (possibleException.End.Value.ToUniversalTime() < DateTime.UtcNow)
{
continue;
}
else
{
// matches
}
}
return new MatchingEntity
{
Call = possibleException.Call,
Adif = possibleException.Adif,
Continent = possibleException.Continent,
CqZone = possibleException.CqZone,
Entity = possibleException.Entity,
Latitude = possibleException.Latitude,
Longitude = possibleException.Longitude,
};
}
var fragments = GetFragments(heardCall);
foreach (var callFragement in fragments)
{
var possiblePrefixes = ctyXml.Prefixes.Where(p => String.Equals(callFragement, p.Call, StringComparison.OrdinalIgnoreCase));
foreach (var possiblePrefix in possiblePrefixes)
{
if (possiblePrefix.Start.HasValue)
{
if (possiblePrefix.Start.Value.ToUniversalTime() > DateTime.UtcNow)
{
continue;
}
else
{
// matches
}
}
if (possiblePrefix.End.HasValue)
{
if (possiblePrefix.End.Value.ToUniversalTime() < DateTime.UtcNow)
{
continue;
}
else
{
// matches
}
}
return new MatchingEntity
{
Call = possiblePrefix.Call,
Adif = possiblePrefix.Adif,
Continent = possiblePrefix.Continent,
CqZone = possiblePrefix.CqZone,
Entity = possiblePrefix.Entity,
Latitude = possiblePrefix.Latitude,
Longitude = possiblePrefix.Longitude,
};
}
}
return null;
}
private static IEnumerable<string> GetFragments(string call)
{
for (int i = call.Length; i > 0; i--)
{
yield return call.Substring(0, i);
}
}
static string GetHeardCall(byte[] msg)
{
/*int cur = 0;
foreach (var batch in msg.Batch(8))
{
Console.Write(cur.ToString("00") + " ");
foreach (var b in batch)
{
string bytestr = b.ToString("X").ToLower();
if (bytestr.Length == 1)
{
bytestr = "0" + bytestr;
}
Console.Write(bytestr + " ");
}
Console.WriteLine();
Console.Write(" ");
foreach (var b in batch)
{
char ch = (char)b;
if (Char.IsLetterOrDigit(ch) || Char.IsPunctuation(ch) || Char.IsSymbol(ch) || (ch == ' '))
{
Console.Write(ch);
Console.Write(" ");
}
}
Console.WriteLine();
Console.WriteLine();
cur += 8;
}*/
string text;
try
{
text = Encoding.ASCII.GetString(msg.Skip(52).SkipLast(2).ToArray());
}
catch (Exception)
{
return null;
}
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
string[] split = text.Split(' ');
string heard;
if (split.Length == 0)
{
heard = null;
}
else if (split.Length == 1)
{
heard = split[0];
}
else if (split.Length == 2)
{
heard = split[split.Length - 1];
}
else if (split.Length == 3)
{
heard = split[split.Length - 2];
}
else if (split.Length == 4)
{
heard = split[split.Length - 2];
}
else
{
heard = null;
}
if (heard != null)
{
heard = heard.Replace("<", "").Replace(">", "");
}
return heard;
}
}
internal class MatchingEntity
{
public string Call { get; set; }
public int? Adif { get; set; }
public string Continent { get; set; }
public int? CqZone { get; set; }
public string Entity { get; set; }
public double? Latitude { get; set; }
public double? Longitude { get; set; }
}
public static class Extensions
{
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
TSource[] bucket = null;
var count = 0;
foreach (var item in source)
{
if (bucket == null)
bucket = new TSource[size];
bucket[count++] = item;
if (count != size)
continue;
yield return bucket;
bucket = null;
count = 0;
}
if (bucket != null && count > 0)
yield return bucket.Take(count);
}
}
}

302241
ft8spotter/cty.xml 100644

Plik diff jest za duży Load Diff

Wyświetl plik

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="cty.xml" />
</ItemGroup>
<ItemGroup>
<Content Include="cty.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="1.60.6" />
<PackageReference Include="MySql.Data" Version="8.0.16" />
</ItemGroup>
</Project>