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); }