Skip to content

Commit 6710f46

Browse files
committed
Merge branch 'ble-auto-upgrade'
2 parents b36e9ef + 6c65f16 commit 6710f46

File tree

4 files changed

+117
-17
lines changed

4 files changed

+117
-17
lines changed
Binary file not shown.

backend/devices/bitbox02/bluetooth.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2025 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package bitbox02
16+
17+
import (
18+
"crypto/sha256"
19+
_ "embed" // Needed for the go:embed directive below.
20+
)
21+
22+
//go:embed assets/da14531-firmware.bin
23+
var bluetoothFirmware []byte
24+
25+
func bundledBluetoothFirmwareHash() []byte {
26+
hash := sha256.Sum256(bluetoothFirmware)
27+
return hash[:]
28+
}

backend/devices/bitbox02/device.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package bitbox02
1818

1919
import (
20+
"encoding/hex"
21+
2022
deviceevent "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/device/event"
2123
keystoreInterface "github.com/BitBoxSwiss/bitbox-wallet-app/backend/keystore"
2224
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
@@ -90,6 +92,27 @@ func NewDevice(
9092
Subject: string(deviceevent.EventKeystoreAvailable),
9193
Action: action.Replace,
9294
})
95+
96+
}
97+
}
98+
99+
// Temporary automatic Bluetooth firmware upgrade during the testing phase. We prompt an
100+
// upgrade every time if the bundled firmware does not match the actual firmware on the
101+
// device. TODO: remove before production release.
102+
if ev == firmware.EventStatusChanged && device.SupportsBluetooth() {
103+
switch device.Device.Status() {
104+
case firmware.StatusInitialized, firmware.StatusUninitialized:
105+
info, err := device.DeviceInfo()
106+
if err != nil {
107+
device.log.WithError(err).Error("DeviceInfo")
108+
return
109+
}
110+
if info.Bluetooth != nil &&
111+
info.Bluetooth.FirmwareHash != hex.EncodeToString(bundledBluetoothFirmwareHash()) {
112+
if err := device.BluetoothUpgrade(bluetoothFirmware); err != nil {
113+
device.log.WithError(err).Error("BluetoothUpgrade")
114+
}
115+
}
93116
}
94117
}
95118
})

frontends/ios/BitBoxApp/BitBoxApp/Bluetooth.swift

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ var pairedDeviceIdentifiers: Set<String> {
5151
}
5252
}
5353

54+
class BLEConnectionContext {
55+
let identifier = UUID()
56+
let semaphore = DispatchSemaphore(value: 0)
57+
var readBuffer = Data()
58+
var readBufferLock = NSLock()
59+
}
60+
5461
class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
5562
private var state: State = State(
5663
bluetoothAvailable: false,
@@ -70,9 +77,11 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
7077
// This is for failed connections to not enter an infinite connect loop.
7178
private var dontAutoConnectSet: Set<UUID> = []
7279

73-
private var readBuffer = Data()
74-
private let readBufferLock = NSLock() // Ensure thread-safe buffer access
75-
private let semaphore = DispatchSemaphore(value: 0)
80+
private var currentContext: BLEConnectionContext?
81+
// Locks access to the `currentContext` var only, not to its contents. This is important, as
82+
// one can't keep the context locked while waiting for the semaphore, which would lead to a
83+
// deadlock.
84+
private let currentContextLock = NSLock()
7685

7786
override init() {
7887
super.init()
@@ -93,6 +102,9 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
93102
state.discoveredPeripherals[peripheralID] = metadata
94103
state.scanning = false
95104
updateBackendState()
105+
currentContextLock.lock()
106+
currentContext = BLEConnectionContext()
107+
currentContextLock.unlock()
96108
centralManager.connect(metadata.peripheral, options: nil)
97109
}
98110

@@ -246,17 +258,24 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
246258
return
247259
}
248260

261+
currentContextLock.lock()
262+
guard let ctx = currentContext else {
263+
currentContextLock.unlock()
264+
return
265+
}
266+
currentContextLock.unlock()
267+
249268
if characteristic == pReader, let data = characteristic.value {
250269
if data.count != 64 {
251270
print("BLE: ERROR, expected 64 bytes")
252271
}
253272
print("BLE: received data: \(data.hexEncodedString())")
254-
readBufferLock.lock()
255-
readBuffer.append(data)
256-
readBufferLock.unlock()
273+
ctx.readBufferLock.lock()
274+
ctx.readBuffer.append(data)
275+
ctx.readBufferLock.unlock()
257276

258277
// Signal the semaphore to unblock `readBlocking`
259-
semaphore.signal()
278+
ctx.semaphore.signal()
260279
}
261280
if characteristic == pProduct {
262281
print("BLE: product changed: \(String(describing: parseProduct()))")
@@ -281,13 +300,19 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
281300
pWriter = nil
282301
pProduct = nil
283302
state.discoveredPeripherals.removeAll()
284-
isPaired = false
303+
isPaired = false
285304
updateBackendState()
286305

287306
// Have the backend scan right away, which will make it detect that we disconnected.
288307
// Otherwise there would be up to a second of delay (the backend device manager scan interval).
289308
MobileserverUsbUpdate()
290309

310+
// Unblock a pending readBlocking() call if there is one.
311+
currentContextLock.lock()
312+
currentContext?.semaphore.signal()
313+
currentContext = nil
314+
currentContextLock.unlock()
315+
291316
restartScan()
292317
}
293318

@@ -297,22 +322,46 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
297322
handleDisconnect()
298323
}
299324

300-
func readBlocking(length: Int) -> Data? {
325+
struct ReadError: Error {
326+
let message: String
327+
}
328+
329+
func readBlocking(length: Int) throws -> Data {
301330
if !isConnected() {
302-
return nil
331+
throw ReadError(message: "not connected")
303332
}
304333
print("BLE: wants to read \(length)")
305334

335+
currentContextLock.lock()
336+
guard let ctx = currentContext else {
337+
currentContextLock.unlock()
338+
throw ReadError(message: "no connection context")
339+
}
340+
currentContextLock.unlock()
341+
342+
let currentID = ctx.identifier;
343+
306344
var data = Data()
307345

308346
// Loop until we've read the required amount of data
309347
while data.count < length {
310-
// Block until BLE reader callback notifies us
311-
semaphore.wait()
312-
readBufferLock.lock()
313-
data.append(readBuffer.prefix(64))
314-
readBuffer = readBuffer.advanced(by: 64)
315-
readBufferLock.unlock()
348+
// Block until BLE reader callback notifies us or the peripheral is disconnected.
349+
ctx.semaphore.wait()
350+
351+
if !isConnected() {
352+
throw ReadError(message: "the peripheral has disconnected while reading")
353+
}
354+
currentContextLock.lock()
355+
let exit = currentContext?.identifier != currentID
356+
currentContextLock.unlock()
357+
if exit {
358+
throw ReadError(message: "the peripheral has disconnected while reading")
359+
}
360+
361+
ctx.readBufferLock.lock()
362+
data.append(ctx.readBuffer.prefix(64))
363+
ctx.readBuffer = ctx.readBuffer.advanced(by: 64)
364+
ctx.readBufferLock.unlock()
316365
}
317366
print("BLE: got \(data.count)")
318367

@@ -457,7 +506,7 @@ class BluetoothReadWriteCloser: NSObject, MobileserverGoReadWriteCloserInterface
457506
}
458507

459508
func read(_ n: Int) throws -> Data {
460-
return bluetoothManager.readBlocking(length: n)!
509+
try bluetoothManager.readBlocking(length: n)
461510
}
462511

463512
func write(_ data: Data?, n: UnsafeMutablePointer<Int>?) throws {

0 commit comments

Comments
 (0)