2517 lines
92 KiB
C#
2517 lines
92 KiB
C#
/*
|
|
* This file contains a modified version of the APRS Parser from APRSsharp:
|
|
* https://github.com/CBielstein/APRSsharp/tree/main/src/AprsParser
|
|
* The source of APRS Parser was retrieved and modified for use in skyscraper5 on 20.06.2022
|
|
*
|
|
* The following changes were made to the APRS Parser
|
|
* - (back-)ported from .NET 6 to .NET 5
|
|
* - Put everything into a single file.
|
|
* - Replaced Geocoordinate Portable with a custom implementation
|
|
* - Prevent Exception Throwing on some APRS Data Types.
|
|
* - Added support to raw APRS messages. (Data Type ":")
|
|
*
|
|
* Since APRS Parser is licensed under MIT, and skyscraper5 is as well, there should be no
|
|
* licensing issues.
|
|
*/
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using skyscraper5.Aprs.AprsSharp;
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
/// <summary>
|
|
/// Specifies whether a function is referring to latitude or longitude during Maidenhead gridsquare encode/decode.
|
|
/// </summary>
|
|
public enum CoordinateSystem
|
|
{
|
|
/// <summary>
|
|
/// Latitute.
|
|
/// </summary>
|
|
Latitude,
|
|
|
|
/// <summary>
|
|
/// Longitude.
|
|
/// </summary>
|
|
Longitude,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to specify format during decode of raw GPS data packets.
|
|
/// </summary>
|
|
public enum NmeaType
|
|
{
|
|
/// <summary>
|
|
/// Not yet decoded
|
|
/// </summary>
|
|
NotDecoded,
|
|
|
|
/// <summary>
|
|
/// GGA: Global Position System Fix Data
|
|
/// </summary>
|
|
GGA,
|
|
|
|
/// <summary>
|
|
/// GLL: Geographic Position, Latitude/Longitude Data
|
|
/// </summary>
|
|
GLL,
|
|
|
|
/// <summary>
|
|
/// RMC: Recommended Minimum Specific GPS/Transit Data
|
|
/// </summary>
|
|
RMC,
|
|
|
|
/// <summary>
|
|
/// VTG: Velocity and Track Data
|
|
/// </summary>
|
|
VTG,
|
|
|
|
/// <summary>
|
|
/// WPT: Way Point Location (also WPL)
|
|
/// </summary>
|
|
WPT,
|
|
|
|
/// <summary>
|
|
/// Not supported/known type
|
|
/// </summary>
|
|
Unknown,
|
|
}
|
|
|
|
/// <summary>
|
|
/// The APRS packet type.
|
|
/// </summary>
|
|
public enum PacketType
|
|
{
|
|
/// <summary>
|
|
/// Current Mic-E Data (Rev 0 beta)
|
|
/// </summary>
|
|
CurrentMicEData,
|
|
|
|
/// <summary>
|
|
/// Old Mic-E Data (Rev 0 beta)
|
|
/// </summary>
|
|
OldMicEData,
|
|
|
|
/// <summary>
|
|
/// Position without timestamp (no APRS messaging), or Ultimeter 2000 WX Station
|
|
/// </summary>
|
|
PositionWithoutTimestampNoMessaging,
|
|
|
|
/// <summary>
|
|
/// Peet Bros U-II Weather Station
|
|
/// </summary>
|
|
PeetBrosUIIWeatherStation,
|
|
|
|
/// <summary>
|
|
/// Raw GPS data or Ultimeter 2000
|
|
/// </summary>
|
|
RawGPSData,
|
|
|
|
/// <summary>
|
|
/// Agrelo DFJr / MicroFinder
|
|
/// </summary>
|
|
AgreloDFJrMicroFinder,
|
|
|
|
/// <summary>
|
|
/// [Reserved - Map Feature]
|
|
/// </summary>
|
|
MapFeature,
|
|
|
|
/// <summary>
|
|
/// Old Mic-E Data (but Current data for TM-D700)
|
|
/// </summary>
|
|
OldMicEDataCurrentTMD700,
|
|
|
|
/// <summary>
|
|
/// Item
|
|
/// </summary>
|
|
Item,
|
|
|
|
/// <summary>
|
|
/// [Reserved - shelter data with time]
|
|
/// </summary>
|
|
ShelterDataWithTime,
|
|
|
|
/// <summary>
|
|
/// Invalid data or test data
|
|
/// </summary>
|
|
InvalidOrTestData,
|
|
|
|
/// <summary>
|
|
/// [Reserved - Space Weather]
|
|
/// </summary>
|
|
SpaceWeather,
|
|
|
|
/// <summary>
|
|
/// Unused
|
|
/// </summary>
|
|
Unused,
|
|
|
|
/// <summary>
|
|
/// Position with timestamp (no APRS messaging)
|
|
/// </summary>
|
|
PositionWithTimestampNoMessaging,
|
|
|
|
/// <summary>
|
|
/// Message
|
|
/// </summary>
|
|
Message,
|
|
|
|
/// <summary>
|
|
/// Object
|
|
/// </summary>
|
|
[field: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1720", Justification = "APRS spec uses the word 'object', so it is appropriate here")]
|
|
Object,
|
|
|
|
/// <summary>
|
|
/// Station Capabilities
|
|
/// </summary>
|
|
StationCapabilities,
|
|
|
|
/// <summary>
|
|
/// Position without timestamp (with APRS messaging)
|
|
/// </summary>
|
|
PositionWithoutTimestampWithMessaging,
|
|
|
|
/// <summary>
|
|
/// Status
|
|
/// </summary>
|
|
Status,
|
|
|
|
/// <summary>
|
|
/// Query
|
|
/// </summary>
|
|
Query,
|
|
|
|
/// <summary>
|
|
/// [Do not use]
|
|
/// </summary>
|
|
DoNotUse,
|
|
|
|
/// <summary>
|
|
/// Positionwith timestamp (with APRS messaging)
|
|
/// </summary>
|
|
PositionWithTimestampWithMessaging,
|
|
|
|
/// <summary>
|
|
/// Telemetry data
|
|
/// </summary>
|
|
TelemetryData,
|
|
|
|
/// <summary>
|
|
/// Maidenhead grid locator beacon (obsolete)
|
|
/// </summary>
|
|
MaidenheadGridLocatorBeacon,
|
|
|
|
/// <summary>
|
|
/// Weather Report (without position)
|
|
/// </summary>
|
|
WeatherReport,
|
|
|
|
/// <summary>
|
|
/// Current Mic-E Data (not used in TM-D700)
|
|
/// </summary>
|
|
CurrentMicEDataNotTMD700,
|
|
|
|
/// <summary>
|
|
/// User-Defined APRS packet format
|
|
/// </summary>
|
|
UserDefinedAPRSPacketFormat,
|
|
|
|
/// <summary>
|
|
/// [Do not use - TNC stream switch character]
|
|
/// </summary>
|
|
DoNotUseTNSStreamSwitchCharacter,
|
|
|
|
/// <summary>
|
|
/// Third-party traffic
|
|
/// </summary>
|
|
ThirdPartyTraffic,
|
|
|
|
/// <summary>
|
|
/// Not a recognized symbol
|
|
/// </summary>
|
|
Unknown,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the type of APRS timestamp.
|
|
/// </summary>
|
|
public enum TimestampType
|
|
{
|
|
/// <summary>
|
|
/// Days/Hours/Minutes zulu
|
|
/// </summary>
|
|
DHMz,
|
|
|
|
/// <summary>
|
|
/// Days/Hours/Minutes local
|
|
/// </summary>
|
|
DHMl,
|
|
|
|
/// <summary>
|
|
/// Hours/Minutes/Seconds (always zulu)
|
|
/// </summary>
|
|
HMS,
|
|
|
|
/// <summary>
|
|
/// Hours/Minutes/Seconds (always zulu)
|
|
/// </summary>
|
|
MDHM,
|
|
|
|
/// <summary>
|
|
/// Not a decoded timestamp
|
|
/// </summary>
|
|
NotDecoded,
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs.Extensions
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
/// <summary>
|
|
/// Extension methods for conversion of enum types.
|
|
/// </summary>
|
|
public static class EnumConversionExtensions
|
|
{
|
|
/// <summary>
|
|
/// Maps a type-identifying character to a packet <see cref="PacketType"/>.
|
|
/// </summary>
|
|
private static readonly Dictionary<char, PacketType> DataTypeMap = new Dictionary<char, PacketType>()
|
|
{
|
|
{ (char)0x1c, PacketType.CurrentMicEData },
|
|
{ (char)0x1d, PacketType.OldMicEData },
|
|
{ '!', PacketType.PositionWithoutTimestampNoMessaging },
|
|
{ '\"', PacketType.Unused },
|
|
{ '#', PacketType.PeetBrosUIIWeatherStation },
|
|
{ '$', PacketType.RawGPSData },
|
|
{ '%', PacketType.AgreloDFJrMicroFinder },
|
|
{ '&', PacketType.MapFeature },
|
|
{ '\'', PacketType.OldMicEDataCurrentTMD700 },
|
|
{ '(', PacketType.Unused },
|
|
{ ')', PacketType.Item },
|
|
{ '*', PacketType.PeetBrosUIIWeatherStation },
|
|
{ '+', PacketType.ShelterDataWithTime },
|
|
{ ',', PacketType.InvalidOrTestData },
|
|
{ '-', PacketType.Unused },
|
|
{ '.', PacketType.SpaceWeather },
|
|
{ '/', PacketType.PositionWithTimestampNoMessaging },
|
|
{ ':', PacketType.Message },
|
|
{ ';', PacketType.Object },
|
|
{ '<', PacketType.StationCapabilities },
|
|
{ '=', PacketType.PositionWithoutTimestampWithMessaging },
|
|
{ '>', PacketType.Status },
|
|
{ '?', PacketType.Query },
|
|
{ '@', PacketType.PositionWithTimestampWithMessaging },
|
|
{ 'T', PacketType.TelemetryData },
|
|
{ '[', PacketType.MaidenheadGridLocatorBeacon },
|
|
{ '\\', PacketType.Unused },
|
|
{ ']', PacketType.Unused },
|
|
{ '^', PacketType.Unused },
|
|
{ '_', PacketType.WeatherReport },
|
|
{ '`', PacketType.CurrentMicEDataNotTMD700 },
|
|
{ '{', PacketType.UserDefinedAPRSPacketFormat },
|
|
{ '}', PacketType.ThirdPartyTraffic },
|
|
{ 'A', PacketType.DoNotUse },
|
|
{ 'S', PacketType.DoNotUse },
|
|
{ 'U', PacketType.DoNotUse },
|
|
{ 'Z', PacketType.DoNotUse },
|
|
{ '0', PacketType.DoNotUse },
|
|
{ '9', PacketType.DoNotUse },
|
|
};
|
|
|
|
/// <summary>
|
|
/// Converts a char to its corresponding <see cref="PacketType"/>.
|
|
/// </summary>
|
|
/// <param name="typeIdentifier">A char representation of <see cref="PacketType"/>.</param>
|
|
/// <returns><see cref="PacketType"/> of the info field, <see cref="PacketType.Unknown"/> if not a valid mapping.</returns>
|
|
public static PacketType ToPacketType(this char typeIdentifier) => DataTypeMap.GetValueOrDefault(char.ToUpperInvariant(typeIdentifier), PacketType.Unknown);
|
|
|
|
/// <summary>
|
|
/// Converts to the char representation of a given <see cref="PacketType"/>.
|
|
/// </summary>
|
|
/// <param name="type"><see cref="PacketType"/> to represent.</param>
|
|
/// <returns>A char representing type.</returns>
|
|
public static char ToChar(this PacketType type) => DataTypeMap.First(pair => pair.Value == type).Key;
|
|
|
|
/// <summary>
|
|
/// Determines the <see cref="NmeaType"/> of a raw GPS packet determined by a string.
|
|
/// If the string is length 3, the three letters are taken as is.
|
|
/// If the string is length 6 or longer, the indentifier is expected in the place dictated by the NMEA formats.
|
|
/// </summary>
|
|
/// <param name="nmeaInput">String of length 3 identifying a raw GPS type or an entire NMEA string.</param>
|
|
/// <returns>The raw GPS <see cref="NmeaType"/> represented by the argument.</returns>
|
|
public static NmeaType ToNmeaType(this string nmeaInput)
|
|
{
|
|
if (nmeaInput == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(nmeaInput));
|
|
}
|
|
else if (nmeaInput.Length != 3 && nmeaInput.Length < 6)
|
|
{
|
|
throw new ArgumentException("rawGpsIdentifier should be exactly 3 characters or at least 6. Given: " + nmeaInput.Length, nameof(nmeaInput));
|
|
}
|
|
else if (nmeaInput.Length >= 6)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
return nmeaInput.ToUpperInvariant() switch
|
|
{
|
|
"GGA" => NmeaType.GGA,
|
|
"GLL" => NmeaType.GLL,
|
|
"RMC" => NmeaType.RMC,
|
|
"VTG" => NmeaType.VTG,
|
|
"WPT" => NmeaType.WPT,
|
|
"WPL" => NmeaType.WPT,
|
|
_ => NmeaType.Unknown,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs.Extensions
|
|
{
|
|
using System;
|
|
using System.Text.RegularExpressions;
|
|
|
|
/// <summary>
|
|
/// Extension methods for <see cref="Match"/>.
|
|
/// </summary>
|
|
public static class MatchExtensions
|
|
{
|
|
/// <summary>
|
|
/// Asserts success of a Regex.Match call.
|
|
/// If the Match object is not successful, throws an ArgumentException.
|
|
/// </summary>
|
|
/// <param name="match">Match object to check for success.</param>
|
|
/// <param name="type">Type that was being checked against the regex.</param>
|
|
/// <param name="paramName">The name of the param that did not match the regex.</param>
|
|
public static void AssertSuccess(this Match match, string type, string paramName)
|
|
{
|
|
if (match == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(match));
|
|
}
|
|
else if (!match.Success)
|
|
{
|
|
throw new ArgumentException($"{type} did not match regex", paramName);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts success of a Regex.Match call.
|
|
/// If the Match object is not successful, throws an ArgumentException.
|
|
/// </summary>
|
|
/// <param name="match">Match object to check for success.</param>
|
|
/// <param name="type"><see cref="PacketType"/> that was being checked against the regex.</param>
|
|
/// <param name="paramName">The name of the param that did not match the regex.</param>
|
|
public static void AssertSuccess(this Match match, PacketType type, string paramName)
|
|
{
|
|
match.AssertSuccess(type.ToString(), paramName);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs.Extensions
|
|
{
|
|
using System;
|
|
using System.Globalization;
|
|
|
|
/// <summary>
|
|
/// Extension methods for handling weather packets.
|
|
/// </summary>
|
|
public static class WeatherExtensions
|
|
{
|
|
/// <summary>
|
|
/// Determines if a <see cref="Position"/>'s symbol is a weather station.
|
|
/// </summary>
|
|
/// <param name="position">A <see cref="Position"/> to check.</param>
|
|
/// <returns>True if the <see cref="Position"/>'s symbol is a weather station, else false.</returns>
|
|
public static bool IsWeatherSymbol(this Position position)
|
|
{
|
|
if (position == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(position));
|
|
}
|
|
|
|
return (position.SymbolTableIdentifier == '/' || position.SymbolTableIdentifier == '\\') &&
|
|
position.SymbolCode == '_';
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a string encoding of a single measurement for a complete weather packet.
|
|
/// </summary>
|
|
/// <param name="measurement">The measurement to convert to string.</param>
|
|
/// <param name="encodingLength">The number of characters to use in this representation.</param>
|
|
/// <returns>The integer as a string or all dots if null.</returns>
|
|
public static string ToWeatherEncoding(this int? measurement, int encodingLength = 3)
|
|
{
|
|
if (measurement == null)
|
|
{
|
|
return new string('.', encodingLength);
|
|
}
|
|
|
|
string encoded = Math.Abs(measurement.Value).ToString(CultureInfo.InvariantCulture)
|
|
.PadLeft(encodingLength, '0');
|
|
|
|
// If negative, ensure the negative sign is at the front of the padding zeros instead of in the middle
|
|
// that's why we take the absolute value above, so that we can put the negative sign all the way at the front.
|
|
if (measurement < 0)
|
|
{
|
|
encoded = $"-{encoded.Substring(1)}";
|
|
}
|
|
|
|
return encoded;
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
using System;
|
|
using AprsSharp.Parsers.Aprs.Extensions;
|
|
|
|
/// <summary>
|
|
/// A representation of an info field on an APRS packet.
|
|
/// </summary>
|
|
public abstract class InfoField
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="InfoField"/> class.
|
|
/// </summary>
|
|
public InfoField()
|
|
{
|
|
Type = PacketType.Unknown;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="InfoField"/> class from an encoded string.
|
|
/// </summary>
|
|
/// <param name="encodedInfoField">An encoded InfoField from which to pull the Type.</param>
|
|
public InfoField(string encodedInfoField)
|
|
{
|
|
if (encodedInfoField == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(encodedInfoField));
|
|
}
|
|
|
|
Type = GetPacketType(encodedInfoField);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="InfoField"/> class from a <see cref="PacketType"/>.
|
|
/// </summary>
|
|
/// <param name="type">The <see cref="PacketType"/> of this <see cref="InfoField"/>.</param>
|
|
public InfoField(PacketType type)
|
|
{
|
|
Type = type;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="InfoField"/> class from another <see cref="InfoField"/>.
|
|
/// This is the copy constructor.
|
|
/// </summary>
|
|
/// <param name="infoField">An <see cref="InfoField"/> to copy.</param>
|
|
public InfoField(InfoField infoField)
|
|
{
|
|
if (infoField == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(infoField));
|
|
}
|
|
|
|
Type = infoField.Type;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="PacketType"/> of this packet.
|
|
/// </summary>
|
|
public PacketType Type { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Instantiates a type of <see cref="InfoField"/> from the given string.
|
|
/// </summary>
|
|
/// <param name="encodedInfoField">String representation of the APRS info field.</param>
|
|
/// <returns>A class extending <see cref="InfoField"/>.</returns>
|
|
public static InfoField FromString(string encodedInfoField)
|
|
{
|
|
PacketType type = GetPacketType(encodedInfoField);
|
|
|
|
switch (type)
|
|
{
|
|
case PacketType.PositionWithoutTimestampNoMessaging:
|
|
case PacketType.PositionWithoutTimestampWithMessaging:
|
|
case PacketType.PositionWithTimestampNoMessaging:
|
|
case PacketType.PositionWithTimestampWithMessaging:
|
|
PositionInfo positionInfo = new PositionInfo(encodedInfoField);
|
|
return positionInfo.Position.IsWeatherSymbol() ? new WeatherInfo(positionInfo) : positionInfo;
|
|
|
|
case PacketType.Status:
|
|
return new StatusInfo(encodedInfoField);
|
|
|
|
case PacketType.MaidenheadGridLocatorBeacon:
|
|
return new MaidenheadBeaconInfo(encodedInfoField);
|
|
|
|
case PacketType.CurrentMicEDataNotTMD700:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.Unknown:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.Object:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.Message:
|
|
return new MessageField(encodedInfoField);
|
|
|
|
case PacketType.TelemetryData:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.StationCapabilities:
|
|
return new StationCapabilities(encodedInfoField);
|
|
|
|
case PacketType.WeatherReport:
|
|
return new WeatherReportInfoField(encodedInfoField);
|
|
|
|
case PacketType.OldMicEDataCurrentTMD700:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.DoNotUse:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.Item:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.AgreloDFJrMicroFinder:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.RawGPSData:
|
|
return new RawGpsInfoField(encodedInfoField);
|
|
|
|
case PacketType.SpaceWeather:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.PeetBrosUIIWeatherStation:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.UserDefinedAPRSPacketFormat:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.Unused:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.Query:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.InvalidOrTestData:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
|
|
case PacketType.ThirdPartyTraffic:
|
|
return new UnimplementedInfoField(encodedInfoField);
|
|
default:
|
|
throw new NotImplementedException($"FromString not implemented for info field type {type}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes an APRS info field to a string.
|
|
/// </summary>
|
|
/// <returns>String representation of the packet.</returns>
|
|
public abstract string Encode();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="PacketType"/> of a string representation of an APRS info field.
|
|
/// </summary>
|
|
/// <param name="encodedInfoField">A string-encoded APRS info field.</param>
|
|
/// <returns><see cref="PacketType"/> of the info field.</returns>
|
|
private static PacketType GetPacketType(string encodedInfoField)
|
|
{
|
|
if (encodedInfoField == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(encodedInfoField));
|
|
}
|
|
|
|
// TODO Issue #67: This isn't always true.
|
|
// '!' can come up to the 40th position.
|
|
char dataTypeIdentifier = char.ToUpperInvariant(encodedInfoField[0]);
|
|
|
|
return dataTypeIdentifier.ToPacketType();
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
using System;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using AprsSharp.Parsers.Aprs.Extensions;
|
|
|
|
/// <summary>
|
|
/// Represents an info field for packet type <see cref="PacketType.MaidenheadGridLocatorBeacon"/>.
|
|
/// </summary>
|
|
public class MaidenheadBeaconInfo : InfoField
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MaidenheadBeaconInfo"/> class.
|
|
/// NOTE: This packet type is considere obsolete and is included for decoding legacy devices. Use a status report instead.
|
|
/// </summary>
|
|
/// <param name="encodedInfoField">A string encoding of a <see cref="StatusInfo"/>.</param>
|
|
public MaidenheadBeaconInfo(string encodedInfoField)
|
|
: base(encodedInfoField)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(encodedInfoField))
|
|
{
|
|
throw new ArgumentNullException(nameof(encodedInfoField));
|
|
}
|
|
|
|
if (Type != PacketType.MaidenheadGridLocatorBeacon)
|
|
{
|
|
throw new ArgumentException($"Packet encoding not of type {nameof(PacketType.MaidenheadGridLocatorBeacon)}. Type was {Type}", nameof(encodedInfoField));
|
|
}
|
|
|
|
Match match = Regex.Match(encodedInfoField, RegexStrings.MaidenheadGridLocatorBeacon);
|
|
match.AssertSuccess(PacketType.Status, nameof(encodedInfoField));
|
|
|
|
Position = new Position();
|
|
Position.DecodeMaidenhead(match.Groups[1].Value);
|
|
|
|
if (match.Groups[2].Success)
|
|
{
|
|
Comment = match.Groups[2].Value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MaidenheadBeaconInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="position">A position, which will be encoded as a maidenhead gridsquare locator.</param>
|
|
/// <param name="comment">An optional comment.</param>
|
|
public MaidenheadBeaconInfo(Position position, string? comment)
|
|
: base(PacketType.MaidenheadGridLocatorBeacon)
|
|
{
|
|
Comment = comment;
|
|
Position = position ?? throw new ArgumentNullException(nameof(position));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the packet comment.
|
|
/// </summary>
|
|
public string? Comment { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the position from which the message was sent.
|
|
/// </summary>
|
|
public Position Position { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override string Encode()
|
|
{
|
|
StringBuilder encoded = new StringBuilder();
|
|
|
|
encoded.Append($"[{Position.EncodeGridsquare(6, false)}]");
|
|
|
|
if (!string.IsNullOrEmpty(Comment))
|
|
{
|
|
encoded.Append(Comment);
|
|
}
|
|
|
|
return encoded.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
using System;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using AprsSharp.Parsers.Aprs.Extensions;
|
|
|
|
/// <summary>
|
|
/// Represents an info field for the following types of packet:
|
|
/// * <see cref="PacketType.PositionWithoutTimestampNoMessaging"/>
|
|
/// * <see cref="PacketType.PositionWithTimestampNoMessaging"/>
|
|
/// * <see cref="PacketType.PositionWithoutTimestampWithMessaging"/>
|
|
/// * <see cref="PacketType.PositionWithTimestampWithMessaging"/>.
|
|
/// </summary>
|
|
public class PositionInfo : InfoField
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PositionInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="encodedInfoField">A string encoding of a <see cref="PositionInfo"/>.</param>
|
|
public PositionInfo(string encodedInfoField)
|
|
: base(encodedInfoField)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(encodedInfoField))
|
|
{
|
|
throw new ArgumentNullException(nameof(encodedInfoField));
|
|
}
|
|
|
|
if (Type == PacketType.PositionWithoutTimestampNoMessaging || Type == PacketType.PositionWithoutTimestampWithMessaging)
|
|
{
|
|
HasMessaging = Type == PacketType.PositionWithoutTimestampWithMessaging;
|
|
|
|
Match match = Regex.Match(encodedInfoField, RegexStrings.PositionWithoutTimestamp);
|
|
match.AssertSuccess(PacketType.PositionWithoutTimestampNoMessaging, nameof(encodedInfoField));
|
|
|
|
Position = new Position(match.Groups[1].Value);
|
|
|
|
if (match.Groups[6].Success)
|
|
{
|
|
Comment = match.Groups[6].Value;
|
|
}
|
|
}
|
|
else if (Type == PacketType.PositionWithTimestampNoMessaging || Type == PacketType.PositionWithTimestampWithMessaging)
|
|
{
|
|
HasMessaging = Type == PacketType.PositionWithTimestampWithMessaging;
|
|
|
|
Match match = Regex.Match(encodedInfoField, RegexStrings.PositionWithTimestamp);
|
|
match.AssertSuccess(
|
|
HasMessaging ?
|
|
PacketType.PositionWithTimestampWithMessaging :
|
|
PacketType.PositionWithTimestampNoMessaging,
|
|
nameof(encodedInfoField));
|
|
|
|
Timestamp = new Timestamp(match.Groups[2].Value);
|
|
|
|
Position = new Position(match.Groups[3].Value);
|
|
|
|
if (match.Groups[8].Success)
|
|
{
|
|
Comment = match.Groups[8].Value;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException($"Packet encoding not one of the position types. Type was {Type}", nameof(encodedInfoField));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PositionInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="position"><see cref="Position"/> for this packet.</param>
|
|
/// <param name="hasMessaging">True if the sender supports messaging.</param>
|
|
/// <param name="timestamp">Optional <see cref="Timestamp"/> for this packet.</param>
|
|
/// <param name="comment">Optional comment for this packet.</param>
|
|
public PositionInfo(Position position, bool hasMessaging, Timestamp? timestamp, string? comment)
|
|
{
|
|
Position = position;
|
|
HasMessaging = hasMessaging;
|
|
Timestamp = timestamp;
|
|
Comment = comment;
|
|
|
|
if (Timestamp == null)
|
|
{
|
|
Type = HasMessaging ? PacketType.PositionWithoutTimestampWithMessaging
|
|
: PacketType.PositionWithoutTimestampNoMessaging;
|
|
}
|
|
else
|
|
{
|
|
Type = HasMessaging ? PacketType.PositionWithTimestampWithMessaging
|
|
: PacketType.PositionWithTimestampNoMessaging;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PositionInfo"/> class.
|
|
/// This is the copy constructor.
|
|
/// </summary>
|
|
/// <param name="positionInfo">A <see cref="PositionInfo"/> to copy.</param>
|
|
public PositionInfo(PositionInfo positionInfo)
|
|
: base(positionInfo)
|
|
{
|
|
if (positionInfo == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(positionInfo));
|
|
}
|
|
|
|
HasMessaging = positionInfo.HasMessaging;
|
|
Comment = positionInfo.Comment;
|
|
Timestamp = positionInfo.Timestamp;
|
|
Position = positionInfo.Position;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the sender of the packet supports messaging.
|
|
/// </summary>
|
|
public bool HasMessaging { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the packet comment.
|
|
/// </summary>
|
|
public string? Comment { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the time at which the message was sent.
|
|
/// </summary>
|
|
public Timestamp? Timestamp { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the position from which the message was sent.
|
|
/// </summary>
|
|
public Position Position { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override string Encode() => Encode(TimestampType.DHMz);
|
|
|
|
/// <summary>
|
|
/// Encodes an APRS info field to a string.
|
|
/// </summary>
|
|
/// <param name="timeType">The <see cref="TimestampType"/> to use for timestamp encoding.</param>
|
|
/// <returns>String representation of the info field.</returns>
|
|
public string Encode(TimestampType timeType = TimestampType.DHMz)
|
|
{
|
|
if (Position == null)
|
|
{
|
|
throw new ArgumentException($"Position cannot be null when encoding with type ${Type}");
|
|
}
|
|
|
|
StringBuilder encoded = new StringBuilder();
|
|
|
|
encoded.Append(Type.ToChar());
|
|
|
|
if (Type == PacketType.PositionWithTimestampWithMessaging ||
|
|
Type == PacketType.PositionWithTimestampNoMessaging)
|
|
{
|
|
if (Timestamp == null)
|
|
{
|
|
throw new ArgumentException($"Timestamp cannot be null when encoding with type ${Type}");
|
|
}
|
|
|
|
encoded.Append(Timestamp.Encode(timeType));
|
|
}
|
|
|
|
encoded.Append(Position.Encode());
|
|
encoded.Append(Comment);
|
|
|
|
return encoded.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
using System;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using AprsSharp.Parsers.Aprs.Extensions;
|
|
|
|
/// <summary>
|
|
/// Represents an info field for packet type <see cref="PacketType.Status"/>.
|
|
/// </summary>
|
|
public class StatusInfo : InfoField
|
|
{
|
|
/// <summary>
|
|
/// The list of characters which may not be used in a comment on this type.
|
|
/// </summary>
|
|
private static readonly char[] CommentDisallowedChars = { '|', '~' };
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="StatusInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="encodedInfoField">A string encoding of a <see cref="StatusInfo"/>.</param>
|
|
public StatusInfo(string encodedInfoField)
|
|
: base(encodedInfoField)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(encodedInfoField))
|
|
{
|
|
throw new ArgumentNullException(nameof(encodedInfoField));
|
|
}
|
|
|
|
if (Type != PacketType.Status)
|
|
{
|
|
throw new ArgumentException($"Packet encoding not of type {nameof(PacketType.Status)}. Type was {Type}", nameof(encodedInfoField));
|
|
}
|
|
|
|
Match match = Regex.Match(encodedInfoField, RegexStrings.StatusWithMaidenheadAndComment);
|
|
if (match.Success)
|
|
{
|
|
match.AssertSuccess(PacketType.Status, nameof(encodedInfoField));
|
|
|
|
Position = new Position();
|
|
Position.DecodeMaidenhead(match.Groups[1].Value);
|
|
|
|
if (match.Groups[4].Success)
|
|
{
|
|
Comment = match.Groups[4].Value;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO Issue #88
|
|
throw new ArgumentException("Status report without maidenhead not yet implemented.Tracked by issue #88.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="StatusInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="position">A position, which will be encoded as a maidenhead gridsquare locator.</param>
|
|
/// <param name="comment">An optional comment.</param>
|
|
public StatusInfo(Position position, string? comment)
|
|
: this(null, position, comment)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="StatusInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="timestamp">An optional timestamp.</param>
|
|
/// <param name="comment">An optional comment.</param>
|
|
public StatusInfo(Timestamp timestamp, string? comment)
|
|
: this(timestamp, null, comment)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="StatusInfo"/> class.
|
|
/// Consolidates logic from the other constructors.
|
|
/// </summary>
|
|
/// <param name="timestamp">An optional timestamp.</param>
|
|
/// <param name="position">A position, which will be encoded as a maidenhead gridsquare locator.</param>
|
|
/// <param name="comment">An optional comment.</param>
|
|
private StatusInfo(Timestamp? timestamp, Position? position, string? comment)
|
|
: base(PacketType.Status)
|
|
{
|
|
if (position != null && timestamp != null)
|
|
{
|
|
throw new ArgumentException($"{nameof(timestamp)} may not be specified if a position is given.");
|
|
}
|
|
|
|
if (comment != null)
|
|
{
|
|
// Validate lengths
|
|
if (position != null && comment.Length > 53)
|
|
{
|
|
// Delimiting space + 53 characters = 54.
|
|
// We will add the space during encode, so only 53 here.
|
|
throw new ArgumentException(
|
|
$"With a position specified, comment may be at most 53 characters. Given comment had length {comment.Length}",
|
|
nameof(comment));
|
|
}
|
|
else if (timestamp != null && comment.Length > 55)
|
|
{
|
|
throw new ArgumentException(
|
|
$"With a timestamp, comment may be at most 55 characters. Given comment had length {comment.Length}",
|
|
nameof(comment));
|
|
}
|
|
else if (comment.Length > 62)
|
|
{
|
|
throw new ArgumentException(
|
|
$"Without timestamp or or position, comment may be at most 62 characters. Given comment had length {comment.Length}",
|
|
nameof(comment));
|
|
}
|
|
|
|
// TODO Issue #90: Share this logic across all packet types with a comment.
|
|
// Validate no disallowed characters were used
|
|
if (comment.IndexOfAny(CommentDisallowedChars) != -1)
|
|
{
|
|
throw new ArgumentException($"Comment may not include `|` or `~` but was given: {comment}", nameof(comment));
|
|
}
|
|
}
|
|
|
|
Timestamp = timestamp;
|
|
Position = position;
|
|
Comment = comment;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the packet comment.
|
|
/// </summary>
|
|
public string? Comment { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the time at which the message was sent.
|
|
/// </summary>
|
|
public Timestamp? Timestamp { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the position from which the message was sent.
|
|
/// </summary>
|
|
public Position? Position { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override string Encode()
|
|
{
|
|
StringBuilder encoded = new StringBuilder();
|
|
|
|
if (Position != null)
|
|
{
|
|
if (Timestamp != null)
|
|
{
|
|
throw new ArgumentException($"{nameof(Timestamp)} may not be specified if a position is given.");
|
|
}
|
|
|
|
encoded.Append(Type.ToChar());
|
|
encoded.Append(Position.EncodeGridsquare(6, true));
|
|
|
|
if (!string.IsNullOrEmpty(Comment))
|
|
{
|
|
encoded.Append($" {Comment}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO Issue #88
|
|
throw new NotImplementedException("Status report without maidenhead not yet implemented.Tracked by issue #88.");
|
|
}
|
|
|
|
return encoded.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
using System;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using AprsSharp.Parsers.Aprs.Extensions;
|
|
|
|
/// <summary>
|
|
/// Represents an info field for position packets carrying weather information.
|
|
/// </summary>
|
|
public class WeatherInfo : PositionInfo
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="WeatherInfo"/> class from an encoded <see cref="InfoField"/>.
|
|
/// </summary>
|
|
/// <param name="encodedInfoField">A string encoding of a <see cref="PositionInfo"/> with weather information.</param>
|
|
public WeatherInfo(string encodedInfoField)
|
|
: this(new PositionInfo(encodedInfoField))
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="WeatherInfo"/> class from a <see cref="PositionInfo"/>.
|
|
/// </summary>
|
|
/// <param name="positionInfo">A <see cref="PositionInfo"/> from which to decode a <see cref="WeatherInfo"/>.</param>
|
|
public WeatherInfo(PositionInfo positionInfo)
|
|
: base(positionInfo)
|
|
{
|
|
if (positionInfo == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(positionInfo));
|
|
}
|
|
|
|
if (!positionInfo.Position.IsWeatherSymbol())
|
|
{
|
|
throw new ArgumentException(
|
|
$@"Encoded packet must have weather symbol (`/_` or `\_`). Given: `{positionInfo.Position.SymbolTableIdentifier}{positionInfo.Position.SymbolCode}",
|
|
nameof(positionInfo));
|
|
}
|
|
|
|
WindDirection = GetWeatherMeasurement('^');
|
|
WindSpeed = GetWeatherMeasurement('/');
|
|
WindGust = GetWeatherMeasurement('g');
|
|
Temperature = GetWeatherMeasurement('t');
|
|
Rainfall1Hour = GetWeatherMeasurement('r');
|
|
Rainfall24Hour = GetWeatherMeasurement('p');
|
|
RainfallSinceMidnight = GetWeatherMeasurement('P');
|
|
Humidity = GetWeatherMeasurement('h', 2);
|
|
BarometricPressure = GetWeatherMeasurement('b', 5);
|
|
Luminosity = GetWeatherMeasurement('L') ?? GetWeatherMeasurement('l') + 1000;
|
|
RainRaw = GetWeatherMeasurement('#');
|
|
Snow = GetWeatherMeasurement('s');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="WeatherInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="position"><see cref="Position"/> for this packet.</param>
|
|
/// <param name="hasMessaging">True if the sender supports messaging.</param>
|
|
/// <param name="timestamp">Optional <see cref="Timestamp"/> for this packet.</param>
|
|
/// <param name="comment">Optional comment for this packet, will be appended after encoded weather information.</param>
|
|
/// <param name="windDirection">Wind direction in degrees.</param>
|
|
/// <param name="windSpeed">Wind speed 1-minute sustained in miles per hour.</param>
|
|
/// <param name="windGust">5-minute max wind gust in miles per hour.</param>
|
|
/// <param name="temperature">Temperature in degrees Fahrenheit.</param>
|
|
/// <param name="rainfall1Hour">1-hour rainfall in 100ths of an inch.</param>
|
|
/// <param name="rainfall24Hour">24-hour rainfall in 100ths of an inch.</param>
|
|
/// <param name="rainfallSinceMidnight">Rainfall since midnight in 100ths of an inch.</param>
|
|
/// <param name="humidity">Humidity in percentage.</param>
|
|
/// <param name="barometricPressure">Barometric pressure in 10ths of millibars/10ths of hPascal.</param>
|
|
/// <param name="luminosity">Luminosity in watts per square meter.</param>
|
|
/// <param name="rainRaw">Raw rain.</param>
|
|
/// <param name="snow">Snowfall in inches in the last 24 hours.</param>
|
|
public WeatherInfo(
|
|
Position position,
|
|
bool hasMessaging,
|
|
Timestamp? timestamp,
|
|
string? comment,
|
|
int? windDirection,
|
|
int? windSpeed,
|
|
int? windGust,
|
|
int? temperature,
|
|
int? rainfall1Hour,
|
|
int? rainfall24Hour,
|
|
int? rainfallSinceMidnight,
|
|
int? humidity,
|
|
int? barometricPressure,
|
|
int? luminosity,
|
|
int? rainRaw,
|
|
int? snow)
|
|
: base(position, hasMessaging, timestamp, comment)
|
|
{
|
|
WindDirection = windDirection;
|
|
WindSpeed = windSpeed;
|
|
WindGust = windGust;
|
|
Temperature = temperature;
|
|
Rainfall1Hour = rainfall1Hour;
|
|
Rainfall24Hour = rainfall24Hour;
|
|
RainfallSinceMidnight = rainfallSinceMidnight;
|
|
Humidity = humidity;
|
|
BarometricPressure = barometricPressure;
|
|
Luminosity = luminosity;
|
|
RainRaw = rainRaw;
|
|
Snow = snow;
|
|
|
|
Comment = $"{EncodeWeatherInfo()}{comment}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets wind direction as degrees.
|
|
/// </summary>
|
|
public int? WindDirection { get; }
|
|
|
|
/// <summary>
|
|
/// Gets wind speed 1-minute sustained in miles per hour.
|
|
/// </summary>
|
|
public int? WindSpeed { get; }
|
|
|
|
/// <summary>
|
|
/// Gets 5-minute max wind gust in miles per hour.
|
|
/// </summary>
|
|
public int? WindGust { get; }
|
|
|
|
/// <summary>
|
|
/// Gets temperature in degrees Fahrenheit.
|
|
/// </summary>
|
|
public int? Temperature { get; }
|
|
|
|
/// <summary>
|
|
/// Gets 1-hour rainfall in 100ths of an inch.
|
|
/// </summary>
|
|
public int? Rainfall1Hour { get; }
|
|
|
|
/// <summary>
|
|
/// Gets 24-hour rainfall in 100ths of an inch.
|
|
/// </summary>
|
|
public int? Rainfall24Hour { get; }
|
|
|
|
/// <summary>
|
|
/// Gets rainfall since midnight in 100ths of an inch.
|
|
/// </summary>
|
|
public int? RainfallSinceMidnight { get; }
|
|
|
|
/// <summary>
|
|
/// Gets humidity in percentage.
|
|
/// </summary>
|
|
public int? Humidity { get; }
|
|
|
|
/// <summary>
|
|
/// Gets Barometric pressure in 10ths of millibars/10ths of hPascal.
|
|
/// </summary>
|
|
public int? BarometricPressure { get; }
|
|
|
|
/// <summary>
|
|
/// Gets luminosity in watts per square meter.
|
|
/// </summary>
|
|
public int? Luminosity { get; }
|
|
|
|
/// <summary>
|
|
/// Gets raw rain.
|
|
/// </summary>
|
|
public int? RainRaw { get; }
|
|
|
|
/// <summary>
|
|
/// Gets snowfall in inches in the last 24 hours.
|
|
/// </summary>
|
|
public int? Snow { get; }
|
|
|
|
/// <summary>
|
|
/// Retrieves an APRS weather measurement from the comment string.
|
|
/// </summary>
|
|
/// <param name="measurementKey">The weather element to fetch, as defined by the key the ARPS specification.</param>
|
|
/// <param name="length">The expected number of digits in the measurement.</param>
|
|
/// <returns>An int value, if found. Else, null.</returns>
|
|
private int? GetWeatherMeasurement(char measurementKey, int length = 3)
|
|
{
|
|
// Regex below looks for the measurement key followed by either `length` numbers
|
|
// or a negative number of `length - 1` digits (to allow for the negative sign)
|
|
var match = Regex.Match(Comment, $"{measurementKey}(([0-9]{{{length}}})|(-[0-9]{{{length - 1}}}))");
|
|
return match.Success ? int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture) : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes weather information to be placed in the comment field.
|
|
/// </summary>
|
|
/// <returns>An APRS encoding of weather information on this packet.</returns>
|
|
private string EncodeWeatherInfo()
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
sb.Append(WindDirection.ToWeatherEncoding());
|
|
sb.Append($"/{WindSpeed.ToWeatherEncoding()}");
|
|
sb.Append($"g{WindGust.ToWeatherEncoding()}");
|
|
sb.Append($"t{Temperature.ToWeatherEncoding()}");
|
|
sb.Append($"r{Rainfall1Hour.ToWeatherEncoding()}");
|
|
sb.Append($"p{Rainfall24Hour.ToWeatherEncoding()}");
|
|
sb.Append($"P{RainfallSinceMidnight.ToWeatherEncoding()}");
|
|
sb.Append($"h{Humidity.ToWeatherEncoding(2)}");
|
|
sb.Append($"b{BarometricPressure.ToWeatherEncoding(5)}");
|
|
|
|
// Only add less common measurements if provided
|
|
if (Luminosity != null)
|
|
{
|
|
char lum = Luminosity < 1000 ? 'L' : 'l';
|
|
sb.Append($"{lum}{(Luminosity % 1000).ToWeatherEncoding()}");
|
|
}
|
|
|
|
if (RainRaw != null)
|
|
{
|
|
sb.Append($"#{RainRaw.ToWeatherEncoding()}");
|
|
}
|
|
|
|
if (Snow != null)
|
|
{
|
|
sb.Append($"s{Snow.ToWeatherEncoding()}");
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text.RegularExpressions;
|
|
using AprsSharp.Parsers.Aprs.Extensions;
|
|
|
|
/// <summary>
|
|
/// Represents a full APRS packet.
|
|
/// </summary>
|
|
public class Packet
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Packet"/> class.
|
|
/// </summary>
|
|
/// <param name="encodedPacket">The string encoding of the APRS packet.</param>
|
|
public Packet(string encodedPacket)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(encodedPacket))
|
|
{
|
|
throw new ArgumentNullException(nameof(encodedPacket));
|
|
}
|
|
|
|
Match match = Regex.Match(encodedPacket, RegexStrings.Tnc2Packet);
|
|
match.AssertSuccess("Full TNC2 Packet", nameof(encodedPacket));
|
|
|
|
Sender = match.Groups[1].Value;
|
|
Path = match.Groups[2].Value.Split(',');
|
|
ReceivedTime = DateTime.UtcNow;
|
|
InfoField = InfoField.FromString(match.Groups[3].Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Packet"/> class.
|
|
/// </summary>
|
|
/// <param name="sender">The callsign of the sender.</param>
|
|
/// <param name="path">The digipath for the packet.</param>
|
|
/// <param name="infoField">An <see cref="InfoField"/> or extending class to send.</param>
|
|
public Packet(string sender, IList<string> path, InfoField infoField)
|
|
{
|
|
Sender = sender;
|
|
Path = path;
|
|
InfoField = infoField;
|
|
ReceivedTime = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the sender's callsign.
|
|
/// </summary>
|
|
public string Sender { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the APRS digipath of the packet.
|
|
/// </summary>
|
|
public IList<string> Path { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the time this packet was decoded.
|
|
/// Null if this packet was created instead of decoded.
|
|
/// </summary>
|
|
public DateTime? ReceivedTime { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the APRS information field for this packet.
|
|
/// </summary>
|
|
public InfoField InfoField { get; }
|
|
|
|
/// <summary>
|
|
/// Encodes an APRS packet to a string.
|
|
/// </summary>
|
|
/// <returns>String representation of the packet.</returns>
|
|
public virtual string Encode()
|
|
{
|
|
return $"{Sender}>{string.Join(',', Path)}:{InfoField.Encode()}";
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
using System;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using AprsSharp.Parsers.Aprs.Extensions;
|
|
using GeoCoordinatePortable;
|
|
|
|
/// <summary>
|
|
/// Represents lat/long position in an APRS packet.
|
|
/// </summary>
|
|
public class Position
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Position"/> class.
|
|
/// Decodes from LAT/LONG coordinates.
|
|
/// </summary>
|
|
/// <param name="coords">String representing LAT/LONG coordinates.</param>
|
|
public Position(string coords)
|
|
{
|
|
Decode(coords);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Position"/> class.
|
|
/// </summary>
|
|
/// <param name="coords">Coordinates to encode.</param>
|
|
/// <param name="table">The primary or secondary symbole table.</param>
|
|
/// <param name="symbol">The APRS symbol code from the table given.</param>
|
|
/// <param name="amb">Ambiguity to use on the coordinates.</param>
|
|
public Position(GeoCoordinate? coords = null, char? table = null, char? symbol = null, int? amb = null)
|
|
{
|
|
Coordinates = coords ?? Coordinates;
|
|
SymbolTableIdentifier = table ?? SymbolTableIdentifier;
|
|
SymbolCode = symbol ?? SymbolCode;
|
|
Ambiguity = amb ?? Ambiguity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets lat/long coordinates.
|
|
/// </summary>
|
|
public GeoCoordinate Coordinates { get; set; } = new GeoCoordinate(0, 0);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the symbol table identifier.
|
|
/// </summary>
|
|
public char SymbolTableIdentifier { get; set; } = '\\';
|
|
|
|
/// <summary>
|
|
/// Gets or sets the symbol code.
|
|
/// </summary>
|
|
public char SymbolCode { get; set; } = '.';
|
|
|
|
/// <summary>
|
|
/// Gets or sets the position ambiguity.
|
|
/// </summary>
|
|
public int Ambiguity { get; set; }
|
|
|
|
/// <summary>
|
|
/// Converts a string to have the given amount of ambiguity.
|
|
/// </summary>
|
|
/// <param name="coords">String to convert.</param>
|
|
/// <param name="enforceAmbiguity">Amount of ambiguity to emplace.</param>
|
|
/// <returns>String representing ambiguity.</returns>
|
|
public static string EnforceAmbiguity(string coords, int enforceAmbiguity)
|
|
{
|
|
if (coords == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(coords));
|
|
}
|
|
else if (enforceAmbiguity > coords.Length - 2)
|
|
{
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(enforceAmbiguity),
|
|
$"Only {coords.Length - 2} numbers can be filtered, but {enforceAmbiguity} were requested for string {coords}");
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder(coords);
|
|
int remainingAmbiguity = enforceAmbiguity;
|
|
|
|
for (int i = coords.Length - 2; i >= 0 && remainingAmbiguity > 0; --i)
|
|
{
|
|
if (i == coords.Length - 4)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sb[i] = ' ';
|
|
--remainingAmbiguity;
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Counts the number of spaces from right to left and returns the number for ambiguity.
|
|
/// Also ensures there are no spaces left of a digit, which is an invalid APRS ambiguity syntax.
|
|
/// </summary>
|
|
/// <param name="coords">An APRS latitude or longitude string.</param>
|
|
/// <returns>Integer representing ambiguity.</returns>
|
|
public static int CountAmbiguity(string coords)
|
|
{
|
|
if (coords == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(coords));
|
|
}
|
|
|
|
int ambiguity = 0;
|
|
bool foundDigit = false;
|
|
for (int i = coords.Length - 2; i >= 0; --i)
|
|
{
|
|
// skip the period
|
|
if (i == coords.Length - 4)
|
|
{
|
|
continue;
|
|
}
|
|
else if (coords[i] == ' ')
|
|
{
|
|
if (foundDigit)
|
|
{
|
|
throw new ArgumentException(
|
|
$"Coordinates shoud only have spaces growing from right. Error on string: {coords}",
|
|
nameof(coords));
|
|
}
|
|
else
|
|
{
|
|
++ambiguity;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foundDigit = true;
|
|
}
|
|
}
|
|
|
|
return ambiguity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes an encoded APRS coordinate string and uses it to initialize to <see cref="GeoCoordinate"/>.
|
|
/// </summary>
|
|
/// <param name="coords">A string of APRS encoded coordinates.</param>
|
|
public void Decode(string coords)
|
|
{
|
|
if (coords == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(coords));
|
|
}
|
|
|
|
Match match = Regex.Match(coords, RegexStrings.PositionLatLongWithSymbols);
|
|
match.AssertSuccess("Coordinates", nameof(coords));
|
|
|
|
Ambiguity = 0;
|
|
double latitude = DecodeLatitude(match.Groups[1].Value);
|
|
double longitude = DecodeLongitude(match.Groups[3].Value);
|
|
|
|
SymbolTableIdentifier = match.Groups[2].Value[0];
|
|
SymbolCode = match.Groups[4].Value[0];
|
|
Coordinates = new GeoCoordinate(latitude, longitude);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode from Maidenhead Location System gridsquare to a point in the middle of the gridsquare.
|
|
/// </summary>
|
|
/// <param name="gridsquare">4 or 6 char Maidenhead gridsquare, optionally followed by symbol table and identifier.</param>
|
|
public void DecodeMaidenhead(string gridsquare)
|
|
{
|
|
if (gridsquare == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(gridsquare));
|
|
}
|
|
|
|
var match = Regex.Match(gridsquare, RegexStrings.MaidenheadGridFullLine);
|
|
match.AssertSuccess("Maidenhead", nameof(gridsquare));
|
|
|
|
string trimmedGridsquare = match.Groups[1].Value;
|
|
|
|
// If a third group is matched, this group is the two symbol fields.
|
|
if (match.Groups[2].Success)
|
|
{
|
|
string symbols = match.Groups[2].Value;
|
|
SymbolTableIdentifier = symbols[0];
|
|
SymbolCode = symbols[1];
|
|
}
|
|
|
|
double latitude = DecodeFromGridsquare(trimmedGridsquare, CoordinateSystem.Latitude);
|
|
double longitude = DecodeFromGridsquare(trimmedGridsquare, CoordinateSystem.Longitude);
|
|
Coordinates = new GeoCoordinate(latitude, longitude);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes the location as a gridsquare from the coordinates on this object.
|
|
/// </summary>
|
|
/// <param name="length">Max number of characters to use in gridsquare encoding, must be {4, 6, 8}. Ambiguity will subtract from this.</param>
|
|
/// <param name="appendSymbol">If true, appends the symbol to the end of the string.</param>
|
|
/// <returns>A string representation of the Maidenhead gridsquare.</returns>
|
|
public string EncodeGridsquare(int length, bool appendSymbol)
|
|
{
|
|
if (length != 4 &&
|
|
length != 6 &&
|
|
length != 8)
|
|
{
|
|
throw new ArgumentException($"Length should be 4, 6, or 8. Given value was {length}", nameof(length));
|
|
}
|
|
else if (Coordinates.IsNull)
|
|
{
|
|
throw new NullReferenceException("Coordinates were null.");
|
|
}
|
|
else if (Ambiguity != 0 &&
|
|
Ambiguity != 2 &&
|
|
Ambiguity != 4 &&
|
|
Ambiguity != 6)
|
|
{
|
|
throw new ArgumentException($"Ambiguity must be in {{0, 2, 4, 6}} to encode a gridsquare, but is {Ambiguity}");
|
|
}
|
|
else if (Ambiguity > length - 4)
|
|
{
|
|
throw new ArgumentException($"Final length of the string must be at least 4. length - Ambiguity >= 4, but length is {length} and Ambiguity is {Ambiguity}");
|
|
}
|
|
|
|
string gridsquare = string.Empty;
|
|
|
|
string longitude = EncodeGridsquareElement(Coordinates.Longitude, CoordinateSystem.Longitude, (length - Ambiguity) / 2);
|
|
string latitude = EncodeGridsquareElement(Coordinates.Latitude, CoordinateSystem.Latitude, (length - Ambiguity) / 2);
|
|
|
|
for (int i = 0; i < length - Ambiguity; ++i)
|
|
{
|
|
if (i % 2 == 0)
|
|
{
|
|
gridsquare += longitude[i / 2];
|
|
}
|
|
else
|
|
{
|
|
gridsquare += latitude[i / 2];
|
|
}
|
|
}
|
|
|
|
if (appendSymbol)
|
|
{
|
|
gridsquare += SymbolTableIdentifier;
|
|
gridsquare += SymbolCode;
|
|
}
|
|
|
|
return gridsquare;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode the latitude section of the coordinate string.
|
|
/// </summary>
|
|
/// <param name="coords">APRS encoded latitude coordinates.</param>
|
|
/// <returns>Decimal latitude.</returns>
|
|
public double DecodeLatitude(string coords)
|
|
{
|
|
// Ensure latitude is well formatted
|
|
if (coords == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(coords));
|
|
}
|
|
|
|
string upperCoords = coords.ToUpperInvariant();
|
|
if (upperCoords.Length != 8)
|
|
{
|
|
throw new ArgumentException("Latitude coordinates should be 8 characters. Given string was " + upperCoords.Length);
|
|
}
|
|
|
|
char direction = upperCoords[7];
|
|
if (upperCoords[7] != 'N' && upperCoords[7] != 'S')
|
|
{
|
|
throw new ArgumentException("Coordinates should end in N or S. Given string ended in " + direction);
|
|
}
|
|
|
|
if (upperCoords[4] != '.')
|
|
{
|
|
throw new ArgumentException("Coordinates should have '.' at index 4. Given string had " + upperCoords[4]);
|
|
}
|
|
|
|
// Count ambiguity and ensure no out of place spaces
|
|
Ambiguity = CountAmbiguity(upperCoords);
|
|
|
|
// replace spaces with zeros
|
|
string modifiedCoords = upperCoords.Replace(' ', '0');
|
|
|
|
// Break out strings and convert values
|
|
string degreesStr = modifiedCoords.Substring(0, 2);
|
|
string minutesStr = modifiedCoords.Substring(2, 2);
|
|
string hundMinutesStr = modifiedCoords.Substring(5, 2);
|
|
|
|
double degrees = double.Parse(degreesStr, CultureInfo.InvariantCulture);
|
|
if (degrees < 00 || degrees > 90)
|
|
{
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(coords),
|
|
$"Degrees must be in range [00,90]. Found: {degrees} in coord string {modifiedCoords} from given coord string {coords}");
|
|
}
|
|
|
|
double minutes = double.Parse(minutesStr, CultureInfo.InvariantCulture);
|
|
double hundMinutes = double.Parse(hundMinutesStr, CultureInfo.InvariantCulture);
|
|
|
|
double retval = degrees + ((minutes + (hundMinutes / 100)) / 60.0);
|
|
|
|
if (direction == 'S')
|
|
{
|
|
retval *= -1;
|
|
}
|
|
|
|
// limit significant figures
|
|
return Math.Round(retval, 4);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode the longitude section of the coordinate string.
|
|
/// </summary>
|
|
/// <param name="coords">APRS encoded latitude coordinates.</param>
|
|
/// <returns>Decimal longitude.</returns>
|
|
public double DecodeLongitude(string coords)
|
|
{
|
|
if (coords == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(coords));
|
|
}
|
|
else if (coords.Length != 9)
|
|
{
|
|
throw new ArgumentException(
|
|
$"Longitude format calls for 9 characters. Given string has length {coords.Length}",
|
|
nameof(coords));
|
|
}
|
|
|
|
string upperCoords = coords.ToUpperInvariant();
|
|
|
|
char direction = upperCoords[8];
|
|
if (direction != 'W' && direction != 'E')
|
|
{
|
|
throw new ArgumentException(
|
|
$"Coordinates should end in E or W. Given string ended in {direction}",
|
|
nameof(coords));
|
|
}
|
|
else if (upperCoords[5] != '.')
|
|
{
|
|
throw new ArgumentException(
|
|
$"Coordinates should have '.' at index 5. Given string had {upperCoords[5]}",
|
|
nameof(coords));
|
|
}
|
|
|
|
// This ensures no spaces are out of place
|
|
CountAmbiguity(upperCoords);
|
|
|
|
// Enforce ambiguity from latitude and replace spaces
|
|
string modifiedCoords = EnforceAmbiguity(upperCoords, Ambiguity);
|
|
modifiedCoords = modifiedCoords.Replace(' ', '0');
|
|
|
|
// Break out strings and convert values
|
|
string degreesStr = modifiedCoords.Substring(0, 3);
|
|
string minutesStr = modifiedCoords.Substring(3, 2);
|
|
string hundMinuteStr = modifiedCoords.Substring(6, 2);
|
|
|
|
double degrees = double.Parse(degreesStr, CultureInfo.InvariantCulture);
|
|
if (degrees < 000 || degrees > 180)
|
|
{
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(coords),
|
|
$"Degrees longitude must be in range [000, 180]. Found: {degrees} in coord string {modifiedCoords} from given coord string {coords}");
|
|
}
|
|
|
|
double minutes = double.Parse(minutesStr, CultureInfo.InvariantCulture);
|
|
double hundMinutes = double.Parse(hundMinuteStr, CultureInfo.InvariantCulture);
|
|
|
|
double retval = degrees + ((minutes + (hundMinutes / 100)) / 60.0);
|
|
|
|
if (direction == 'W')
|
|
{
|
|
retval *= -1;
|
|
}
|
|
|
|
// limit significant figures
|
|
return Math.Round(retval, 4);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes with the values set on the fields using lat/long.
|
|
/// </summary>
|
|
/// <returns>A string encoding of an APRS position (lat/long).</returns>
|
|
public string Encode()
|
|
{
|
|
return EncodeLatitude() +
|
|
SymbolTableIdentifier +
|
|
EncodeLongitude() +
|
|
SymbolCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes latitude position, enforcing ambiguity
|
|
/// Really just calls through to EncodeCoordinates, but this is easier to test
|
|
/// with PrivateObject.
|
|
/// </summary>
|
|
/// <returns>Encoded APRS latitude position.</returns>
|
|
public string EncodeLatitude()
|
|
{
|
|
return EncodeCoordinates(CoordinateSystem.Latitude);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes longitude position, enforcing ambiguity
|
|
/// Really just calls through to EncodeCoordinates, but this is easier to test
|
|
/// with PrivateObject.
|
|
/// </summary>
|
|
/// <returns>Encoded APRS longitude position.</returns>
|
|
public string EncodeLongitude()
|
|
{
|
|
return EncodeCoordinates(CoordinateSystem.Longitude);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encode either the latitude or longitude part of a Maidenhead gridsquare.
|
|
/// </summary>
|
|
/// <param name="coords">The coordinates to use.</param>
|
|
/// <param name="coordinateEncode">Either latitude or longitude encoding.</param>
|
|
/// <param name="length">The number of chars for this encoding (should be half the total length of the gridsquare as this is only lat or long).</param>
|
|
/// <returns>A string of the lat or long for the gridsquare encoding.</returns>
|
|
private static string EncodeGridsquareElement(double coords, CoordinateSystem coordinateEncode, int length)
|
|
{
|
|
if (coordinateEncode == CoordinateSystem.Longitude && Math.Abs(coords) > 180)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(coords), "Longitude coordinates must be inside [-180, 180]");
|
|
}
|
|
else if (coordinateEncode == CoordinateSystem.Latitude && Math.Abs(coords) > 90)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(coords), "Longitude coordinates must be inside [-90, 90]");
|
|
}
|
|
|
|
string encoded = string.Empty;
|
|
int charIndex;
|
|
double stepDivisor;
|
|
|
|
// Longitude scales most values by 2 over latitude
|
|
int multiplier = (coordinateEncode == CoordinateSystem.Longitude) ? 2 : 1;
|
|
|
|
// no negatives here
|
|
double shiftedCoords = coords + (multiplier * 90.0);
|
|
|
|
if (length >= 1)
|
|
{
|
|
stepDivisor = 10.0 * multiplier;
|
|
charIndex = (int)(shiftedCoords / stepDivisor);
|
|
encoded += char.ConvertFromUtf32('A' + charIndex);
|
|
|
|
shiftedCoords %= stepDivisor;
|
|
}
|
|
|
|
if (length >= 2)
|
|
{
|
|
stepDivisor = 1.0 * multiplier;
|
|
charIndex = (int)(shiftedCoords / stepDivisor);
|
|
encoded += charIndex.ToString(CultureInfo.InvariantCulture);
|
|
|
|
shiftedCoords %= stepDivisor;
|
|
}
|
|
|
|
// moving in to minutes territory
|
|
shiftedCoords *= 60.0;
|
|
|
|
if (length >= 3)
|
|
{
|
|
stepDivisor = 2.5 * multiplier;
|
|
charIndex = (int)(shiftedCoords / stepDivisor);
|
|
encoded += char.ConvertFromUtf32('A' + charIndex);
|
|
|
|
shiftedCoords %= stepDivisor;
|
|
}
|
|
|
|
if (length >= 4)
|
|
{
|
|
stepDivisor = 0.25 * multiplier;
|
|
charIndex = (int)(shiftedCoords / stepDivisor);
|
|
encoded += charIndex.ToString(CultureInfo.InvariantCulture);
|
|
|
|
// shiftedCoords %= stepDivisor;
|
|
}
|
|
|
|
return encoded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a maidenhead gridsquare, converts to decimal latitude or longitude.
|
|
/// </summary>
|
|
/// <param name="gridsquare">A Maidenhead Location System gridsquare.</param>
|
|
/// <param name="coordinateDecode">Latitude or longitude for decode.</param>
|
|
/// <returns>A double representing latitude [-180.0, 180.0] or longitude [-90.0, 90.0].</returns>
|
|
private static double DecodeFromGridsquare(string gridsquare, CoordinateSystem coordinateDecode)
|
|
{
|
|
if (gridsquare == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(gridsquare));
|
|
}
|
|
else if (gridsquare.Length != 4 &&
|
|
gridsquare.Length != 6 &&
|
|
gridsquare.Length != 8)
|
|
{
|
|
throw new ArgumentException(
|
|
$"The given Maidenhead Location System gridsquare was {gridsquare.Length} characters length when a length of 4 or 6 characters was expected.",
|
|
nameof(gridsquare));
|
|
}
|
|
|
|
double coord = 0;
|
|
double gridsquareSize = 0;
|
|
|
|
// Chars alternatingly refer to longitude and latitude
|
|
int index = (coordinateDecode == CoordinateSystem.Latitude) ? 1 : 0;
|
|
|
|
// Longitude scales most values by 2 over latitude
|
|
int multiplier = (coordinateDecode == CoordinateSystem.Longitude) ? 2 : 1;
|
|
|
|
gridsquare = gridsquare.ToUpperInvariant();
|
|
|
|
if (gridsquare.Length >= 2)
|
|
{
|
|
if (gridsquare[index] < 'A' || gridsquare[index] > 'R')
|
|
{
|
|
throw new ArgumentException(
|
|
$"Gridsquare should use A-R for first pair of characters. The given string was {gridsquare}",
|
|
nameof(gridsquare));
|
|
}
|
|
|
|
gridsquareSize = 10 * multiplier;
|
|
coord += gridsquareSize * (gridsquare[index] - 'A');
|
|
index += 2;
|
|
}
|
|
|
|
if (gridsquare.Length >= 4)
|
|
{
|
|
if (!char.IsDigit(gridsquare[index]))
|
|
{
|
|
throw new ArgumentException(
|
|
$"Gridsquare should use 0-9 for second pair of characters. The given string was {gridsquare}",
|
|
nameof(gridsquare));
|
|
}
|
|
|
|
gridsquareSize = multiplier;
|
|
coord += gridsquareSize * char.GetNumericValue(gridsquare[index]);
|
|
index += 2;
|
|
}
|
|
|
|
if (gridsquare.Length >= 6)
|
|
{
|
|
if (gridsquare[index] < 'A' || gridsquare[index] > 'X')
|
|
{
|
|
throw new ArgumentException(
|
|
$"Gridsquare should use A-X for third pair of characters. The given string was {gridsquare}",
|
|
nameof(gridsquare));
|
|
}
|
|
|
|
gridsquareSize = (2.5 * multiplier) / 60.0;
|
|
coord += ((2.5 * multiplier) * (gridsquare[index] - 'A')) / 60.0;
|
|
index += 2;
|
|
}
|
|
|
|
if (gridsquare.Length >= 8)
|
|
{
|
|
if (!char.IsDigit(gridsquare[index]))
|
|
{
|
|
throw new ArgumentException(
|
|
$"Gridsquare should use 0-9 for fourth pair of characters. The given string was {gridsquare}",
|
|
nameof(gridsquare));
|
|
}
|
|
|
|
gridsquareSize = (0.25 * multiplier) / 60.0;
|
|
coord += ((0.25 * multiplier) * char.GetNumericValue(gridsquare[index])) / 60.0;
|
|
}
|
|
|
|
// center the coordinate in the grid
|
|
coord += gridsquareSize / 2.0;
|
|
return coord - (90.0 * multiplier);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes latitude or longitude position, enforcing ambiguity.
|
|
/// </summary>
|
|
/// <param name="type"><see cref="CoordinateSystem"/> to encode: Latitude or Longitude.</param>
|
|
/// <returns>String of APRS latitude or longitude position.</returns>
|
|
private string EncodeCoordinates(CoordinateSystem type)
|
|
{
|
|
double coords;
|
|
char direction;
|
|
string decimalFormat;
|
|
|
|
if (type == CoordinateSystem.Latitude)
|
|
{
|
|
coords = Coordinates.Latitude;
|
|
direction = coords < 0 ? 'S' : 'N';
|
|
decimalFormat = "D2";
|
|
}
|
|
else if (type == CoordinateSystem.Longitude)
|
|
{
|
|
coords = Coordinates.Longitude;
|
|
direction = coords > 0 ? 'E' : 'W';
|
|
decimalFormat = "D3";
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException($"Invalid CoordinateSystem: {type}", nameof(type));
|
|
}
|
|
|
|
coords = Math.Abs(coords);
|
|
|
|
int degrees = (int)Math.Floor(coords);
|
|
int minutes = (int)Math.Floor((coords - degrees) * 60);
|
|
int hundMinutes = (int)Math.Round((((coords - degrees) * 60) - minutes) * 100);
|
|
|
|
string encoded = degrees.ToString(decimalFormat, CultureInfo.InvariantCulture) +
|
|
minutes.ToString("D2", CultureInfo.InvariantCulture) +
|
|
'.' +
|
|
hundMinutes.ToString("D2", CultureInfo.InvariantCulture) +
|
|
direction;
|
|
|
|
encoded = EnforceAmbiguity(encoded, Ambiguity);
|
|
|
|
return encoded;
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
/// <summary>
|
|
/// Holds strings for regex comparisons for reuse.
|
|
/// </summary>
|
|
internal static class RegexStrings
|
|
{
|
|
/// <summary>
|
|
/// Same as <see cref="MaidenheadGridWithSymbols"/> but symbols are optional.
|
|
/// Three groups: Full, alphanumeric grid, symbols.
|
|
/// </summary>
|
|
public static readonly string MaidenheadGridWithOptionalSymbols = $@"{MaidenheadGridWithSymbols}?";
|
|
|
|
/// <summary>
|
|
/// Matches Maidenhead grid with symbols.
|
|
/// Three groups: Full, alphanumeric grid, symbols.
|
|
/// </summary>
|
|
public const string MaidenheadGridWithSymbols = @"([a-zA-Z0-9]{4,8})(.{2})";
|
|
|
|
/// <summary>
|
|
/// Matches Maidenhead grid of 4-6 characters without symbols.
|
|
/// One group: Full match.
|
|
/// </summary>
|
|
public const string MaidenheadGrid4or6NoSymbols = @"([a-zA-Z0-9]{4,6})";
|
|
|
|
/// <summary>
|
|
/// Matchdes a lat/long position, including with ambiguity, including symbols.
|
|
/// Five matches:
|
|
/// Full
|
|
/// Latitute
|
|
/// Symbol table ID
|
|
/// Longitude
|
|
/// Symbol Code.
|
|
/// </summary>
|
|
public const string PositionLatLongWithSymbols = @"([0-9 \.NS]{8})(.)([0-9 \.EW]{9})(.)";
|
|
|
|
/// <summary>
|
|
/// Same as <see cref="MaidenheadGridWithOptionalSymbols"/> but forces full line match.
|
|
/// </summary>
|
|
/// <value></value>
|
|
public static readonly string MaidenheadGridFullLine = $@"^{MaidenheadGridWithOptionalSymbols}$";
|
|
|
|
/// <summary>
|
|
/// Matches a Miadenhead Grid in square brackets [] with comment.
|
|
/// Four groups: Full, alphanumeric grid, comment.
|
|
/// </summary>
|
|
public static readonly string MaidenheadGridLocatorBeacon = $@"^\[{MaidenheadGrid4or6NoSymbols}\](.+)?$";
|
|
|
|
/// <summary>
|
|
/// Matches a Status info field with Maidenhead grid and optional comment (comment separated by a space)
|
|
/// Four matches: Full, full maidenhead, alphanumeric grid, symbols, comment.
|
|
/// </summary>
|
|
public static readonly string StatusWithMaidenheadAndComment = $@"^>({MaidenheadGridWithSymbols}) ?(.+)?$";
|
|
|
|
/// <summary>
|
|
/// Matches a PositionWithoutTimestamp info field.
|
|
/// Seven mathces:
|
|
/// Full
|
|
/// Full lat/long coords and symbols
|
|
/// Latitude
|
|
/// Symbol table
|
|
/// Longitude
|
|
/// Symbol code
|
|
/// Optional comment.
|
|
/// </summary>
|
|
public static readonly string PositionWithoutTimestamp = $@"^[!=]({PositionLatLongWithSymbols})(.+)?$";
|
|
|
|
/// <summary>
|
|
/// Matches a PositionWithTimestamp info field.
|
|
/// 9 mathces:
|
|
/// Full
|
|
/// Packet type symbol (/ or @)
|
|
/// Timestamp
|
|
/// Full position
|
|
/// Latitude
|
|
/// Symbol table
|
|
/// Longitude
|
|
/// Symbol code
|
|
/// Optional comment.
|
|
/// </summary>
|
|
public static readonly string PositionWithTimestamp = $@"^([/@])([0-9]{{6}}[/zh0-9])({PositionLatLongWithSymbols})(.+)?$";
|
|
|
|
/// <summary>
|
|
/// Matches a full TNC2-encoded packet.
|
|
/// 4 matches:
|
|
/// Full
|
|
/// Sender callsign
|
|
/// Path
|
|
/// Info field.
|
|
/// </summary>
|
|
public const string Tnc2Packet = @"^([^>]+)>([^:]+):(.+)$";
|
|
}
|
|
}
|
|
|
|
namespace AprsSharp.Parsers.Aprs
|
|
{
|
|
using System;
|
|
using System.Globalization;
|
|
|
|
/// <summary>
|
|
/// Represents, encodes, and decodes an APRS timestamp.
|
|
/// </summary>
|
|
public class Timestamp
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Timestamp"/> class
|
|
/// by decoding the given timestamp in to it.
|
|
/// </summary>
|
|
/// <param name="timestamp">APRS timestamp.</param>
|
|
public Timestamp(string timestamp)
|
|
{
|
|
Decode(timestamp);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Timestamp"/> class
|
|
/// given a <see cref="DateTime"/> object.
|
|
/// </summary>
|
|
/// <param name="dt">DateTime object to use for this Timestamp.</param>
|
|
public Timestamp(DateTime dt)
|
|
{
|
|
DateTime = dt;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a <see cref="DateTime"/> representing the APRS timestamp.
|
|
/// </summary>
|
|
public DateTime DateTime { get; set; } = DateTime.UtcNow;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a <see cref="TimestampType"/> representing the APRS timestamp type.
|
|
/// </summary>
|
|
public TimestampType DecodedType { get; set; } = TimestampType.NotDecoded;
|
|
|
|
/// <summary>
|
|
/// If we're given a time in DHM, we need to find the correct month and year to fill in for DateTime.
|
|
/// Assuming the day is in the past and the most recent occurance of that day in a month, this function finds the year and month.
|
|
/// </summary>
|
|
/// <param name="day">The day number to find.</param>
|
|
/// <param name="hint">A hint to the timeframe we're looking for. Generally, DateTime.Now.</param>
|
|
/// <param name="year">The year in which the most recent occurance of that day number occured.</param>
|
|
/// <param name="month">The month in which the most recent occurance of that day number occured.</param>
|
|
public static void FindCorrectYearAndMonth(
|
|
int day,
|
|
DateTime hint,
|
|
out int year,
|
|
out int month)
|
|
{
|
|
if (day > 31 || day < 1)
|
|
{
|
|
throw new ArgumentOutOfRangeException("Day must be in range [1, 31], but the passed in day number was " + day);
|
|
}
|
|
|
|
int currYear = hint.Year;
|
|
int currMonth = hint.Month;
|
|
int currDay = hint.Day;
|
|
|
|
// If that day number has already happened this month, we're done!
|
|
if (day <= currDay)
|
|
{
|
|
year = currYear;
|
|
month = currMonth;
|
|
}
|
|
else
|
|
{
|
|
DateTime itrDate = hint;
|
|
while (itrDate.Day != day)
|
|
{
|
|
itrDate = itrDate.AddDays(-1);
|
|
}
|
|
|
|
month = itrDate.Month;
|
|
year = itrDate.Year;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given an hour, minute, and second, determines if this should be the same day as the hint (generally DateTime.Now) or the day before.
|
|
/// </summary>
|
|
/// <param name="hour">Hour of packet.</param>
|
|
/// <param name="minute">Minute of packet.</param>
|
|
/// <param name="second">Second of packet.</param>
|
|
/// <param name="hint">Usually DateTime.Now.</param>
|
|
/// <param name="day">Found day.</param>
|
|
/// <param name="month">Found month.</param>
|
|
/// <param name="year">Found year.</param>
|
|
public static void FindCorrectDayMonthAndYear(
|
|
int hour,
|
|
int minute,
|
|
int second,
|
|
DateTime hint,
|
|
out int day,
|
|
out int month,
|
|
out int year)
|
|
{
|
|
DateTime packetTime = new DateTime(hint.Year, hint.Month, hint.Day, hour, minute, second, hint.Kind);
|
|
TimeSpan diff = hint.Subtract(packetTime);
|
|
|
|
// allow for a few minutes of clock drift between receiver and transmitter
|
|
if (diff.TotalMinutes < -5)
|
|
{
|
|
// assume it's from yesterday
|
|
packetTime = packetTime.AddDays(-1);
|
|
}
|
|
|
|
day = packetTime.Day;
|
|
month = packetTime.Month;
|
|
year = packetTime.Year;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a month, day, hour, and minute, determines the appropriate year (in the past as determiend by hint).
|
|
/// </summary>
|
|
/// <param name="month">Packet month.</param>
|
|
/// <param name="day">Packet day.</param>
|
|
/// <param name="hour">Packet hour.</param>
|
|
/// <param name="minute">Packet minute.</param>
|
|
/// <param name="hint">Usually DateTime.Now.</param>
|
|
/// <param name="year">Found year.</param>
|
|
public static void FindCorrectYear(
|
|
int month,
|
|
int day,
|
|
int hour,
|
|
int minute,
|
|
DateTime hint,
|
|
out int year)
|
|
{
|
|
DateTime packet;
|
|
int numRetries = 0;
|
|
|
|
// Retries on four separate years to find a year
|
|
// that could match the day and month (incl. leap)
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
packet = new DateTime(
|
|
hint.Year - numRetries,
|
|
month,
|
|
day,
|
|
hour,
|
|
minute,
|
|
0,
|
|
hint.Kind);
|
|
|
|
// again, use five minute clock drift
|
|
if (hint.Subtract(packet).TotalMinutes > -5)
|
|
{
|
|
break;
|
|
}
|
|
|
|
++numRetries;
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
++numRetries;
|
|
if (numRetries >= 4)
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
year = packet.Year;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes the data from the stored datetime as a string with the requested type.
|
|
/// </summary>
|
|
/// <param name="type">The APRS <see cref="TimestampType"/>.</param>
|
|
/// <returns>String. 7-8 characters as defined by the APRS spec.</returns>
|
|
public string Encode(TimestampType type)
|
|
{
|
|
return type switch
|
|
{
|
|
TimestampType.DHMz => EncodeDHM(isZulu: true),
|
|
TimestampType.DHMl => EncodeDHM(isZulu: false),
|
|
TimestampType.HMS => EncodeHMS(),
|
|
TimestampType.MDHM => EncodeMDHM(),
|
|
TimestampType.NotDecoded => throw new ArgumentOutOfRangeException(nameof(type), $"Cannot encode to type: {TimestampType.NotDecoded}"),
|
|
_ => throw new NotSupportedException(),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes the APRS Time Format string, detects the formatting, and decodes it in to this object.
|
|
/// </summary>
|
|
/// <param name="timestamp">APRS timestamp.</param>
|
|
public void Decode(string timestamp)
|
|
{
|
|
if (timestamp == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(timestamp));
|
|
}
|
|
else if (timestamp.Length != 7 && timestamp.Length != 8)
|
|
{
|
|
throw new ArgumentException("The given APRS timestamp is " + timestamp.Length + " characters long instead of the required 7-8.");
|
|
}
|
|
|
|
char timeIndicator = timestamp[6];
|
|
|
|
if (timeIndicator == 'z' || timeIndicator == '/')
|
|
{
|
|
DecodeDHM(timestamp);
|
|
}
|
|
else if (timeIndicator == 'h')
|
|
{
|
|
DecodeHMS(timestamp);
|
|
}
|
|
else if (char.IsNumber(timeIndicator))
|
|
{
|
|
DecodeMDHM(timestamp);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException("timestamp was not a valid APRS format (did not have a valid Time Indicator character)");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes to a Day/Hour/Minute (DHM) string in zulu time or local time.
|
|
/// </summary>
|
|
/// <param name="isZulu">If true, encodes in DHM with zulu time, else local time. Zulu time should be used.</param>
|
|
/// <returns>DHM encoded APRS timestamp string.</returns>
|
|
private string EncodeDHM(bool isZulu)
|
|
{
|
|
string encodedPacket = string.Empty;
|
|
DateTime convertedDateTime = isZulu ? DateTime.ToUniversalTime() : DateTime.ToLocalTime();
|
|
|
|
// Add day
|
|
encodedPacket += convertedDateTime.Day.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add hour
|
|
encodedPacket += convertedDateTime.Hour.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add minute
|
|
encodedPacket += convertedDateTime.Minute.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add time indicator
|
|
encodedPacket += isZulu ? "z" : "/";
|
|
|
|
return encodedPacket;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes to an Hour/Minute/Second (HMS) string in zulu time.
|
|
/// </summary>
|
|
/// <returns>HMS encoded APRS timestamp string.</returns>
|
|
private string EncodeHMS()
|
|
{
|
|
string encodedPacket = string.Empty;
|
|
DateTime convertedDateTime = DateTime.ToUniversalTime();
|
|
|
|
// Add hour
|
|
encodedPacket += convertedDateTime.Hour.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add minute
|
|
encodedPacket += convertedDateTime.Minute.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add second
|
|
encodedPacket += convertedDateTime.Second.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add the time indicator
|
|
encodedPacket += "h";
|
|
|
|
return encodedPacket;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes to an Month/Day/Hour/Minute (MDHM) string in zulu time.
|
|
/// </summary>
|
|
/// <returns>MDHM encoded APRS timestamp string.</returns>
|
|
private string EncodeMDHM()
|
|
{
|
|
string encodedPacket = string.Empty;
|
|
DateTime convertedDateTime = DateTime.ToUniversalTime();
|
|
|
|
// Add month
|
|
encodedPacket += convertedDateTime.Month.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add day
|
|
encodedPacket += convertedDateTime.Day.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add hour
|
|
encodedPacket += convertedDateTime.Hour.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
// Add minute
|
|
encodedPacket += convertedDateTime.Minute.ToString("D2", CultureInfo.InvariantCulture);
|
|
|
|
return encodedPacket;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes a Day/Hours/Minutes formatted APRS timestamp and decodes it in to this object.
|
|
/// </summary>
|
|
/// <param name="timestamp">Day/Hours/Minutes formatted APRS timestamp.</param>
|
|
private void DecodeDHM(string timestamp)
|
|
{
|
|
if (timestamp == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(timestamp));
|
|
}
|
|
else if (timestamp.Length != 7)
|
|
{
|
|
throw new ArgumentException("timestamp is not in APRS DHM datetime format. Length is " + timestamp.Length + " when it should be 7");
|
|
}
|
|
|
|
char timeIndicator = timestamp[6];
|
|
|
|
bool wasZuluTime;
|
|
if (timeIndicator == 'z')
|
|
{
|
|
wasZuluTime = true;
|
|
}
|
|
else if (timeIndicator == '/')
|
|
{
|
|
wasZuluTime = false;
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException("timestamp is not in DHM format as time indicator is " + timeIndicator + " when it should be z or /");
|
|
}
|
|
|
|
DecodedType = wasZuluTime ? TimestampType.DHMz : TimestampType.DHMl;
|
|
if (!int.TryParse(timestamp.Substring(0, 6), out _))
|
|
{
|
|
throw new ArgumentException("timestamp contained non-numeric values in the first 6 spaces: " + timestamp);
|
|
}
|
|
|
|
string dayStr = timestamp.Substring(0, 2);
|
|
string hourStr = timestamp.Substring(2, 2);
|
|
string minuteStr = timestamp.Substring(4, 2);
|
|
|
|
int day = int.Parse(dayStr, CultureInfo.InvariantCulture);
|
|
int hour = int.Parse(hourStr, CultureInfo.InvariantCulture);
|
|
int minute = int.Parse(minuteStr, CultureInfo.InvariantCulture);
|
|
DateTime hint = wasZuluTime ? DateTime.UtcNow : DateTime.Now;
|
|
|
|
FindCorrectYearAndMonth(day, hint, out int year, out int month);
|
|
DateTimeKind dtKind = wasZuluTime ? DateTimeKind.Utc : DateTimeKind.Local;
|
|
|
|
DateTime = new DateTime(year, month, day, hour, minute, 0, dtKind);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes an Hours/Minutes/Seconds formatted APRS timestamp and decodes it in to this object.
|
|
/// </summary>
|
|
/// <param name="timestamp">Hours/Minutes/Seconds formatted APRS timestamp.</param>
|
|
private void DecodeHMS(string timestamp)
|
|
{
|
|
if (timestamp == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(timestamp));
|
|
}
|
|
else if (timestamp.Length != 7 || timestamp[6] != 'h')
|
|
{
|
|
throw new ArgumentException("timestamp is not in APRS HMS datetime format. Length is " + timestamp.Length + " when it should be 7 or format marker is " + timestamp[6] + " when it should be 'h'");
|
|
}
|
|
|
|
DecodedType = TimestampType.HMS;
|
|
|
|
string hourStr = timestamp.Substring(0, 2);
|
|
string minuteStr = timestamp.Substring(2, 2);
|
|
string secondStr = timestamp.Substring(4, 2);
|
|
|
|
int hour = int.Parse(hourStr, CultureInfo.InvariantCulture);
|
|
int minute = int.Parse(minuteStr, CultureInfo.InvariantCulture);
|
|
int second = int.Parse(secondStr, CultureInfo.InvariantCulture);
|
|
DateTime hint = DateTime.UtcNow;
|
|
|
|
FindCorrectDayMonthAndYear(
|
|
hour,
|
|
minute,
|
|
second,
|
|
hint,
|
|
out int day,
|
|
out int month,
|
|
out int year);
|
|
|
|
DateTimeKind dtKind = DateTimeKind.Utc;
|
|
DateTime = new DateTime(year, month, day, hour, minute, second, dtKind);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes a Month/Day/Hours/Minutes formmatted APRS timestamp and decodes it in to this object.
|
|
/// </summary>
|
|
/// <param name="timestamp">Month/Day/Hours/Minutes forammted APRS timestamp.</param>
|
|
private void DecodeMDHM(string timestamp)
|
|
{
|
|
if (timestamp == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(timestamp));
|
|
}
|
|
else if (timestamp.Length != 8)
|
|
{
|
|
throw new ArgumentException("timestamp is not in APRS MDHM datetime format. Length is " + timestamp.Length + " when it should be 8");
|
|
}
|
|
|
|
DecodedType = TimestampType.MDHM;
|
|
|
|
string monthStr = timestamp.Substring(0, 2);
|
|
string dayStr = timestamp.Substring(2, 2);
|
|
string hourStr = timestamp.Substring(4, 2);
|
|
string minuteStr = timestamp.Substring(6, 2);
|
|
|
|
int month = int.Parse(monthStr, CultureInfo.InvariantCulture);
|
|
int day = int.Parse(dayStr, CultureInfo.InvariantCulture);
|
|
int hour = int.Parse(hourStr, CultureInfo.InvariantCulture);
|
|
int minute = int.Parse(minuteStr, CultureInfo.InvariantCulture);
|
|
DateTime hint = DateTime.UtcNow;
|
|
|
|
FindCorrectYear(
|
|
month,
|
|
day,
|
|
hour,
|
|
minute,
|
|
hint,
|
|
out int year);
|
|
|
|
DateTimeKind dtKind = DateTimeKind.Utc;
|
|
DateTime = new DateTime(year, month, day, hour, minute, 0 /* second */, dtKind);
|
|
}
|
|
}
|
|
} |