Read MAC address during provisioning and use as hardware_id

- Modified BeaconProvisioner to read device info (0x30) before writing config
- Extract MAC address from beacon and return in ProvisioningResult
- Use MAC address as hardware_id field (snake_case for backend)
- Reorder scan view: Configurable Devices section now appears first
- Add debug logging for beacon registration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Pinkyfloyd 2026-03-16 17:35:28 -07:00
parent 2ec195243c
commit 02592ea249
3 changed files with 120 additions and 32 deletions

View file

@ -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

View file

@ -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()
}
}

View file

@ -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 {