From 75c68353ca15102fd53f5df9dee280094857442c Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 08:35:49 +0900 Subject: [PATCH 01/10] Refactor BLE UUIDs into enum namespace Replace nonisolated(unsafe) global CBUUID variables with BLEUUIDs enum containing static properties for better encapsulation and namespacing. --- BadBird/BLECentralViewController.swift | 10 +++++----- BadBird/UUIDKey.swift | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/BadBird/BLECentralViewController.swift b/BadBird/BLECentralViewController.swift index 56abdc0..0be1b1b 100644 --- a/BadBird/BLECentralViewController.swift +++ b/BadBird/BLECentralViewController.swift @@ -71,7 +71,7 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag print(isLocked ? "Now Locking..." : "Now Unlocking...") scanTimer?.invalidate() - centralManager?.scanForPeripherals(withServices: [BLEService_UUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) + centralManager?.scanForPeripherals(withServices: [BLEUUIDs.service], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) scanTimer = Timer.scheduledTimer(timeInterval: 17, target: self, selector: #selector(cancelScan), userInfo: nil, repeats: false) } @@ -124,7 +124,7 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag print("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 { @@ -156,7 +156,7 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag 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)") } @@ -172,13 +172,13 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag print("Found \(characteristics.count) characteristics!") for characteristic in characteristics { - if characteristic.uuid.isEqual(BLE_Characteristic_uuid_Rx) { + if characteristic.uuid.isEqual(BLEUUIDs.rx) { rxCharacteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) peripheral.readValue(for: characteristic) print("Rx Characteristic: \(characteristic.uuid)") } - if characteristic.uuid.isEqual(BLE_Characteristic_uuid_Tx) { + if characteristic.uuid.isEqual(BLEUUIDs.tx) { txCharacteristic = characteristic print("Tx Characteristic: \(characteristic.uuid)") } 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 { From 17d82ae9af8d128c67a16153d81ba985450f7d98 Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 08:43:56 +0900 Subject: [PATCH 02/10] Fix isLocked bug, encapsulate global state, add os.Logger, extract magic numbers, fix MainActor.assumeIsolated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove isLocked toggle from startScan() — lock state now only changes on explicit user command - Replace 5 global @MainActor variables with BLEConnectionState singleton class - Replace all print() calls with structured os.Logger (info/debug/error/warning levels) - Extract magic numbers into named constants (scanTimeoutInterval, keyboardScrollOffset) - Replace brittle MainActor.assumeIsolated with safe Task { @MainActor in } --- BadBird/BLECentralViewController.swift | 103 +++++++++++++------------ BadBird/UartModuleViewController.swift | 29 +++---- 2 files changed, 70 insertions(+), 62 deletions(-) diff --git a/BadBird/BLECentralViewController.swift b/BadBird/BLECentralViewController.swift index 0be1b1b..0e74794 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() + private 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,16 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag func startScan() { peripherals = [] - isLocked = !isLocked - print(isLocked ? "Now Locking..." : "Now Unlocking...") scanTimer?.invalidate() centralManager?.scanForPeripherals(withServices: [BLEUUIDs.service], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) - scanTimer = Timer.scheduledTimer(timeInterval: 17, target: self, selector: #selector(cancelScan), userInfo: nil, repeats: 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 +94,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 +103,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 +122,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([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,19 +142,19 @@ 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") } // 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 } @@ -158,83 +163,83 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag for service in services { 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(BLEUUIDs.rx) { - rxCharacteristic = characteristic + 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(BLEUUIDs.tx) { - txCharacteristic = characteristic - print("Tx Characteristic: \(characteristic.uuid)") + 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 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 +262,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 +270,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/UartModuleViewController.swift b/BadBird/UartModuleViewController.swift index 978cbf6..0896084 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,6 +24,7 @@ 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() @@ -61,7 +64,7 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel object: nil, queue: .main ) { [weak self] _ in - MainActor.assumeIsolated { + Task { @MainActor in guard let self else { return } let appendString = "\n" @@ -71,7 +74,7 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel .foregroundColor: UIColor.red ] let attribString = NSAttributedString( - string: "[Incoming]: " + characteristicASCIIValue + appendString, + string: "[Incoming]: " + BLEConnectionState.shared.lastReceivedValue + appendString, attributes: attributes ) self.consoleAsciiText.append(attribString) @@ -94,8 +97,8 @@ 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, @@ -112,18 +115,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 +145,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 +156,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) } From bfe3a5eb970b03b1e0c3722444e3526938894ce5 Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 08:44:54 +0900 Subject: [PATCH 03/10] Add guard for force-unwrapped peripheral in UartModuleViewController Provides a clear diagnostic crash message if the view controller is instantiated without setting the peripheral property. --- BadBird/UartModuleViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BadBird/UartModuleViewController.swift b/BadBird/UartModuleViewController.swift index 0896084..88e58ec 100644 --- a/BadBird/UartModuleViewController.swift +++ b/BadBird/UartModuleViewController.swift @@ -29,6 +29,10 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel 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 From dd9fab294ee3cee57151ae5ddfa6cf89331238f1 Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 08:46:05 +0900 Subject: [PATCH 04/10] Add explicit SWIFT_STRICT_CONCURRENCY = complete to build settings Documents the intent of full strict concurrency checking, which is already the default for Swift 6.0 language mode. --- BadBird.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BadBird.xcodeproj/project.pbxproj b/BadBird.xcodeproj/project.pbxproj index 900046a..ef0572a 100644 --- a/BadBird.xcodeproj/project.pbxproj +++ b/BadBird.xcodeproj/project.pbxproj @@ -289,6 +289,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = Zimperium.BadBird; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0; }; name = Debug; @@ -305,6 +306,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = Zimperium.BadBird; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0; }; name = Release; From 81e336aa55ab2bda07dcc080bb9771093eb3730e Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 08:56:29 +0900 Subject: [PATCH 05/10] Add Swift Testing test target with BLE and command tests Add BadBirdTests target with 8 tests across 3 suites: - BLEUUIDTests: verify Nordic UART service/tx/rx UUIDs - Mi365CommandTests: verify lock/unlock byte sequences and diff - BLEConnectionStateTests: verify singleton and default state --- BadBird.xcodeproj/project.pbxproj | 123 ++++++++++++++++++ .../xcschemes/BadBirdTests.xcscheme | 68 ++++++++++ BadBirdTests/BadBirdTests.swift | 68 ++++++++++ 3 files changed, 259 insertions(+) create mode 100644 BadBird.xcodeproj/xcshareddata/xcschemes/BadBirdTests.xcscheme create mode 100644 BadBirdTests/BadBirdTests.swift diff --git a/BadBird.xcodeproj/project.pbxproj b/BadBird.xcodeproj/project.pbxproj index ef0572a..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 = { @@ -314,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/BadBirdTests/BadBirdTests.swift b/BadBirdTests/BadBirdTests.swift new file mode 100644 index 0000000..9b9067e --- /dev/null +++ b/BadBirdTests/BadBirdTests.swift @@ -0,0 +1,68 @@ +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.. Date: Sat, 21 Mar 2026 09:01:40 +0900 Subject: [PATCH 06/10] Fix notification observer lifecycle, remove unnecessary async hop, serialize stateful tests - Move updateIncomingData() from viewDidLoad to viewDidAppear to re-register observer after viewDidDisappear removes it - Remove Task { @MainActor in } wrapper since callback already runs on main queue (queue: .main) - Add .serialized trait to BLEConnectionStateTests to prevent data races on shared singleton in parallel test execution --- BadBird/UartModuleViewController.swift | 32 ++++++++++++-------------- BadBirdTests/BadBirdTests.swift | 2 +- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/BadBird/UartModuleViewController.swift b/BadBird/UartModuleViewController.swift index 88e58ec..bce4f53 100644 --- a/BadBird/UartModuleViewController.swift +++ b/BadBird/UartModuleViewController.swift @@ -46,12 +46,12 @@ 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 = "" + updateIncomingData() } override func viewDidDisappear(_ animated: Bool) { @@ -68,22 +68,20 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel object: nil, queue: .main ) { [weak self] _ in - Task { @MainActor in - 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]: " + BLEConnectionState.shared.lastReceivedValue + appendString, - attributes: attributes - ) - self.consoleAsciiText.append(attribString) - self.baseTextView.attributedText = self.consoleAsciiText - } + 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]: " + BLEConnectionState.shared.lastReceivedValue + appendString, + attributes: attributes + ) + self.consoleAsciiText.append(attribString) + self.baseTextView.attributedText = self.consoleAsciiText } } diff --git a/BadBirdTests/BadBirdTests.swift b/BadBirdTests/BadBirdTests.swift index 9b9067e..c210568 100644 --- a/BadBirdTests/BadBirdTests.swift +++ b/BadBirdTests/BadBirdTests.swift @@ -46,7 +46,7 @@ struct Mi365CommandTests { } } -@Suite("BLEConnectionState") +@Suite("BLEConnectionState", .serialized) struct BLEConnectionStateTests { @Test("Shared instance is singleton") @MainActor From 7ebe2aa055f4072744011f04183b1889e92e6db0 Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 09:10:43 +0900 Subject: [PATCH 07/10] Fix outgoingData lock toggle, switchAction state sync, and console history bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove lock/unlock command from outgoingData() — text send should not toggle scooter lock state - Update BLEConnectionState.isLocked in switchAction to keep state in sync with physical switch position - Clear consoleAsciiText in viewDidAppear to prevent stale history reappearing after navigation --- BadBird/UartModuleViewController.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/BadBird/UartModuleViewController.swift b/BadBird/UartModuleViewController.swift index bce4f53..b7d41e9 100644 --- a/BadBird/UartModuleViewController.swift +++ b/BadBird/UartModuleViewController.swift @@ -50,7 +50,8 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - baseTextView.text = "" + consoleAsciiText = NSMutableAttributedString() + baseTextView.attributedText = consoleAsciiText updateIncomingData() } @@ -90,8 +91,8 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel } func outgoingData() { - let appendString = "\n" let inputText = inputTextField.text ?? "" + guard !inputText.isEmpty else { return } let myFont = UIFont(name: "Helvetica Neue", size: 15.0) ?? UIFont.systemFont(ofSize: 15.0) let attributes: [NSAttributedString.Key: Any] = [ @@ -99,11 +100,8 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel .foregroundColor: UIColor.blue ] - 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) @@ -157,7 +155,10 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel // MARK: - Switch Action @IBAction func switchAction(_ sender: Any) { - if switchUI.isOn { + let locking = switchUI.isOn + BLEConnectionState.shared.isLocked = locking + + if locking { Self.logger.info("Switch: Lock ON") sendCommand(lock: true) writeCharacteristic(val: 1) From 32a538e271000fc85aaa3940bf1d624c5a60fb65 Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 09:17:53 +0900 Subject: [PATCH 08/10] Fix BLE protocol corruption, observer leak, and rssiValues desync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove writeCharacteristic() calls from switchAction — sending a legacy 1-byte value after the 9-byte Mi365 command corrupts the BLE payload - Guard against duplicate notification observers by removing existing observer before registering new one in updateIncomingData - Clear rssiValues alongside peripherals in startScan() to prevent array index mismatch and potential crash --- BadBird/BLECentralViewController.swift | 1 + BadBird/UartModuleViewController.swift | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/BadBird/BLECentralViewController.swift b/BadBird/BLECentralViewController.swift index 0e74794..42a9a62 100644 --- a/BadBird/BLECentralViewController.swift +++ b/BadBird/BLECentralViewController.swift @@ -75,6 +75,7 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag func startScan() { peripherals = [] + rssiValues = [] scanTimer?.invalidate() centralManager?.scanForPeripherals(withServices: [BLEUUIDs.service], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) diff --git a/BadBird/UartModuleViewController.swift b/BadBird/UartModuleViewController.swift index b7d41e9..4697398 100644 --- a/BadBird/UartModuleViewController.swift +++ b/BadBird/UartModuleViewController.swift @@ -64,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, @@ -161,11 +165,9 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel if locking { Self.logger.info("Switch: Lock ON") sendCommand(lock: true) - writeCharacteristic(val: 1) } else { Self.logger.info("Switch: Lock OFF") sendCommand(lock: false) - writeCharacteristic(val: 0) } } From ff518cf24d04d7d9bf1ec02514f239b04d7ce86f Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 09:30:15 +0900 Subject: [PATCH 09/10] Restore original outgoingData and switchAction behavior Revert outgoingData() to send lock/unlock commands on Send press and toggle isLocked state, matching original app behavior. Restore writeCharacteristic() calls in switchAction to preserve the dual-write pattern expected by the scooter firmware. --- BadBird/UartModuleViewController.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/BadBird/UartModuleViewController.swift b/BadBird/UartModuleViewController.swift index 4697398..6c46fdd 100644 --- a/BadBird/UartModuleViewController.swift +++ b/BadBird/UartModuleViewController.swift @@ -96,7 +96,6 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel func outgoingData() { let inputText = inputTextField.text ?? "" - guard !inputText.isEmpty else { return } let myFont = UIFont(name: "Helvetica Neue", size: 15.0) ?? UIFont.systemFont(ofSize: 15.0) let attributes: [NSAttributedString.Key: Any] = [ @@ -104,6 +103,9 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel .foregroundColor: UIColor.blue ] + sendCommand(lock: BLEConnectionState.shared.isLocked) + BLEConnectionState.shared.isLocked = !BLEConnectionState.shared.isLocked + let attribString = NSAttributedString( string: "[Outgoing]: " + inputText + "\n", attributes: attributes @@ -159,15 +161,14 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel // MARK: - Switch Action @IBAction func switchAction(_ sender: Any) { - let locking = switchUI.isOn - BLEConnectionState.shared.isLocked = locking - - if locking { + if switchUI.isOn { Self.logger.info("Switch: Lock ON") sendCommand(lock: true) + writeCharacteristic(val: 1) } else { Self.logger.info("Switch: Lock OFF") sendCommand(lock: false) + writeCharacteristic(val: 0) } } From f274a4e948adcb5c458eb9ebc48529043a47967b Mon Sep 17 00:00:00 2001 From: Wolf Date: Sat, 21 Mar 2026 09:38:20 +0900 Subject: [PATCH 10/10] Fix memory leak on disconnect, add error handling, fix test tautology, add MainActor isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clear BLE state (peripheral, characteristics) on disconnect to prevent memory leak in BLEConnectionState singleton - Add error check in didUpdateValueFor to avoid processing stale data - Use MainActor.assumeIsolated for notification closure (queue: .main guarantees main thread execution) - Fix tautological test — verify fresh instance default instead of set-then-check pattern - Make BLEConnectionState.init internal for testability --- BadBird/BLECentralViewController.swift | 9 +++++++- BadBird/UartModuleViewController.swift | 29 +++++++++++++------------- BadBirdTests/BadBirdTests.swift | 9 +++----- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/BadBird/BLECentralViewController.swift b/BadBird/BLECentralViewController.swift index 42a9a62..7d6a7d5 100644 --- a/BadBird/BLECentralViewController.swift +++ b/BadBird/BLECentralViewController.swift @@ -14,7 +14,7 @@ import os @MainActor final class BLEConnectionState { static let shared = BLEConnectionState() - private init() {} + init() {} var txCharacteristic: CBCharacteristic? var rxCharacteristic: CBCharacteristic? @@ -149,6 +149,9 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: (any Error)?) { Self.logger.info("Disconnected") + BLEConnectionState.shared.peripheral = nil + BLEConnectionState.shared.txCharacteristic = nil + BLEConnectionState.shared.rxCharacteristic = nil } // MARK: - CBPeripheralDelegate @@ -193,6 +196,10 @@ class BLECentralViewController: UIViewController, @preconcurrency CBCentralManag } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: (any Error)?) { + 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 } diff --git a/BadBird/UartModuleViewController.swift b/BadBird/UartModuleViewController.swift index 6c46fdd..75f23f7 100644 --- a/BadBird/UartModuleViewController.swift +++ b/BadBird/UartModuleViewController.swift @@ -73,20 +73,21 @@ class UartModuleViewController: UIViewController, UITextViewDelegate, UITextFiel object: nil, queue: .main ) { [weak self] _ in - 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]: " + BLEConnectionState.shared.lastReceivedValue + appendString, - attributes: attributes - ) - self.consoleAsciiText.append(attribString) - self.baseTextView.attributedText = self.consoleAsciiText + MainActor.assumeIsolated { + guard let self else { return } + + 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]: " + BLEConnectionState.shared.lastReceivedValue + "\n", + attributes: attributes + ) + self.consoleAsciiText.append(attribString) + self.baseTextView.attributedText = self.consoleAsciiText + } } } diff --git a/BadBirdTests/BadBirdTests.swift b/BadBirdTests/BadBirdTests.swift index c210568..21eb086 100644 --- a/BadBirdTests/BadBirdTests.swift +++ b/BadBirdTests/BadBirdTests.swift @@ -56,13 +56,10 @@ struct BLEConnectionStateTests { #expect(a === b) } - @Test("Default state is locked") + @Test("Initial isLocked is true") @MainActor func defaultLocked() { - // Note: This tests the class definition, not runtime state - let state = BLEConnectionState.shared - // Reset for test - state.isLocked = true - #expect(state.isLocked == true) + let freshState = BLEConnectionState() + #expect(freshState.isLocked == true) } }