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:
parent
2ec195243c
commit
02592ea249
3 changed files with 120 additions and 32 deletions
|
|
@ -341,7 +341,7 @@ class Api {
|
||||||
"UUID": uuid,
|
"UUID": uuid,
|
||||||
"Major": major,
|
"Major": major,
|
||||||
"Minor": minor,
|
"Minor": minor,
|
||||||
"HardwareID": hardwareId
|
"hardware_id": hardwareId
|
||||||
]
|
]
|
||||||
if let mac = macAddress, !mac.isEmpty {
|
if let mac = macAddress, !mac.isEmpty {
|
||||||
body["MACAddress"] = mac
|
body["MACAddress"] = mac
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import CoreBluetooth
|
||||||
|
|
||||||
/// Result of a provisioning operation
|
/// Result of a provisioning operation
|
||||||
enum ProvisioningResult {
|
enum ProvisioningResult {
|
||||||
case success
|
case success(macAddress: String?)
|
||||||
case failure(String)
|
case failure(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +123,8 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
private var dxSmartNotifySubscribed = false
|
private var dxSmartNotifySubscribed = false
|
||||||
private var dxSmartCommandQueue: [Data] = []
|
private var dxSmartCommandQueue: [Data] = []
|
||||||
private var dxSmartWriteIndex = 0
|
private var dxSmartWriteIndex = 0
|
||||||
|
private var provisioningMacAddress: String?
|
||||||
|
private var awaitingDeviceInfoForProvisioning = false
|
||||||
|
|
||||||
// Read config mode
|
// Read config mode
|
||||||
private enum OperationMode { case provisioning, readingConfig }
|
private enum OperationMode { case provisioning, readingConfig }
|
||||||
|
|
@ -178,6 +180,8 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
self.dxSmartNotifySubscribed = false
|
self.dxSmartNotifySubscribed = false
|
||||||
self.dxSmartCommandQueue.removeAll()
|
self.dxSmartCommandQueue.removeAll()
|
||||||
self.dxSmartWriteIndex = 0
|
self.dxSmartWriteIndex = 0
|
||||||
|
self.provisioningMacAddress = nil
|
||||||
|
self.awaitingDeviceInfoForProvisioning = false
|
||||||
self.connectionRetryCount = 0
|
self.connectionRetryCount = 0
|
||||||
self.currentBeacon = beacon
|
self.currentBeacon = beacon
|
||||||
|
|
||||||
|
|
@ -256,6 +260,8 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
dxSmartNotifySubscribed = false
|
dxSmartNotifySubscribed = false
|
||||||
dxSmartCommandQueue.removeAll()
|
dxSmartCommandQueue.removeAll()
|
||||||
dxSmartWriteIndex = 0
|
dxSmartWriteIndex = 0
|
||||||
|
provisioningMacAddress = nil
|
||||||
|
awaitingDeviceInfoForProvisioning = false
|
||||||
connectionRetryCount = 0
|
connectionRetryCount = 0
|
||||||
currentBeacon = nil
|
currentBeacon = nil
|
||||||
state = .idle
|
state = .idle
|
||||||
|
|
@ -273,12 +279,13 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func succeed() {
|
private func succeed() {
|
||||||
DebugLog.shared.log("BLE: Success!")
|
DebugLog.shared.log("BLE: Success! MAC=\(provisioningMacAddress ?? "unknown")")
|
||||||
state = .success
|
state = .success
|
||||||
if let peripheral = peripheral {
|
if let peripheral = peripheral {
|
||||||
centralManager.cancelPeripheralConnection(peripheral)
|
centralManager.cancelPeripheralConnection(peripheral)
|
||||||
}
|
}
|
||||||
completion?(.success)
|
let mac = provisioningMacAddress
|
||||||
|
completion?(.success(macAddress: mac))
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,6 +334,32 @@ class BeaconProvisioner: NSObject, ObservableObject {
|
||||||
peripheral?.writeValue(passwordData, for: passwordChar, type: .withResponse)
|
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
|
/// Build the full command sequence and start writing
|
||||||
/// New 24-step write sequence for DX-Smart CP28:
|
/// New 24-step write sequence for DX-Smart CP28:
|
||||||
/// 1. DeviceName 0x71 [name bytes] — service point name (max 20 ASCII chars)
|
/// 1. DeviceName 0x71 [name bytes] — service point name (max 20 ASCII chars)
|
||||||
|
|
@ -1031,7 +1064,8 @@ extension BeaconProvisioner: CBPeripheralDelegate {
|
||||||
if operationMode == .readingConfig {
|
if operationMode == .readingConfig {
|
||||||
dxSmartReadQueryAfterAuth()
|
dxSmartReadQueryAfterAuth()
|
||||||
} else {
|
} else {
|
||||||
dxSmartWriteConfig()
|
// Read device info first to get MAC address, then write config
|
||||||
|
dxSmartReadDeviceInfoBeforeWrite()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1043,6 +1077,9 @@ extension BeaconProvisioner: CBPeripheralDelegate {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
|
||||||
self?.dxSmartSendNextReadQuery()
|
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 {
|
} else {
|
||||||
dxSmartWriteIndex += 1
|
dxSmartWriteIndex += 1
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
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)")
|
DebugLog.shared.log("BLE: Read \(characteristic.uuid): \(hex)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Provisioning mode — just log FFE1 notifications
|
// Provisioning mode
|
||||||
if characteristic.uuid == BeaconProvisioner.DXSMART_NOTIFY_CHAR {
|
if characteristic.uuid == BeaconProvisioner.DXSMART_NOTIFY_CHAR {
|
||||||
let hex = data.map { String(format: "%02X", $0) }.joined(separator: " ")
|
let hex = data.map { String(format: "%02X", $0) }.joined(separator: " ")
|
||||||
DebugLog.shared.log("BLE: FFE1 notification: \(hex)")
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,25 +98,7 @@ struct ScanView: View {
|
||||||
// Beacon lists
|
// Beacon lists
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(spacing: 8) {
|
LazyVStack(spacing: 8) {
|
||||||
// Detected iBeacons section (shows ownership status)
|
// BLE devices section (for provisioning) - shown first
|
||||||
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)
|
|
||||||
if !bleScanner.discoveredBeacons.isEmpty {
|
if !bleScanner.discoveredBeacons.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Configurable Devices")
|
Text("Configurable Devices")
|
||||||
|
|
@ -132,6 +114,12 @@ struct ScanView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
if !detectedIBeacons.isEmpty {
|
||||||
|
Divider()
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
} else if bleScanner.isScanning {
|
} else if bleScanner.isScanning {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
|
|
@ -155,6 +143,20 @@ struct ScanView: View {
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 40)
|
.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)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
|
@ -887,21 +889,23 @@ struct ScanView: View {
|
||||||
|
|
||||||
provisioningProgress = "Provisioning beacon..."
|
provisioningProgress = "Provisioning beacon..."
|
||||||
|
|
||||||
let hardwareId = beacon.id.uuidString // BLE peripheral identifier
|
|
||||||
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success(let macAddress):
|
||||||
do {
|
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 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(
|
try await Api.shared.registerBeaconHardware(
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
servicePointId: sp.servicePointId,
|
servicePointId: sp.servicePointId,
|
||||||
uuid: uuidWithDashes,
|
uuid: uuidWithDashes,
|
||||||
major: config.major,
|
major: config.major,
|
||||||
minor: config.minor,
|
minor: config.minor,
|
||||||
hardwareId: hardwareId
|
hardwareId: hardwareId,
|
||||||
|
macAddress: macAddress
|
||||||
)
|
)
|
||||||
finishProvisioning(name: sp.name)
|
finishProvisioning(name: sp.name)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -967,21 +971,24 @@ struct ScanView: View {
|
||||||
provisioningProgress = "Provisioning beacon..."
|
provisioningProgress = "Provisioning beacon..."
|
||||||
|
|
||||||
// 4. Provision the beacon via GATT
|
// 4. Provision the beacon via GATT
|
||||||
let hardwareId = beacon.id.uuidString // BLE peripheral identifier
|
|
||||||
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success(let macAddress):
|
||||||
// Register in backend (use UUID with dashes for API)
|
// Register in backend (use UUID with dashes for API)
|
||||||
do {
|
do {
|
||||||
|
// Use MAC address as hardware ID, fallback to iBeacon UUID if unavailable
|
||||||
let uuidWithDashes = formatUuidWithDashes(config.uuid)
|
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(
|
try await Api.shared.registerBeaconHardware(
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
servicePointId: servicePoint.servicePointId,
|
servicePointId: servicePoint.servicePointId,
|
||||||
uuid: uuidWithDashes,
|
uuid: uuidWithDashes,
|
||||||
major: config.major,
|
major: config.major,
|
||||||
minor: config.minor,
|
minor: config.minor,
|
||||||
hardwareId: hardwareId
|
hardwareId: hardwareId,
|
||||||
|
macAddress: macAddress
|
||||||
)
|
)
|
||||||
finishProvisioning(name: name)
|
finishProvisioning(name: name)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue