diff --git a/BadBird.xcodeproj/project.pbxproj b/BadBird.xcodeproj/project.pbxproj index 900046a..f876839 100644 --- a/BadBird.xcodeproj/project.pbxproj +++ b/BadBird.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A1B2C3D4E5F6A7B8C9D0E1F4 /* BadBirdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F2 /* BadBirdTests.swift */; }; C3B98B6F22130F0000124A45 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B98B6322130EFF00124A45 /* AppDelegate.swift */; }; C3B98B7022130F0000124A45 /* PeripheralTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B98B6422130EFF00124A45 /* PeripheralTableViewCell.swift */; }; C3B98B7122130F0000124A45 /* UartModuleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B98B6522130EFF00124A45 /* UartModuleViewController.swift */; }; @@ -18,7 +19,19 @@ C3B98B8222130F0000124A45 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C3B98B6922130EFF00124A45 /* Assets.xcassets */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + A1B2C3D4E5F6A7B8C9D0E1FD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D5B24DD01DEE393B00D33B5A /* Project object */; + proxyType = 1; + remoteGlobalIDString = D5B24DD71DEE393B00D33B5A; + remoteInfo = BadBird; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + A1B2C3D4E5F6A7B8C9D0E1F2 /* BadBirdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadBirdTests.swift; sourceTree = ""; }; + A1B2C3D4E5F6A7B8C9D0E1F3 /* BadBirdTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BadBirdTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C3B98B6322130EFF00124A45 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C3B98B6422130EFF00124A45 /* PeripheralTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeripheralTableViewCell.swift; sourceTree = ""; }; C3B98B6522130EFF00124A45 /* UartModuleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UartModuleViewController.swift; sourceTree = ""; }; @@ -32,6 +45,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + A1B2C3D4E5F6A7B8C9D0E1F7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D5B24DD51DEE393B00D33B5A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -42,10 +62,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + A1B2C3D4E5F6A7B8C9D0E1F5 /* BadBirdTests */ = { + isa = PBXGroup; + children = ( + A1B2C3D4E5F6A7B8C9D0E1F2 /* BadBirdTests.swift */, + ); + path = BadBirdTests; + sourceTree = ""; + }; D5B24DCF1DEE393B00D33B5A = { isa = PBXGroup; children = ( D5B24DDA1DEE393B00D33B5A /* BadBird */, + A1B2C3D4E5F6A7B8C9D0E1F5 /* BadBirdTests */, D5B24DD91DEE393B00D33B5A /* Products */, ); sourceTree = ""; @@ -54,6 +83,7 @@ isa = PBXGroup; children = ( D5B24DD81DEE393B00D33B5A /* BadBird.app */, + A1B2C3D4E5F6A7B8C9D0E1F3 /* BadBirdTests.xctest */, ); name = Products; sourceTree = ""; @@ -77,6 +107,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + A1B2C3D4E5F6A7B8C9D0E1F8 /* BadBirdTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A1B2C3D4E5F6A7B8C9D0E1FB /* Build configuration list for PBXNativeTarget "BadBirdTests" */; + buildPhases = ( + A1B2C3D4E5F6A7B8C9D0E1F6 /* Sources */, + A1B2C3D4E5F6A7B8C9D0E1F7 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + A1B2C3D4E5F6A7B8C9D0E1FC /* PBXTargetDependency */, + ); + name = BadBirdTests; + productName = BadBirdTests; + productReference = A1B2C3D4E5F6A7B8C9D0E1F3 /* BadBirdTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; D5B24DD71DEE393B00D33B5A /* BadBird */ = { isa = PBXNativeTarget; buildConfigurationList = D5B24DEA1DEE393B00D33B5A /* Build configuration list for PBXNativeTarget "BadBird" */; @@ -105,6 +152,10 @@ LastUpgradeCheck = 2630; ORGANIZATIONNAME = "Vanguard Logic LLC"; TargetAttributes = { + A1B2C3D4E5F6A7B8C9D0E1F8 = { + CreatedOnToolsVersion = 16.0; + TestTargetID = D5B24DD71DEE393B00D33B5A; + }; D5B24DD71DEE393B00D33B5A = { CreatedOnToolsVersion = 15.0; ProvisioningStyle = Automatic; @@ -125,6 +176,7 @@ projectRoot = ""; targets = ( D5B24DD71DEE393B00D33B5A /* BadBird */, + A1B2C3D4E5F6A7B8C9D0E1F8 /* BadBirdTests */, ); }; /* End PBXProject section */ @@ -143,6 +195,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + A1B2C3D4E5F6A7B8C9D0E1F6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1B2C3D4E5F6A7B8C9D0E1F4 /* BadBirdTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D5B24DD41DEE393B00D33B5A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -158,7 +218,61 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + A1B2C3D4E5F6A7B8C9D0E1FC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D5B24DD71DEE393B00D33B5A /* BadBird */; + targetProxy = A1B2C3D4E5F6A7B8C9D0E1FD /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + A1B2C3D4E5F6A7B8C9D0E1F9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = R87JLM4BQL; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Zimperium.BadBirdTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 6.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BadBird.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/BadBird"; + }; + name = Debug; + }; + A1B2C3D4E5F6A7B8C9D0E1FA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = R87JLM4BQL; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Zimperium.BadBirdTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 6.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BadBird.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/BadBird"; + }; + name = Release; + }; D5B24DE81DEE393B00D33B5A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -289,6 +403,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = Zimperium.BadBird; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0; }; name = Debug; @@ -305,6 +420,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = Zimperium.BadBird; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0; }; name = Release; @@ -312,6 +428,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + A1B2C3D4E5F6A7B8C9D0E1FB /* Build configuration list for PBXNativeTarget "BadBirdTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1B2C3D4E5F6A7B8C9D0E1F9 /* Debug */, + A1B2C3D4E5F6A7B8C9D0E1FA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D5B24DD31DEE393B00D33B5A /* Build configuration list for PBXProject "BadBird" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/BadBird.xcodeproj/xcshareddata/xcschemes/BadBirdTests.xcscheme b/BadBird.xcodeproj/xcshareddata/xcschemes/BadBirdTests.xcscheme new file mode 100644 index 0000000..3e985a0 --- /dev/null +++ b/BadBird.xcodeproj/xcshareddata/xcschemes/BadBirdTests.xcscheme @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BadBird/BLECentralViewController.swift b/BadBird/BLECentralViewController.swift index 56abdc0..7d6a7d5 100644 --- a/BadBird/BLECentralViewController.swift +++ b/BadBird/BLECentralViewController.swift @@ -9,21 +9,29 @@ import Foundation import UIKit import CoreBluetooth - -// Shared BLE state accessible from @MainActor context -@MainActor var txCharacteristic: CBCharacteristic? -@MainActor var rxCharacteristic: CBCharacteristic? -@MainActor var blePeripheral: CBPeripheral? -@MainActor var characteristicASCIIValue = "" -@MainActor var isLocked = true +import os + +@MainActor +final class BLEConnectionState { + static let shared = BLEConnectionState() + init() {} + + var txCharacteristic: CBCharacteristic? + var rxCharacteristic: CBCharacteristic? + var peripheral: CBPeripheral? + var lastReceivedValue = "" + var isLocked = true +} class BLECentralViewController: UIViewController, @preconcurrency CBCentralManagerDelegate, @preconcurrency CBPeripheralDelegate, UITableViewDelegate, UITableViewDataSource { + private static let logger = Logger(subsystem: "com.mi365locker", category: "BLE") // MARK: - Data var centralManager: CBCentralManager! var rssiValues: [NSNumber] = [] var peripherals: [CBPeripheral] = [] var scanTimer: Timer? + private let scanTimeoutInterval: TimeInterval = 17 // MARK: - UI @IBOutlet weak var baseTableView: UITableView! @@ -52,12 +60,12 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag disconnectFromDevice() super.viewDidAppear(animated) refreshScanView() - print("View Cleared") + Self.logger.debug("View Cleared") } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - print("Stop Scanning") + Self.logger.debug("Stop Scanning") centralManager?.stopScan() scanTimer?.invalidate() scanTimer = nil @@ -67,18 +75,17 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag func startScan() { peripherals = [] - isLocked = !isLocked - print(isLocked ? "Now Locking..." : "Now Unlocking...") + rssiValues = [] scanTimer?.invalidate() - centralManager?.scanForPeripherals(withServices: [BLEService_UUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) - scanTimer = Timer.scheduledTimer(timeInterval: 17, target: self, selector: #selector(cancelScan), userInfo: nil, repeats: false) + centralManager?.scanForPeripherals(withServices: [BLEUUIDs.service], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) + scanTimer = Timer.scheduledTimer(timeInterval: scanTimeoutInterval, target: self, selector: #selector(cancelScan), userInfo: nil, repeats: false) } @objc func cancelScan() { centralManager?.stopScan() - print("Scan Stopped") - print("Number of Peripherals Found: \(peripherals.count)") + Self.logger.debug("Scan Stopped") + Self.logger.info("Number of Peripherals Found: \(self.peripherals.count, privacy: .public)") } func refreshScanView() { @@ -88,7 +95,7 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag // MARK: - Connection Management func disconnectFromDevice() { - guard let peripheral = blePeripheral else { return } + guard let peripheral = BLEConnectionState.shared.peripheral else { return } centralManager?.cancelPeripheralConnection(peripheral) } @@ -97,8 +104,8 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag } func connectToDevice() { - guard let peripheral = blePeripheral else { - print("No peripheral to connect to") + guard let peripheral = BLEConnectionState.shared.peripheral else { + Self.logger.error("No peripheral to connect to") return } centralManager?.connect(peripheral, options: nil) @@ -116,19 +123,18 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - print("*****************************") - print("Connection complete") - print("Peripheral info: \(String(describing: peripheral))") + Self.logger.info("Connection complete") + Self.logger.debug("Peripheral info: \(String(describing: peripheral), privacy: .public)") centralManager?.stopScan() - print("Scan Stopped") + Self.logger.debug("Scan Stopped") peripheral.delegate = self - peripheral.discoverServices([BLEService_UUID]) + peripheral.discoverServices([BLEUUIDs.service]) let storyboard = UIStoryboard(name: "Main", bundle: nil) guard let uartViewController = storyboard.instantiateViewController(withIdentifier: "UartModuleViewController") as? UartModuleViewController else { - print("Failed to instantiate UartModuleViewController") + Self.logger.error("Failed to instantiate UartModuleViewController") return } uartViewController.peripheral = peripheral @@ -137,104 +143,111 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: (any Error)?) { if let error { - print("Failed to connect to peripheral: \(error.localizedDescription)") + Self.logger.error("Failed to connect to peripheral: \(error.localizedDescription, privacy: .public)") } } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: (any Error)?) { - print("Disconnected") + Self.logger.info("Disconnected") + BLEConnectionState.shared.peripheral = nil + BLEConnectionState.shared.txCharacteristic = nil + BLEConnectionState.shared.rxCharacteristic = nil } // MARK: - CBPeripheralDelegate func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: (any Error)?) { if let error { - print("Error discovering services: \(error.localizedDescription)") + Self.logger.error("Error discovering services: \(error.localizedDescription, privacy: .public)") return } guard let services = peripheral.services else { return } for service in services { - peripheral.discoverCharacteristics([BLE_Characteristic_uuid_Tx, BLE_Characteristic_uuid_Rx], for: service) + peripheral.discoverCharacteristics([BLEUUIDs.tx, BLEUUIDs.rx], for: service) } - print("Discovered Services: \(services)") + Self.logger.debug("Discovered Services: \(String(describing: services), privacy: .public)") } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: (any Error)?) { if let error { - print("Error discovering characteristics: \(error.localizedDescription)") + Self.logger.error("Error discovering characteristics: \(error.localizedDescription, privacy: .public)") return } guard let characteristics = service.characteristics else { return } - print("Found \(characteristics.count) characteristics!") + Self.logger.info("Found \(characteristics.count, privacy: .public) characteristics!") for characteristic in characteristics { - if characteristic.uuid.isEqual(BLE_Characteristic_uuid_Rx) { - rxCharacteristic = characteristic + if characteristic.uuid.isEqual(BLEUUIDs.rx) { + BLEConnectionState.shared.rxCharacteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) peripheral.readValue(for: characteristic) - print("Rx Characteristic: \(characteristic.uuid)") + Self.logger.debug("Rx Characteristic: \(characteristic.uuid.uuidString, privacy: .public)") } - if characteristic.uuid.isEqual(BLE_Characteristic_uuid_Tx) { - txCharacteristic = characteristic - print("Tx Characteristic: \(characteristic.uuid)") + if characteristic.uuid.isEqual(BLEUUIDs.tx) { + BLEConnectionState.shared.txCharacteristic = characteristic + Self.logger.debug("Tx Characteristic: \(characteristic.uuid.uuidString, privacy: .public)") } peripheral.discoverDescriptors(for: characteristic) } } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: (any Error)?) { - if characteristic == rxCharacteristic { + if let error { + Self.logger.error("Error reading characteristic: \(error.localizedDescription, privacy: .public)") + return + } + if characteristic == BLEConnectionState.shared.rxCharacteristic { guard let value = characteristic.value, let asciiString = String(data: value, encoding: .utf8) else { return } - characteristicASCIIValue = asciiString - print("Value Received: \(asciiString)") + BLEConnectionState.shared.lastReceivedValue = asciiString + Self.logger.debug("Value Received: \(asciiString, privacy: .public)") NotificationCenter.default.post(name: NSNotification.Name(rawValue: "Notify"), object: nil) } } func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: (any Error)?) { if let error { - print("\(error.localizedDescription)") + Self.logger.error("\(error.localizedDescription, privacy: .public)") return } guard let descriptors = characteristic.descriptors else { return } for descriptor in descriptors { - print("Descriptor: \(String(describing: descriptor.description))") - print("Rx Value \(String(describing: rxCharacteristic?.value))") - print("Tx Value \(String(describing: txCharacteristic?.value))") + Self.logger.debug("Descriptor: \(String(describing: descriptor.description), privacy: .public)") + Self.logger.debug("Rx Value \(String(describing: BLEConnectionState.shared.rxCharacteristic?.value), privacy: .public)") + Self.logger.debug("Tx Value \(String(describing: BLEConnectionState.shared.txCharacteristic?.value), privacy: .public)") } } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: (any Error)?) { if let error { - print("Error changing notification state: \(error.localizedDescription)") + Self.logger.error("Error changing notification state: \(error.localizedDescription, privacy: .public)") } else { - print("Characteristic's value subscribed") + Self.logger.debug("Characteristic's value subscribed") } if characteristic.isNotifying { - print("Subscribed. Notification has begun for: \(characteristic.uuid)") + Self.logger.debug("Subscribed. Notification has begun for: \(characteristic.uuid.uuidString, privacy: .public)") } } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: (any Error)?) { guard error == nil else { - print("Error writing value: \(error!.localizedDescription)") + Self.logger.error("Error writing value: \(error!.localizedDescription, privacy: .public)") return } - print("Message sent") + Self.logger.debug("Message sent") } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: (any Error)?) { guard error == nil else { - print("Error writing descriptor: \(error!.localizedDescription)") + Self.logger.error("Error writing descriptor: \(error!.localizedDescription, privacy: .public)") return } - print("Succeeded!") + Self.logger.debug("Succeeded!") } // MARK: - UITableViewDataSource @@ -257,7 +270,7 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - blePeripheral = peripherals[indexPath.row] + BLEConnectionState.shared.peripheral = peripherals[indexPath.row] connectToDevice() } @@ -265,10 +278,10 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == .poweredOn { - print("Bluetooth Enabled") + Self.logger.info("Bluetooth Enabled") startScan() } else { - print("Bluetooth Disabled- Make sure your Bluetooth is turned on") + Self.logger.warning("Bluetooth Disabled- Make sure your Bluetooth is turned on") let alertVC = UIAlertController(title: "Bluetooth is not enabled", message: "Make sure that your bluetooth is turned on", preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .default) { _ in diff --git a/BadBird/UUIDKey.swift b/BadBird/UUIDKey.swift index c096ab2..f0e08a6 100644 --- a/BadBird/UUIDKey.swift +++ b/BadBird/UUIDKey.swift @@ -9,9 +9,11 @@ import CoreBluetooth // Nordic UART Service UUIDs -nonisolated(unsafe) let BLEService_UUID = CBUUID(string: "6e400001-b5a3-f393-e0a9-e50e24dcca9e") -nonisolated(unsafe) let BLE_Characteristic_uuid_Tx = CBUUID(string: "6e400002-b5a3-f393-e0a9-e50e24dcca9e") // Write without response -nonisolated(unsafe) let BLE_Characteristic_uuid_Rx = CBUUID(string: "6e400003-b5a3-f393-e0a9-e50e24dcca9e") // Read/Notify +enum BLEUUIDs { + nonisolated(unsafe) static let service = CBUUID(string: "6e400001-b5a3-f393-e0a9-e50e24dcca9e") + nonisolated(unsafe) static let tx = CBUUID(string: "6e400002-b5a3-f393-e0a9-e50e24dcca9e") + nonisolated(unsafe) static let rx = CBUUID(string: "6e400003-b5a3-f393-e0a9-e50e24dcca9e") +} // Mi365 scooter commands enum Mi365Command { diff --git a/BadBird/UartModuleViewController.swift b/BadBird/UartModuleViewController.swift index 978cbf6..75f23f7 100644 --- a/BadBird/UartModuleViewController.swift +++ b/BadBird/UartModuleViewController.swift @@ -8,8 +8,10 @@ import UIKit import CoreBluetooth +import os class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFieldDelegate { + private static let logger = Logger(subsystem: "com.mi365locker", category: "UART") // MARK: - UI @IBOutlet weak var baseTextView: UITextView! @@ -22,10 +24,15 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel var peripheral: CBPeripheral! private var consoleAsciiText = NSMutableAttributedString() private var notificationObserver: (any NSObjectProtocol)? + private let keyboardScrollOffset: CGFloat = 250 override func viewDidLoad() { super.viewDidLoad() + guard peripheral != nil else { + fatalError("UartModuleViewController requires a peripheral to be set before presentation") + } + navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) baseTextView.delegate = self inputTextField.delegate = self @@ -39,12 +46,13 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel inputTextField.layer.borderColor = UIColor.blue.cgColor inputTextField.layer.cornerRadius = 3.0 - updateIncomingData() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - baseTextView.text = "" + consoleAsciiText = NSMutableAttributedString() + baseTextView.attributedText = consoleAsciiText + updateIncomingData() } override func viewDidDisappear(_ animated: Bool) { @@ -56,6 +64,10 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel } func updateIncomingData() { + if let existing = notificationObserver { + NotificationCenter.default.removeObserver(existing) + notificationObserver = nil + } notificationObserver = NotificationCenter.default.addObserver( forName: NSNotification.Name(rawValue: "Notify"), object: nil, @@ -64,14 +76,13 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel MainActor.assumeIsolated { guard let self else { return } - let appendString = "\n" let myFont = UIFont(name: "Helvetica Neue", size: 15.0) ?? UIFont.systemFont(ofSize: 15.0) let attributes: [NSAttributedString.Key: Any] = [ .font: myFont, .foregroundColor: UIColor.red ] let attribString = NSAttributedString( - string: "[Incoming]: " + characteristicASCIIValue + appendString, + string: "[Incoming]: " + BLEConnectionState.shared.lastReceivedValue + "\n", attributes: attributes ) self.consoleAsciiText.append(attribString) @@ -85,7 +96,6 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel } func outgoingData() { - let appendString = "\n" let inputText = inputTextField.text ?? "" let myFont = UIFont(name: "Helvetica Neue", size: 15.0) ?? UIFont.systemFont(ofSize: 15.0) @@ -94,11 +104,11 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel .foregroundColor: UIColor.blue ] - sendCommand(lock: isLocked) - isLocked = !isLocked + sendCommand(lock: BLEConnectionState.shared.isLocked) + BLEConnectionState.shared.isLocked = !BLEConnectionState.shared.isLocked let attribString = NSAttributedString( - string: "[Outgoing]: " + inputText + appendString, + string: "[Outgoing]: " + inputText + "\n", attributes: attributes ) consoleAsciiText.append(attribString) @@ -112,18 +122,18 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel let bytes = lock ? Mi365Command.lock : Mi365Command.unlock let data = Data(bytes) - guard let peripheral = blePeripheral, - let characteristic = txCharacteristic else { - print("BLE not connected — cannot send command") + guard let peripheral = BLEConnectionState.shared.peripheral, + let characteristic = BLEConnectionState.shared.txCharacteristic else { + Self.logger.error("BLE not connected — cannot send command") return } peripheral.writeValue(data, for: characteristic, type: .withoutResponse) } func writeCharacteristic(val: Int8) { - guard let peripheral = blePeripheral, - let characteristic = txCharacteristic else { - print("BLE not connected — cannot write characteristic") + guard let peripheral = BLEConnectionState.shared.peripheral, + let characteristic = BLEConnectionState.shared.txCharacteristic else { + Self.logger.error("BLE not connected — cannot write characteristic") return } var value = val @@ -142,7 +152,7 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel } func textFieldDidBeginEditing(_ textField: UITextField) { - scrollView.setContentOffset(CGPoint(x: 0, y: 250), animated: true) + scrollView.setContentOffset(CGPoint(x: 0, y: keyboardScrollOffset), animated: true) } func textFieldDidEndEditing(_ textField: UITextField) { @@ -153,11 +163,11 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel @IBAction func switchAction(_ sender: Any) { if switchUI.isOn { - print("On") + Self.logger.info("Switch: Lock ON") sendCommand(lock: true) writeCharacteristic(val: 1) } else { - print("Off") + Self.logger.info("Switch: Lock OFF") sendCommand(lock: false) writeCharacteristic(val: 0) } diff --git a/BadBirdTests/BadBirdTests.swift b/BadBirdTests/BadBirdTests.swift new file mode 100644 index 0000000..21eb086 --- /dev/null +++ b/BadBirdTests/BadBirdTests.swift @@ -0,0 +1,65 @@ +import Testing +@testable import BadBird + +@Suite("BLE UUIDs") +struct BLEUUIDTests { + @Test("Service UUID matches Nordic UART") + func serviceUUID() { + #expect(BLEUUIDs.service.uuidString == "6E400001-B5A3-F393-E0A9-E50E24DCCA9E") + } + + @Test("TX characteristic UUID is correct") + func txUUID() { + #expect(BLEUUIDs.tx.uuidString == "6E400002-B5A3-F393-E0A9-E50E24DCCA9E") + } + + @Test("RX characteristic UUID is correct") + func rxUUID() { + #expect(BLEUUIDs.rx.uuidString == "6E400003-B5A3-F393-E0A9-E50E24DCCA9E") + } +} + +@Suite("Mi365 Commands") +struct Mi365CommandTests { + @Test("Lock command has correct byte sequence") + func lockCommand() { + #expect(Mi365Command.lock == [0x55, 0xAA, 0x03, 0x20, 0x03, 0x70, 0x01, 0x68, 0xFF]) + #expect(Mi365Command.lock.count == 9) + } + + @Test("Unlock command has correct byte sequence") + func unlockCommand() { + #expect(Mi365Command.unlock == [0x55, 0xAA, 0x03, 0x20, 0x03, 0x71, 0x01, 0x67, 0xFF]) + #expect(Mi365Command.unlock.count == 9) + } + + @Test("Lock and unlock commands differ only in command byte and checksum") + func commandDifference() { + // Commands should be identical except bytes at index 5 (command) and 7 (checksum) + for i in 0..