Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CGMBLEKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
438621CE2292074A00741DFE /* ShareClientUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4325E9EE210EAF3F00969CE5 /* ShareClientUI.framework */; };
43880F981D9E19FC009061A8 /* TransmitterVersionRxMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43880F971D9E19FC009061A8 /* TransmitterVersionRxMessage.swift */; };
43880F9A1D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43880F991D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift */; };
CA04000000000000000010C2 /* AnubisDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA04000000000000000010C1 /* AnubisDetectionTests.swift */; };
43A8EC4A210D09BE00A81379 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43A8EC49210D09BE00A81379 /* LoopKitUI.framework */; };
43A8EC4C210D09DA00A81379 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43A8EC49210D09BE00A81379 /* LoopKitUI.framework */; };
43A8EC56210D0A7400A81379 /* CGMBLEKitUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 43A8EC54210D0A7400A81379 /* CGMBLEKitUI.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand Down Expand Up @@ -391,6 +392,7 @@
43846AC71D8F89BE00799272 /* CalibrationDataRxMessageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationDataRxMessageTests.swift; sourceTree = "<group>"; };
43880F971D9E19FC009061A8 /* TransmitterVersionRxMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransmitterVersionRxMessage.swift; sourceTree = "<group>"; };
43880F991D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransmitterVersionRxMessageTests.swift; sourceTree = "<group>"; };
CA04000000000000000010C1 /* AnubisDetectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnubisDetectionTests.swift; sourceTree = "<group>"; };
43A8EC49210D09BE00A81379 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
43A8EC52210D0A7400A81379 /* CGMBLEKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CGMBLEKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
43A8EC54210D0A7400A81379 /* CGMBLEKitUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CGMBLEKitUI.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -730,6 +732,7 @@
43460F87200B30D10030C0E3 /* TransmitterIDTests.swift */,
43F82BCB1D035AA4006F5DD7 /* TransmitterTimeRxMessageTests.swift */,
43880F991D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift */,
CA04000000000000000010C1 /* AnubisDetectionTests.swift */,
);
path = CGMBLEKitTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -1281,6 +1284,7 @@
43F82BD21D037040006F5DD7 /* SessionStopRxMessageTests.swift in Sources */,
43E397911D5692080028E321 /* GlucoseTests.swift in Sources */,
43880F9A1D9E1BD7009061A8 /* TransmitterVersionRxMessageTests.swift in Sources */,
CA04000000000000000010C2 /* AnubisDetectionTests.swift in Sources */,
43DC87C21C8B520F005BC30D /* GlucoseRxMessageTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
118 changes: 118 additions & 0 deletions CGMBLEKitTests/AnubisDetectionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// AnubisDetectionTests.swift
// CGMBLEKit
//
// Covers transmitter-expiry parsing + Anubis-mod detection across the two
// layers it spans: the version-rx frame decoder and the persisted manager
// state.
//

import XCTest
@testable import CGMBLEKit

class AnubisDetectionTests: XCTestCase {

// MARK: - TransmitterVersionRxMessage parsing

/// Stock G6 reports a 90-day lifetime in the version-rx frame.
func testStockG6ExpiryParsedAndNotAnubis() {
// Same layout as TransmitterVersionRxMessageTests' fixture; expiry
// bytes (13..14, little-endian) overwritten to 0x5A 0x00 = 90.
let data = makeVersionRxPayload(expiryDays: 90)
let message = TransmitterVersionRxMessage(data: data)!

XCTAssertEqual(90, message.transmitterExpiryInDays)
XCTAssertFalse(message.isAnubis)
}

/// Anubis-modded G6 reports 180 days. Mirrors xDrip4iOS's heuristic.
func testAnubisG6ExpiryParsedAndIsAnubis() {
let data = makeVersionRxPayload(expiryDays: 180)
let message = TransmitterVersionRxMessage(data: data)!

XCTAssertEqual(180, message.transmitterExpiryInDays)
XCTAssertTrue(message.isAnubis)
}

/// Some early transmitters report neither 90 nor 180. They're stock G6
/// firmware that just doesn't expose the lifetime field reliably; the
/// only known classification we care about is "is this a 180-day Anubis."
func testOddExpiryNotClassifiedAsAnubis() {
let data = makeVersionRxPayload(expiryDays: 112)
let message = TransmitterVersionRxMessage(data: data)!

XCTAssertEqual(112, message.transmitterExpiryInDays)
XCTAssertFalse(message.isAnubis)
}

func testWrongOpcodeReturnsNil() {
var data = makeVersionRxPayload(expiryDays: 90)
data[0] = 0x4c // not .transmitterVersionRx
// Re-CRC so the opcode rejection is exercised, not a CRC failure.
let resealed = data.dropLast(2).appendingCRC()
XCTAssertNil(TransmitterVersionRxMessage(data: resealed))
}

func testInvalidCRCReturnsNil() {
var data = makeVersionRxPayload(expiryDays: 90)
// Flip the last byte of the CRC trailer.
data[data.count - 1] ^= 0xFF
XCTAssertNil(TransmitterVersionRxMessage(data: data))
}

func testWrongLengthReturnsNil() {
let data = makeVersionRxPayload(expiryDays: 90).dropLast()
XCTAssertNil(TransmitterVersionRxMessage(data: Data(data)))
}

// MARK: - TransmitterManagerState round-trip

func testManagerStateStoresAndRestoresExpiry() {
let state = TransmitterManagerState(transmitterID: "ABCDEF", transmitterExpiryInDays: 180)
let restored = TransmitterManagerState(rawValue: state.rawValue)!

XCTAssertEqual(180, restored.transmitterExpiryInDays)
XCTAssertTrue(restored.isAnubis)
}

/// Older installs persisted the expiry as a plain `Int` (UserDefaults
/// rounds UInt16 → Int on archive). The decoder accepts both shapes so an
/// upgrade doesn't drop the Anubis flag.
func testManagerStateRestoresLegacyIntExpiry() {
let raw: [String: Any] = [
"transmitterID": "ABCDEF",
"transmitterExpiryInDays": Int(180)
]
let restored = TransmitterManagerState(rawValue: raw)!

XCTAssertEqual(180, restored.transmitterExpiryInDays)
XCTAssertTrue(restored.isAnubis)
}

func testManagerStateMissingExpiryIsNotAnubis() {
let state = TransmitterManagerState(transmitterID: "ABCDEF")

XCTAssertNil(state.transmitterExpiryInDays)
XCTAssertFalse(state.isAnubis)
}

// MARK: - Fixture helper

/// Builds a 19-byte version-rx payload with the given expiry, opcode +
/// CRC trailer correctly applied. Patterned on the canonical fixture in
/// `TransmitterVersionRxMessageTests`.
private func makeVersionRxPayload(expiryDays: UInt16) -> Data {
// Bytes 0..12: opcode + status + firmware + filler from the canonical
// existing fixture. Bytes 13..14: expiry (little-endian). Bytes
// 15..16: trailing filler. CRC re-computed at the end.
let body = Data([
0x4b, 0x00, // opcode + status
0x01, 0x00, 0x00, 0x11, // firmwareVersion
0xdf, 0x29, 0x00, 0x00, 0x51, 0x00, 0x03, // filler (bytes 6..12)
UInt8(expiryDays & 0xff), // byte 13: low byte
UInt8((expiryDays >> 8) & 0xff), // byte 14: high byte
0xf0, 0x00 // filler (bytes 15..16)
])
return body.appendingCRC()
}
}
21 changes: 21 additions & 0 deletions CGMBLEKitUI/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@
}
}
},
"Calibration Error" : {
"comment" : "Sensor calibration error"
},
"Calibration Needed" : {
"comment" : "Sensor needs calibration"
},
"Cancel" : {
"comment" : "The title of the cancel action in an action sheet",
"localizations" : {
Expand Down Expand Up @@ -2088,6 +2094,12 @@
}
}
},
"Sensor Failed" : {
"comment" : "Sensor hardware failure"
},
"Sensor Stopped" : {
"comment" : "Sensor session stopped"
},
"Session Age" : {
"comment" : "Title describing sensor session age",
"localizations" : {
Expand Down Expand Up @@ -2237,6 +2249,12 @@
}
}
},
"Session Failed" : {
"comment" : "Sensor session failed"
},
"Signal Problem" : {
"comment" : "Sensor signal problem"
},
"Status" : {
"comment" : "Title describing CGM calibration and battery state",
"localizations" : {
Expand Down Expand Up @@ -2981,6 +2999,9 @@
}
}
}
},
"Warming Up" : {
"comment" : "Sensor warmup status"
}
},
"version" : "1.0"
Expand Down