using System.Globalization;
namespace FieldLogger.Models;
public enum UmMode { Single = 0, Twin = 1, Null = 2, Sweep = 3, TwinSweep = 4, Omni = 5, TwinOmni = 6 }
public enum UmFreqType { Active = 0, LdActive = 1, Power = 2, GroupedPower = 3, Cathodic = 4, Sonde = 5, Radio = 6, FaultFind = 7 }
public enum UmUtility { None = 0, Gas = 1, Power = 2, Communications = 3, Water = 4, Sewer = 5, Fiber = 6, Other = 7 }
public enum UmBatteryType { NotMeasured = 0, Alkaline = 1, AlkalineError = 2, Lithium = 3 }
///
/// A single push-button data-log packet from the UM locating receiver
/// (PBDL schema 1, 21 comma-delimited fields).
///
public sealed class UmLogPacket
{
public UmMode Mode { get; init; }
public int Frequency { get; init; }
public UmFreqType FreqType { get; init; }
public int Signal { get; init; } // 0 - 99
public int GainDb { get; init; } // 0 - 145 dB
public string DepthRaw { get; init; } = ""; // "X' Y\"" imperial, "X.Yym" metric, "- - -" no depth
public string CurrentRaw { get; init; } = ""; // 0 - 999mA
public int CompassAngle { get; init; } // 0 - 180 degrees (direction ambiguous)
public int GuidanceArrows { get; init; } // -300 to 300, negative = left
public int LdPhase { get; init; }
public bool Clipping { get; init; }
public int DepthCurrentSetting { get; init; } // 0 ON, 1 AUTO
public int LrArrowStyle { get; init; } // 0 OFF, 1 Style 1, 2 Style 2
public int AudioVolume { get; init; } // 0 - 3
public int AudioModulation { get; init; } // 0 AM, 1 FM
public int AudioSound { get; init; } // 0 Smooth, 1 Rough
public double BatteryVoltage { get; init; }
public UmBatteryType BatteryType { get; init; }
public int Backlight { get; init; } // 0 Low, 1 Medium, 2 High
public UmUtility Utility { get; init; }
public bool MenuInUse { get; init; }
public string RawText { get; init; } = "";
public const int FieldCount = 21;
/// Depth converted to meters, when the raw string can be interpreted; otherwise null.
public double? DepthMeters => TryParseDepthMeters(DepthRaw);
/// Current in mA parsed from the raw string, when possible.
public double? CurrentMilliamps
{
get
{
var s = CurrentRaw.Trim().TrimEnd('A', 'a').TrimEnd('m', 'M');
return double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : null;
}
}
public static double? TryParseDepthMeters(string raw)
{
raw = raw.Trim();
if (raw.Length == 0 || raw.Contains("- -"))
return null;
// Metric: "X.Yym" e.g. "1.23m"
if (raw.EndsWith("m", StringComparison.OrdinalIgnoreCase))
{
var s = raw[..^1].Trim();
if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var meters))
return meters;
return null;
}
// Imperial: feet' inches" e.g. "3' 7"" or "3' 7'"
if (raw.Contains('\''))
{
var parts = raw.Split('\'', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length >= 1 &&
double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var feet))
{
double inches = 0;
if (parts.Length >= 2)
{
var inchStr = parts[1].TrimEnd('"');
double.TryParse(inchStr, NumberStyles.Float, CultureInfo.InvariantCulture, out inches);
}
return (feet + inches / 12.0) * 0.3048;
}
}
return null;
}
///
/// Parses a 21-field PBDL schema 1 packet. Returns null if the line does not look like a data packet.
///
public static UmLogPacket? TryParse(string line)
{
var fields = line.Trim().Split(',');
if (fields.Length < FieldCount)
return null;
// First field must be a small integer (Mode 0-6) to qualify as a data packet.
if (!int.TryParse(fields[0], out var mode) || mode is < 0 or > 6)
return null;
try
{
return new UmLogPacket
{
Mode = (UmMode)mode,
Frequency = ParseInt(fields[1]),
FreqType = (UmFreqType)ParseInt(fields[2]),
Signal = ParseInt(fields[3]),
GainDb = ParseInt(fields[4]),
DepthRaw = fields[5].Trim(),
CurrentRaw = fields[6].Trim(),
CompassAngle = ParseInt(fields[7]),
GuidanceArrows = ParseInt(fields[8]),
LdPhase = ParseInt(fields[9]),
Clipping = ParseInt(fields[10]) != 0,
DepthCurrentSetting = ParseInt(fields[11]),
LrArrowStyle = ParseInt(fields[12]),
AudioVolume = ParseInt(fields[13]),
AudioModulation = ParseInt(fields[14]),
AudioSound = ParseInt(fields[15]),
BatteryVoltage = ParseDouble(fields[16]),
BatteryType = (UmBatteryType)ParseInt(fields[17]),
Backlight = ParseInt(fields[18]),
Utility = (UmUtility)ParseInt(fields[19]),
MenuInUse = ParseInt(fields[20]) != 0,
RawText = line.Trim(),
};
}
catch (FormatException)
{
return null;
}
}
private static int ParseInt(string s) => int.Parse(s.Trim(), CultureInfo.InvariantCulture);
private static double ParseDouble(string s) => double.Parse(s.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture);
}