Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 70bec48

Browse files
committedMar 12, 2025·
ios: add a connectionState enum for the frontend
We are either scanning, connecting/pairing, or connected. This should help the frontend display everything appropriately.
1 parent 14fab83 commit 70bec48

File tree

4 files changed

+99
-34
lines changed

4 files changed

+99
-34
lines changed
 

‎backend/devices/bluetooth/bluetooth.go

+43-8
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,62 @@
1515
package bluetooth
1616

1717
import (
18+
"encoding/json"
19+
"fmt"
20+
1821
"github.com/BitBoxSwiss/bitbox-wallet-app/util/locker"
1922
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable"
2023
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable/action"
2124
"github.com/sirupsen/logrus"
2225
)
2326

27+
// ConnectionState represents the a peripheral's connection state.
28+
type ConnectionState string
29+
30+
const (
31+
// ConnectionStateDiscovered means the peripheral is discovered.
32+
ConnectionStateDiscovered ConnectionState = "discovered"
33+
// ConnectionStateConnecting means a connection & pairing attempt is in progress.
34+
ConnectionStateConnecting ConnectionState = "connecting"
35+
// ConnectionStateConnected means successfully connected and paired.
36+
ConnectionStateConnected ConnectionState = "connected"
37+
// ConnectionStateError means there was a conection error, see ConnectionError.
38+
ConnectionStateError ConnectionState = "error"
39+
)
40+
41+
func (cs *ConnectionState) UnmarshalJSON(data []byte) error {
42+
var s string
43+
if err := json.Unmarshal(data, &s); err != nil {
44+
return err
45+
}
46+
switch ConnectionState(s) {
47+
case ConnectionStateDiscovered,
48+
ConnectionStateConnecting,
49+
ConnectionStateConnected,
50+
ConnectionStateError:
51+
*cs = ConnectionState(s)
52+
return nil
53+
default:
54+
return fmt.Errorf("invalid ConnectionState: %q", s)
55+
}
56+
}
57+
2458
// Peripheral is a bluetooth peripheral.
2559
type Peripheral struct {
26-
Identifier string `json:"identifier"`
27-
Name string `json:"name"`
28-
ConnectionError *string `json:"connectionError,omitempty"`
60+
Identifier string `json:"identifier"`
61+
Name string `json:"name"`
62+
// ConnectionState shows the current connection phase
63+
ConnectionState ConnectionState `json:"connectionState"`
64+
ConnectionError *string `json:"connectionError,omitempty"`
2965
}
3066

3167
// State contains everything needed to render bluetooth peripherals and other data in the frontend.
3268
type State struct {
3369
// BluetoothAvailable is false if bluetooth is powered off or otherwise unavailable.
34-
BluetoothAvailable bool `json:"bluetoothAvailable"`
35-
Peripherals []*Peripheral `json:"peripherals"`
36-
// Connecting is true from the moment we try to connect until after we are paired (or until
37-
// either step fails).
38-
Connecting bool `json:"connecting"`
70+
BluetoothAvailable bool `json:"bluetoothAvailable"`
71+
// Scanning is true if we are currently scanning for peripherals.
72+
Scanning bool `json:"scanning"`
73+
Peripherals []*Peripheral `json:"peripherals"`
3974
}
4075

4176
// Bluetooth manages a list of peripherals.

‎frontends/ios/BitBoxApp/BitBoxApp/Bluetooth.swift

+42-19
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,25 @@ struct ProductInfo: Codable {
1919
}
2020
}
2121

22+
enum ConnectionState: String, Codable {
23+
case discovered
24+
// from the moment we try to connect until after we are paired (or until either step fails).
25+
case connecting
26+
case connected
27+
// something went wrong, see connectionError.
28+
case error
29+
}
30+
2231
struct State {
2332
var bluetoothAvailable: Bool
33+
var scanning: Bool
2434
var discoveredPeripherals: [UUID: PeripheralMetadata]
25-
// true from the moment we try to connect until after we are paired (or until either step fails).
26-
var connecting: Bool
2735
}
2836

2937
struct PeripheralMetadata {
3038
let peripheral: CBPeripheral
3139
let discoveredDate: Date
40+
var connectionState: ConnectionState
3241
var connectionError: String? = nil
3342
}
3443

@@ -43,14 +52,18 @@ var pairedDeviceIdentifiers: Set<String> {
4352
}
4453

4554
class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
46-
private var state: State = State(bluetoothAvailable: false, discoveredPeripherals: [:], connecting: false)
55+
private var state: State = State(
56+
bluetoothAvailable: false,
57+
scanning: false,
58+
discoveredPeripherals: [:]
59+
)
4760

4861
var centralManager: CBCentralManager!
4962
var connectedPeripheral: CBPeripheral?
5063
var pWriter: CBCharacteristic?
5164
var pReader: CBCharacteristic?
5265
var pProduct: CBCharacteristic?
53-
66+
5467
private var isPaired: Bool = false
5568

5669
// Peripherals in this set will not be auto-connected even if previously paired.
@@ -73,10 +86,12 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
7386
}
7487

7588
func connect(to peripheralID: UUID) {
76-
guard let metadata = state.discoveredPeripherals[peripheralID] else { return }
89+
guard var metadata = state.discoveredPeripherals[peripheralID] else { return }
7790
centralManager.stopScan()
78-
state.discoveredPeripherals[peripheralID]?.connectionError = nil
79-
state.connecting = true
91+
metadata.connectionError = nil
92+
metadata.connectionState = .connecting
93+
state.discoveredPeripherals[peripheralID] = metadata
94+
state.scanning = false
8095
updateBackendState()
8196
centralManager.connect(metadata.peripheral, options: nil)
8297
}
@@ -86,6 +101,7 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
86101
!centralManager.isScanning,
87102
connectedPeripheral == nil else { return }
88103
state.discoveredPeripherals.removeAll()
104+
state.scanning = true
89105
updateBackendState()
90106
centralManager.scanForPeripherals(
91107
withServices: [CBUUID(string: "e1511a45-f3db-44c0-82b8-6c880790d1f1")],
@@ -114,7 +130,8 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
114130
if state.discoveredPeripherals[identifier] == nil {
115131
state.discoveredPeripherals[identifier] = PeripheralMetadata(
116132
peripheral: peripheral,
117-
discoveredDate: Date()
133+
discoveredDate: Date(),
134+
connectionState: .discovered
118135
)
119136
print("BLE: discovered \(peripheral.name ?? "unknown device")")
120137
if let data = advertisementData["kCBAdvDataManufacturerData"] as? Data {
@@ -149,11 +166,12 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
149166

150167
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
151168
let errorMessage = error?.localizedDescription ?? "unknown error"
169+
state.discoveredPeripherals[peripheral.identifier]?.connectionState = .error
152170
state.discoveredPeripherals[peripheral.identifier]?.connectionError = errorMessage
153-
state.connecting = false
154-
dontAutoConnectSet.insert(peripheral.identifier)
155171
updateBackendState()
172+
dontAutoConnectSet.insert(peripheral.identifier)
156173
print("BLE: connection failed to \(peripheral.name ?? "unknown device"): \(errorMessage)")
174+
restartScan()
157175
}
158176

159177
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
@@ -240,29 +258,28 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
240258
// Add to paired devices
241259
pairedDeviceIdentifiers.insert(peripheral.identifier.uuidString)
242260
}
243-
state.connecting = false
261+
state.discoveredPeripherals[peripheral.identifier]?.connectionState = .connected
244262
updateBackendState()
245263
// Invoke device manager to scan now, which will make it detect the device being connected
246264
// (or disconnected, in case the product string indicates that) now instead of waiting for
247265
// the next scan.
248266
MobileserverUsbUpdate()
249267
}
250268
}
251-
269+
252270
func handleDisconnect() {
253271
connectedPeripheral = nil
254272
pReader = nil
255273
pWriter = nil
256274
pProduct = nil
257275
state.discoveredPeripherals.removeAll()
258-
state.connecting = false
259-
isPaired = false
276+
isPaired = false
260277
updateBackendState()
261-
278+
262279
// Have the backend scan right away, which will make it detect that we disconnected.
263280
// Otherwise there would be up to a second of delay (the backend device manager scan interval).
264281
MobileserverUsbUpdate()
265-
282+
266283
restartScan()
267284
}
268285

@@ -299,7 +316,7 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
299316
let value = pProduct.value else {
300317
return nil
301318
}
302-
319+
303320
if value.isEmpty {
304321
return nil
305322
}
@@ -320,25 +337,31 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
320337
struct PeripheralJSON: Codable {
321338
let identifier: String
322339
let name: String
340+
let connectionState: ConnectionState
323341
let connectionError: String?
324342
}
325343

326344
struct StateJSON: Codable {
327345
let bluetoothAvailable: Bool
346+
let scanning: Bool
328347
let peripherals: [PeripheralJSON]
329-
let connecting: Bool
330348
}
331349

332350
// Convert discoveredPeripherals to the JSON structure
333351
let peripherals = Array(state.discoveredPeripherals.values).map { metadata in
334352
PeripheralJSON(
335353
identifier: metadata.peripheral.identifier.uuidString,
336354
name: metadata.peripheral.name ?? "BitBox",
355+
connectionState: metadata.connectionState,
337356
connectionError: metadata.connectionError
338357
)
339358
}
340359

341-
let state = StateJSON(bluetoothAvailable: state.bluetoothAvailable, peripherals: peripherals, connecting: state.connecting)
360+
let state = StateJSON(
361+
bluetoothAvailable: state.bluetoothAvailable,
362+
scanning: state.scanning,
363+
peripherals: peripherals
364+
)
342365

343366
do {
344367
let encoder = JSONEncoder()

‎frontends/web/src/api/bluetooth.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,20 @@ import { subscribeEndpoint, TUnsubscribe } from './subscribe';
2020
export type TPeripheral = {
2121
identifier: string;
2222
name: string;
23-
connectionError?: string;
24-
};
23+
} & (
24+
| {
25+
connectionState: 'discovered' | 'connecting' | 'connected';
26+
}
27+
| {
28+
connectionState: 'error';
29+
connectionError: string;
30+
}
31+
);
2532

2633
export type TState = {
2734
bluetoothAvailable: boolean;
35+
scanning: boolean;
2836
peripherals: TPeripheral[];
29-
connecting: boolean;
3037
};
3138

3239
export const getState = (): Promise<TState> => {

‎frontends/web/src/components/bluetooth/bluetooth.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const _Bluetooth = () => {
3737
{t('bluetooth.select')}
3838
</div>
3939
<div className={styles.container}>
40-
{ state.connecting ? <p>connecting</p> : null }
40+
{ state.scanning ? 'scanning' : null }
4141
{state.peripherals.map(peripheral => {
4242
return (
4343
<ActionableItem
@@ -46,14 +46,14 @@ const _Bluetooth = () => {
4646
<span>
4747
{ peripheral.name !== '' ? peripheral.name : peripheral.identifier }
4848
{' '}
49-
{ peripheral.connectionError ? (
49+
{ peripheral.connectionState === 'error' ? (
5050
<Badge type="danger">
5151
{t('bluetooth.connectionFailed')}
5252
</Badge>
5353
) : null }
54-
{ peripheral.connectionError ? (
54+
{ peripheral.connectionState === 'error' ? (
5555
<p>{ peripheral.connectionError }</p>
56-
) : null }
56+
) : peripheral.connectionState }
5757
</span>
5858

5959
</ActionableItem>

0 commit comments

Comments
 (0)
Please sign in to comment.