Reference implementation of the frozen IF-LOC v1.0 contract (meta/contracts/ble-device-interface.md): byte-accurate codec for the telemetry, capture-trigger, and capture-result frames, plus a LocatorSimulator that replays a canned locate session over a transport-agnostic loopback link. Unblocks App development without firmware (Risk R-3, SRS S3.1). Plain net9.0 (not a MAUI head) so it builds/runs headless under the QA gate without the Android/iOS workloads; qa-gate.sh auto-discovers *.Tests.csproj under app/tests/. Round-trip test asserts field-level fidelity (every SRS S3.1 telemetry + capture-result field), sentinels, endianness (incl. RFC 4122 big-endian pointId), the REJECTED path, and exactly-once capture correlation (no silent drop, SRS-LOG-7). 8/8 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
4.6 KiB
C#
151 lines
4.6 KiB
C#
namespace IfLoc.Sim;
|
|
|
|
// Reference constants for the frozen IF-LOC v1.0 contract.
|
|
// Source of truth: meta/contracts/ble-device-interface.md. Keep the two in lockstep;
|
|
// enum values and message-type ids are append-only once frozen.
|
|
|
|
/// <summary>Wire message-type ids (frame header byte 1).</summary>
|
|
public enum MessageType : byte
|
|
{
|
|
Telemetry = 0x01,
|
|
CaptureTrigger = 0x02,
|
|
CaptureResult = 0x03,
|
|
Alert = 0x04,
|
|
Ack = 0x05,
|
|
|
|
CmdRequestSnapshot = 0x10,
|
|
CmdSetFrequency = 0x11,
|
|
CmdSetMode = 0x12,
|
|
CmdSetTelemetryRate = 0x13,
|
|
CmdTimeSync = 0x14,
|
|
CmdSetUtility = 0x15,
|
|
|
|
OtaBegin = 0x20,
|
|
OtaData = 0x21,
|
|
OtaEnd = 0x22,
|
|
|
|
UsageLogRequest = 0x30,
|
|
UsageLogRecord = 0x31,
|
|
UsageLogEnd = 0x32,
|
|
|
|
ClaimChallenge = 0x40,
|
|
ClaimAssertion = 0x41,
|
|
ClaimCertReq = 0x42,
|
|
ClaimCert = 0x43,
|
|
}
|
|
|
|
/// <summary>§4.1 locateMode.</summary>
|
|
public enum LocateMode : byte
|
|
{
|
|
Single = 0, Twin = 1, Null = 2, Sweep = 3, TwinSweep = 4, Omni = 5, TwinOmni = 6,
|
|
NotAvailable = 0xFF,
|
|
}
|
|
|
|
/// <summary>§4.2 signalType.</summary>
|
|
public enum SignalType : byte
|
|
{
|
|
Active = 0, LineDropActive = 1, Power = 2, GroupedPower = 3, Cathodic = 4,
|
|
Sonde = 5, Radio = 6, FaultFind = 7, NotAvailable = 0xFF,
|
|
}
|
|
|
|
/// <summary>§4.4 utilityType (APWA-aligned).</summary>
|
|
public enum UtilityType : byte
|
|
{
|
|
None = 0, Gas = 1, Power = 2, Communications = 3, Water = 4, Sewer = 5, Fiber = 6,
|
|
Other = 7, NotAvailable = 0xFF,
|
|
}
|
|
|
|
/// <summary>§4.3 warningFlags bitfield.</summary>
|
|
[Flags]
|
|
public enum WarningFlags : ushort
|
|
{
|
|
None = 0,
|
|
Shallow = 1 << 0,
|
|
Overload = 1 << 1,
|
|
SwingTilt = 1 << 2,
|
|
DepthInvalid = 1 << 3,
|
|
CurrentInvalid = 1 << 4,
|
|
OutOfRange = 1 << 5,
|
|
DistortionHigh = 1 << 6,
|
|
LowBattery = 1 << 7,
|
|
}
|
|
|
|
/// <summary>§4.5 statusFlags bitfield.</summary>
|
|
[Flags]
|
|
public enum StatusFlags : byte
|
|
{
|
|
None = 0,
|
|
Locating = 1 << 0,
|
|
MenuActive = 1 << 1,
|
|
TimeSynced = 1 << 2,
|
|
DepthModeAuto = 1 << 3,
|
|
}
|
|
|
|
/// <summary>§5.1 triggerType.</summary>
|
|
public enum TriggerType : byte
|
|
{
|
|
ButtonSingle = 0, ButtonHold = 1, OffsetRequest = 2, AppInitiated = 3,
|
|
}
|
|
|
|
/// <summary>§5.2 outcome — the deterministic capture outcome (SRS-LOG-7).</summary>
|
|
public enum CaptureOutcome : byte
|
|
{
|
|
Stored = 0, StoredFlagged = 1, Rejected = 2,
|
|
}
|
|
|
|
/// <summary>§5.4 reasonCode / gateStatus.</summary>
|
|
public enum ReasonCode : byte
|
|
{
|
|
Ok = 0,
|
|
FixTypeTooLow = 1,
|
|
HrmsExceeded = 2,
|
|
VrmsExceeded = 3,
|
|
CorrectionAgeExceeded = 4,
|
|
NoActiveTicket = 5,
|
|
ImuCalibrationInvalid = 6,
|
|
HeadingConfidenceLow = 7,
|
|
BufferFull = 8,
|
|
NoPosition = 9,
|
|
WaiverRequired = 10,
|
|
Other = 255,
|
|
}
|
|
|
|
/// <summary>§5.2 fixType (GGA-quality mapping; 3 reserved).</summary>
|
|
public enum FixType : byte
|
|
{
|
|
NoFix = 0, Autonomous = 1, Dgps = 2, RtkFixed = 4, RtkFloat = 5,
|
|
}
|
|
|
|
/// <summary>Frozen wire constants and sentinels.</summary>
|
|
public static class IfLoc
|
|
{
|
|
public const byte FrameVersion = 0x01;
|
|
|
|
// Fixed frame lengths (Appendix A).
|
|
public const int HeaderLen = 4;
|
|
public const int TelemetryFrameLen = 32;
|
|
public const int TelemetryPayloadLen = 28; // offsets 4..31
|
|
public const int CaptureTriggerFrameLen = 36;
|
|
public const int CaptureResultFrameLen = 52;
|
|
|
|
// Sentinels (§1).
|
|
public const short DepthNoValue = unchecked((short)0x8000);
|
|
public const ushort CurrentInvalid = 0xFFFF;
|
|
public const uint FrequencyNa = 0xFFFFFFFF;
|
|
public const short GuidanceNa = unchecked((short)0x8000);
|
|
public const int PositionNoValue = unchecked((int)0x80000000);
|
|
public const ushort RmsUnknown = 0xFFFF;
|
|
|
|
// GATT (§2) — recommended UUIDs the App builds against.
|
|
public const string BaseUuidFormat = "A9E1{0:X4}-1B4C-4F9A-9B7E-2D6F0C3A5E11";
|
|
public static string LocateServiceUuid => string.Format(BaseUuidFormat, 0x0001);
|
|
public static string ProtocolInfoUuid => string.Format(BaseUuidFormat, 0x0002);
|
|
public static string TelemetryUuid => string.Format(BaseUuidFormat, 0x0003);
|
|
public static string EventUuid => string.Format(BaseUuidFormat, 0x0004);
|
|
public static string CommandUuid => string.Format(BaseUuidFormat, 0x0005);
|
|
public static string CaptureResultUuid => string.Format(BaseUuidFormat, 0x0006);
|
|
public static string LinkStateUuid => string.Format(BaseUuidFormat, 0x0007);
|
|
public static string BulkOtaUuid => string.Format(BaseUuidFormat, 0x0008);
|
|
public static string ClaimUuid => string.Format(BaseUuidFormat, 0x0009);
|
|
}
|