Compare commits

..

2 commits

Author SHA1 Message Date
640bc32f92 fix: use .withoutResponse for SaveConfig to prevent silent write drop
The beacon reboots instantly on SaveConfig (0x60). Using .withResponse
meant CoreBluetooth expected a GATT ACK that never arrived, potentially
causing the write to be silently dropped — leaving the config unsaved
and the beacon LED still flashing after provisioning.

Switching to .withoutResponse fires the bytes directly into the BLE
radio buffer without requiring a round-trip ACK. The beacon firmware
processes the save command from its buffer before rebooting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 03:57:56 +00:00
157ab6d008 fix: send HardwareId to register_beacon_hardware API
The API requires HardwareId as a mandatory field, but the iOS app was
sending MacAddress (wrong key) and always passing nil. This caused
"HardwareId is required" errors after provisioning.

Since CoreBluetooth doesn't expose raw MAC addresses, we use the
CBPeripheral.identifier UUID as the hardware ID — same concept as
Android's device.address.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 03:55:13 +00:00
3 changed files with 11 additions and 7 deletions

View file

@ -174,9 +174,12 @@ final class DXSmartProvisioner: NSObject, BeaconProvisioner {
await diagnosticLog?.log("write", "[\(index + 1)/\(commands.count)] \(name) (\(packet.count) bytes)")
// SaveConfig (last command) causes beacon MCU to reboot it never sends an ACK.
// Fire the BLE write and return immediately; the disconnect is expected.
// Use .withoutResponse so CoreBluetooth fires the bytes immediately into the
// BLE radio buffer without waiting for a GATT round-trip. With .withResponse,
// the beacon reboots before the ACK arrives, and CoreBluetooth may silently
// drop the write leaving the config unsaved and the beacon still flashing.
if name == "SaveConfig" {
peripheral.writeValue(packet, for: writeChar, type: .withResponse)
peripheral.writeValue(packet, for: writeChar, type: .withoutResponse)
await diagnosticLog?.log("write", "✅ [\(index + 1)/\(commands.count)] SaveConfig sent — beacon will reboot")
await diagnosticLog?.log("write", "✅ All commands written successfully")
return

View file

@ -220,7 +220,7 @@ actor APIClient {
uuid: String,
major: Int,
minor: Int,
macAddress: String?,
hardwareId: String,
beaconType: String,
token: String
) async throws {
@ -230,9 +230,9 @@ actor APIClient {
"UUID": uuid,
"Major": major,
"Minor": minor,
"HardwareId": hardwareId,
"BeaconType": beaconType
]
if let mac = macAddress { body["MacAddress"] = mac }
let data = try await post(path: "/beacon-sharding/register_beacon_hardware.php", body: body, token: token, businessId: businessId)
let resp = try JSONDecoder().decode(OKResponse.self, from: data)
guard resp.OK else {
@ -241,12 +241,13 @@ actor APIClient {
}
func verifyBeaconBroadcast(
hardwareId: String,
uuid: String,
major: Int,
minor: Int,
token: String
) async throws {
let body: [String: Any] = ["UUID": uuid, "Major": major, "Minor": minor]
let body: [String: Any] = ["HardwareId": hardwareId, "UUID": uuid, "Major": major, "Minor": minor]
let data = try await post(path: "/beacon-sharding/verify_beacon_broadcast.php", body: body, token: token)
let resp = try JSONDecoder().decode(OKResponse.self, from: data)
guard resp.OK else {

View file

@ -706,7 +706,7 @@ struct ScanView: View {
uuid: ns.uuid,
major: ns.major,
minor: Int(config.minor),
macAddress: nil,
hardwareId: selectedBeacon?.id.uuidString ?? "unknown-ios",
beaconType: BeaconType.dxsmart.rawValue,
token: token
)
@ -739,7 +739,7 @@ struct ScanView: View {
uuid: ns.uuid,
major: ns.major,
minor: Int(config.minor),
macAddress: nil,
hardwareId: selectedBeacon?.id.uuidString ?? "unknown-ios",
beaconType: selectedBeacon?.type.rawValue ?? "Unknown",
token: token
)