diff --git a/PayfritBeacon/Api.swift b/PayfritBeacon/Api.swift index 9fa2dc5..fcaed4e 100644 --- a/PayfritBeacon/Api.swift +++ b/PayfritBeacon/Api.swift @@ -341,7 +341,7 @@ class Api { "UUID": uuid, "Major": major, "Minor": minor, - "HardwareID": hardwareId + "hardware_id": hardwareId ] if let mac = macAddress, !mac.isEmpty { body["MACAddress"] = mac diff --git a/PayfritBeacon/BeaconProvisioner.swift b/PayfritBeacon/BeaconProvisioner.swift index 6464548..e4304b7 100644 --- a/PayfritBeacon/BeaconProvisioner.swift +++ b/PayfritBeacon/BeaconProvisioner.swift @@ -3,7 +3,7 @@ import CoreBluetooth /// Result of a provisioning operation enum ProvisioningResult { - case success + case success(macAddress: String?) case failure(String) } @@ -123,6 +123,8 @@ class BeaconProvisioner: NSObject, ObservableObject { private var dxSmartNotifySubscribed = false private var dxSmartCommandQueue: [Data] = [] private var dxSmartWriteIndex = 0 + private var provisioningMacAddress: String? + private var awaitingDeviceInfoForProvisioning = false // Read config mode private enum OperationMode { case provisioning, readingConfig } @@ -178,6 +180,8 @@ class BeaconProvisioner: NSObject, ObservableObject { self.dxSmartNotifySubscribed = false self.dxSmartCommandQueue.removeAll() self.dxSmartWriteIndex = 0 + self.provisioningMacAddress = nil + self.awaitingDeviceInfoForProvisioning = false self.connectionRetryCount = 0 self.currentBeacon = beacon @@ -256,6 +260,8 @@ class BeaconProvisioner: NSObject, ObservableObject { dxSmartNotifySubscribed = false dxSmartCommandQueue.removeAll() dxSmartWriteIndex = 0 + provisioningMacAddress = nil + awaitingDeviceInfoForProvisioning = false connectionRetryCount = 0 currentBeacon = nil state = .idle @@ -273,12 +279,13 @@ class BeaconProvisioner: NSObject, ObservableObject { } private func succeed() { - DebugLog.shared.log("BLE: Success!") + DebugLog.shared.log("BLE: Success! MAC=\(provisioningMacAddress ?? "unknown")") state = .success if let peripheral = peripheral { centralManager.cancelPeripheralConnection(peripheral) } - completion?(.success) + let mac = provisioningMacAddress + completion?(.success(macAddress: mac)) cleanup() } @@ -327,6 +334,32 @@ class BeaconProvisioner: NSObject, ObservableObject { peripheral?.writeValue(passwordData, for: passwordChar, type: .withResponse) } + /// Read device info (MAC address) before writing config + private func dxSmartReadDeviceInfoBeforeWrite() { + guard let commandChar = characteristics[BeaconProvisioner.DXSMART_COMMAND_CHAR] else { + DebugLog.shared.log("BLE: FFE2 not found, proceeding without MAC") + dxSmartWriteConfig() + return + } + + progress = "Reading device info..." + awaitingDeviceInfoForProvisioning = true + responseBuffer.removeAll() + + // Send device info query (0x30) + let packet = buildDXPacket(cmd: .deviceInfo, data: []) + DebugLog.shared.log("BLE: Sending device info query to get MAC address") + peripheral?.writeValue(packet, for: commandChar, type: .withResponse) + + // Timeout after 3 seconds - proceed with write even if no MAC + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in + guard let self = self, self.awaitingDeviceInfoForProvisioning else { return } + DebugLog.shared.log("BLE: Device info timeout, proceeding without MAC") + self.awaitingDeviceInfoForProvisioning = false + self.dxSmartWriteConfig() + } + } + /// Build the full command sequence and start writing /// New 24-step write sequence for DX-Smart CP28: /// 1. DeviceName 0x71 [name bytes] — service point name (max 20 ASCII chars) @@ -1031,7 +1064,8 @@ extension BeaconProvisioner: CBPeripheralDelegate { if operationMode == .readingConfig { dxSmartReadQueryAfterAuth() } else { - dxSmartWriteConfig() + // Read device info first to get MAC address, then write config + dxSmartReadDeviceInfoBeforeWrite() } return } @@ -1043,6 +1077,9 @@ extension BeaconProvisioner: CBPeripheralDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in self?.dxSmartSendNextReadQuery() } + } else if awaitingDeviceInfoForProvisioning { + // Device info query was sent - wait for response on FFE1, don't process as normal command + DebugLog.shared.log("BLE: Device info query sent, waiting for response...") } else { dxSmartWriteIndex += 1 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in @@ -1090,11 +1127,55 @@ extension BeaconProvisioner: CBPeripheralDelegate { DebugLog.shared.log("BLE: Read \(characteristic.uuid): \(hex)") } } else { - // Provisioning mode — just log FFE1 notifications + // Provisioning mode if characteristic.uuid == BeaconProvisioner.DXSMART_NOTIFY_CHAR { let hex = data.map { String(format: "%02X", $0) }.joined(separator: " ") DebugLog.shared.log("BLE: FFE1 notification: \(hex)") + + // If awaiting device info for MAC address, process the response + if awaitingDeviceInfoForProvisioning { + processDeviceInfoForProvisioning(data) + } } } } + + /// Process device info response during provisioning to extract MAC address + private func processDeviceInfoForProvisioning(_ data: Data) { + responseBuffer.append(contentsOf: data) + + // Look for complete frame: 4E 4F 30 LEN DATA XOR + guard responseBuffer.count >= 5 else { return } + + // Find header + guard let headerIdx = findDXHeader() else { + responseBuffer.removeAll() + return + } + + if headerIdx > 0 { + responseBuffer.removeFirst(headerIdx) + } + + guard responseBuffer.count >= 5 else { return } + + let cmd = responseBuffer[2] + let len = Int(responseBuffer[3]) + let frameLen = 4 + len + 1 + + guard responseBuffer.count >= frameLen else { return } + + // Check if this is device info response (0x30) + if cmd == DXCmd.deviceInfo.rawValue && len >= 7 { + // Parse MAC address from bytes 1-6 (byte 0 is battery) + let macBytes = Array(responseBuffer[5..<11]) + provisioningMacAddress = macBytes.map { String(format: "%02X", $0) }.joined(separator: ":") + DebugLog.shared.log("BLE: Got MAC address for provisioning: \(provisioningMacAddress ?? "nil")") + } + + // Clear buffer and proceed to write config + responseBuffer.removeAll() + awaitingDeviceInfoForProvisioning = false + dxSmartWriteConfig() + } } diff --git a/PayfritBeacon/ScanView.swift b/PayfritBeacon/ScanView.swift index 51c2cd3..b551461 100644 --- a/PayfritBeacon/ScanView.swift +++ b/PayfritBeacon/ScanView.swift @@ -98,25 +98,7 @@ struct ScanView: View { // Beacon lists ScrollView { LazyVStack(spacing: 8) { - // Detected iBeacons section (shows ownership status) - if !detectedIBeacons.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text("Detected Beacons") - .font(.caption.weight(.semibold)) - .foregroundColor(.secondary) - .padding(.horizontal) - - ForEach(detectedIBeacons, id: \.minor) { ibeacon in - iBeaconRow(ibeacon) - } - } - .padding(.top, 8) - - Divider() - .padding(.vertical, 8) - } - - // BLE devices section (for provisioning) + // BLE devices section (for provisioning) - shown first if !bleScanner.discoveredBeacons.isEmpty { VStack(alignment: .leading, spacing: 8) { Text("Configurable Devices") @@ -132,6 +114,12 @@ struct ScanView: View { } } } + .padding(.top, 8) + + if !detectedIBeacons.isEmpty { + Divider() + .padding(.vertical, 8) + } } else if bleScanner.isScanning { VStack(spacing: 12) { ProgressView() @@ -155,6 +143,20 @@ struct ScanView: View { .frame(maxWidth: .infinity) .padding(.vertical, 40) } + + // Detected iBeacons section (shows ownership status) + if !detectedIBeacons.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Detected Beacons") + .font(.caption.weight(.semibold)) + .foregroundColor(.secondary) + .padding(.horizontal) + + ForEach(detectedIBeacons, id: \.minor) { ibeacon in + iBeaconRow(ibeacon) + } + } + } } .padding(.horizontal) } @@ -887,21 +889,23 @@ struct ScanView: View { provisioningProgress = "Provisioning beacon..." - let hardwareId = beacon.id.uuidString // BLE peripheral identifier provisioner.provision(beacon: beacon, config: beaconConfig) { result in Task { @MainActor in switch result { - case .success: + case .success(let macAddress): do { - // Register with the UUID format expected by API (with dashes) + // Use MAC address as hardware ID, fallback to iBeacon UUID if unavailable let uuidWithDashes = formatUuidWithDashes(config.uuid) + let hardwareId = macAddress ?? uuidWithDashes + DebugLog.shared.log("[ScanView] Registering beacon - MAC: \(macAddress ?? "nil"), hardwareId: \(hardwareId), uuid: \(uuidWithDashes)") try await Api.shared.registerBeaconHardware( businessId: businessId, servicePointId: sp.servicePointId, uuid: uuidWithDashes, major: config.major, minor: config.minor, - hardwareId: hardwareId + hardwareId: hardwareId, + macAddress: macAddress ) finishProvisioning(name: sp.name) } catch { @@ -967,21 +971,24 @@ struct ScanView: View { provisioningProgress = "Provisioning beacon..." // 4. Provision the beacon via GATT - let hardwareId = beacon.id.uuidString // BLE peripheral identifier provisioner.provision(beacon: beacon, config: beaconConfig) { result in Task { @MainActor in switch result { - case .success: + case .success(let macAddress): // Register in backend (use UUID with dashes for API) do { + // Use MAC address as hardware ID, fallback to iBeacon UUID if unavailable let uuidWithDashes = formatUuidWithDashes(config.uuid) + let hardwareId = macAddress ?? uuidWithDashes + DebugLog.shared.log("[ScanView] Registering beacon - MAC: \(macAddress ?? "nil"), hardwareId: \(hardwareId), uuid: \(uuidWithDashes)") try await Api.shared.registerBeaconHardware( businessId: businessId, servicePointId: servicePoint.servicePointId, uuid: uuidWithDashes, major: config.major, minor: config.minor, - hardwareId: hardwareId + hardwareId: hardwareId, + macAddress: macAddress ) finishProvisioning(name: name) } catch {