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 } public enum UmLogType { PushButton = 0, TimeBased = 1 } /// /// A single data-log packet from the UM locating receiver. /// Schema 1 (push-button): 21 fields /// Schema 2 (timed logging): 22 fields with LogType as first field /// public sealed class UmLogPacket { public UmLogType? LogType { get; init; } // Schema 2 only 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 Schema1FieldCount = 21; public const int Schema2FieldCount = 22; /// 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 either Schema 1 (21 fields, push-button) or Schema 2 (22 fields, timed logging). /// Returns null if the line does not look like a data packet. /// public static UmLogPacket? TryParse(string line) { var fields = line.Trim().Split(','); // Try Schema 2 (22 fields with LogType) if (fields.Length >= Schema2FieldCount) { if (int.TryParse(fields[0], out var logType) && logType is 0 or 1) { if (int.TryParse(fields[1], out var mode) && mode is >= 0 and <= 6) { return ParseSchema2(fields, (UmLogType)logType, line); } } } // Try Schema 1 (21 fields without LogType) if (fields.Length >= Schema1FieldCount) { if (int.TryParse(fields[0], out var mode) && mode is >= 0 and <= 6) { return ParseSchema1(fields, line); } } return null; } private static UmLogPacket? ParseSchema1(string[] fields, string line) { try { return new UmLogPacket { LogType = null, // Schema 1 doesn't have LogType Mode = (UmMode)ParseInt(fields[0]), 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 UmLogPacket? ParseSchema2(string[] fields, UmLogType logType, string line) { try { // Schema 2 has LogType at index 0, then same fields as Schema 1 shifted by 1 return new UmLogPacket { LogType = logType, Mode = (UmMode)ParseInt(fields[1]), Frequency = ParseInt(fields[2]), FreqType = (UmFreqType)ParseInt(fields[3]), Signal = ParseInt(fields[4]), GainDb = ParseInt(fields[5]), DepthRaw = fields[6].Trim(), CurrentRaw = fields[7].Trim(), CompassAngle = ParseInt(fields[8]), GuidanceArrows = ParseInt(fields[9]), LdPhase = ParseInt(fields[10]), Clipping = ParseInt(fields[11]) != 0, DepthCurrentSetting = ParseInt(fields[12]), LrArrowStyle = ParseInt(fields[13]), AudioVolume = ParseInt(fields[14]), AudioModulation = ParseInt(fields[15]), AudioSound = ParseInt(fields[16]), BatteryVoltage = ParseDouble(fields[17]), BatteryType = (UmBatteryType)ParseInt(fields[18]), Backlight = ParseInt(fields[19]), Utility = (UmUtility)ParseInt(fields[20]), MenuInUse = ParseInt(fields[21]) != 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); }