using IfLoc.Sim; using Xunit; namespace IfLoc.Sim.Tests; /// /// QA-owned boundary/edge-case vectors for the frozen IF-LOC v1.0 codec (S1-b, SRS ยง3.1). /// Complements (nominal fidelity + capture round-trip) by /// pinning the min/max/resolution limits of each wire field via the pure /// EncodePayload()/EncodeFrame()/DecodeFrame() seam. Values are chosen to stay clear of the /// no-value sentinels; the exact sentinel-collision boundaries are characterised at the /// bottom so the contract's documented ranges are enforceable, not just implied. /// public class BoundaryTests { // --- Telemetry: depth (int16 centimetres, 0.01 m resolution, sentinel 0x8000) -------- [Theory] [InlineData(0.0)] [InlineData(0.01)] // one resolution step [InlineData(-0.01)] [InlineData(12.34)] [InlineData(327.67)] // max representable magnitude (32767 cm) [InlineData(-327.67)] // min valid magnitude (-32767 cm); -327.68 would alias the sentinel public void Depth_roundtrips_at_range_and_resolution_limits(double meters) { var back = Telemetry.DecodePayload(new Telemetry { DepthMeters = meters }.EncodePayload()); Assert.NotNull(back.DepthMeters); Assert.Equal(meters, back.DepthMeters!.Value, 2); } // --- Telemetry: signal current (uint16 mA, sentinel 0xFFFF) --------------------------- [Theory] [InlineData((ushort)0)] [InlineData((ushort)1)] [InlineData((ushort)65534)] // max valid; 65535 == CurrentInvalid sentinel public void SignalCurrent_roundtrips_to_max_valid(ushort mA) { var back = Telemetry.DecodePayload(new Telemetry { SignalCurrentMa = mA }.EncodePayload()); Assert.Equal(mA, back.SignalCurrentMa); } // --- Telemetry: frequency (uint32 Hz, sentinel 0xFFFFFFFF) ---------------------------- [Theory] [InlineData(0u)] [InlineData(82_500u)] [InlineData(4_294_967_294u)] // 0xFFFFFFFE โ€” max valid; 0xFFFFFFFF is n/a sentinel public void Frequency_roundtrips_to_max_valid(uint hz) { var back = Telemetry.DecodePayload(new Telemetry { FrequencyHz = hz }.EncodePayload()); Assert.Equal(hz, back.FrequencyHz); } // --- Telemetry: guidance offset (int16, documented range -1000..+1000; sentinel 0x8000) -- [Theory] [InlineData((short)-1000)] [InlineData((short)-1)] [InlineData((short)0)] [InlineData((short)1)] [InlineData((short)1000)] [InlineData((short)-32767)] // wire minimum (just above the sentinel) [InlineData((short)32767)] // wire maximum public void GuidanceOffset_roundtrips_across_range(short offset) { var back = Telemetry.DecodePayload(new Telemetry { GuidanceOffset = offset }.EncodePayload()); Assert.Equal(offset, back.GuidanceOffset); } // --- Telemetry: single-byte fields at 0 and 0xFF ------------------------------------- [Theory] [InlineData((byte)0)] [InlineData((byte)100)] [InlineData((byte)255)] public void Byte_fields_roundtrip_at_extremes(byte v) { var t = new Telemetry { GainDb = v, SignalLevel = v, DistortionQualityPct = v, SignalDirection = v, CompassAngleDeg = v, BatteryPercent = v, }; var back = Telemetry.DecodePayload(t.EncodePayload()); Assert.Equal(v, back.GainDb); Assert.Equal(v, back.SignalLevel); Assert.Equal(v, back.DistortionQualityPct); Assert.Equal(v, back.SignalDirection); Assert.Equal(v, back.CompassAngleDeg); Assert.Equal(v, back.BatteryPercent); } // --- Telemetry: full bitfields set --------------------------------------------------- [Fact] public void All_warning_and_status_flags_roundtrip() { var allWarn = WarningFlags.Shallow | WarningFlags.Overload | WarningFlags.SwingTilt | WarningFlags.DepthInvalid | WarningFlags.CurrentInvalid | WarningFlags.OutOfRange | WarningFlags.DistortionHigh | WarningFlags.LowBattery; var allStatus = StatusFlags.Locating | StatusFlags.MenuActive | StatusFlags.TimeSynced | StatusFlags.DepthModeAuto; var back = Telemetry.DecodePayload( new Telemetry { Warnings = allWarn, Status = allStatus }.EncodePayload()); Assert.Equal(allWarn, back.Warnings); Assert.Equal(allStatus, back.Status); } // --- CaptureResult: WGS84 position extremes (int32 * 1e7) ----------------------------- [Theory] [InlineData(90.0, 180.0)] [InlineData(-90.0, -180.0)] [InlineData(0.0, 0.0)] [InlineData(45.7649321, 4.8354792)] public void CaptureResult_position_roundtrips_at_wgs84_extremes(double lat, double lon) { var r = new CaptureResult { Outcome = CaptureOutcome.Stored, Reason = ReasonCode.Ok, FixType = FixType.RtkFixed, Lat = lat, Lon = lon, OrthometricHeightM = 0.0, }; var back = CaptureResult.DecodeFrame(r.EncodeFrame(0)); Assert.Equal(lat, back.Lat!.Value, 7); Assert.Equal(lon, back.Lon!.Value, 7); } // --- CaptureResult: RMS accuracy (uint16 mm, sentinel 0xFFFF) ------------------------- [Theory] [InlineData(0.0)] [InlineData(0.001)] // 1 mm resolution step [InlineData(65.534)] // 65534 mm โ€” max valid; 65535 == RmsUnknown sentinel public void CaptureResult_rms_roundtrips_to_max_valid(double meters) { var r = new CaptureResult { Outcome = CaptureOutcome.Stored, Reason = ReasonCode.Ok, FixType = FixType.RtkFixed, Lat = 0, Lon = 0, OrthometricHeightM = 0, HrmsM = meters, VrmsM = meters, }; var back = CaptureResult.DecodeFrame(r.EncodeFrame(0)); Assert.Equal(meters, back.HrmsM!.Value, 3); Assert.Equal(meters, back.VrmsM!.Value, 3); } // --- Characterisation of the sentinel-collision boundaries --------------------------- // These pin the *edges* of the representable ranges so the frozen contract documents // them explicitly. A magnitude one step beyond the max valid value aliases the field's // no-value sentinel and therefore decodes to null โ€” i.e. it is NOT representable. Flagged // to app-owner for the range notes in ble-device-interface.md (IF-LOC v1.0). [Fact] public void Depth_negative_full_scale_aliases_the_no_value_sentinel() { // -327.68 m -> -32768 cm == DepthNoValue (0x8000): not a representable depth. var back = Telemetry.DecodePayload(new Telemetry { DepthMeters = -327.68 }.EncodePayload()); Assert.Null(back.DepthMeters); } [Fact] public void SignalCurrent_0xFFFF_aliases_the_invalid_sentinel() { var back = Telemetry.DecodePayload(new Telemetry { SignalCurrentMa = 0xFFFF }.EncodePayload()); Assert.Null(back.SignalCurrentMa); } }