Skip to content

Commit be8c848

Browse files
committed
Merge remote-tracking branch 'benma/bt'
2 parents 3b5c70a + 9f27f8a commit be8c848

File tree

18 files changed

+473
-46
lines changed

18 files changed

+473
-46
lines changed

backend/backend.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/config"
4040
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bitbox"
4141
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bitbox02"
42+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bluetooth"
4243
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/device"
4344
deviceevent "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/device/event"
4445
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/usb"
@@ -176,6 +177,9 @@ type Environment interface {
176177
// OnAuthSettingChanged is called when the authentication (screen lock) setting is changed.
177178
// This is also called when the app launches with the current setting.
178179
OnAuthSettingChanged(enabled bool)
180+
// BluetoothConnect tries to connect to the peripheral by the given identifier.
181+
// Use `backend.bluetooth.State()` to track failure.
182+
BluetoothConnect(identifier string)
179183
}
180184

181185
// Backend ties everything together and is the main starting point to use the BitBox wallet library.
@@ -194,6 +198,7 @@ type Backend struct {
194198
devices map[string]device.Interface
195199

196200
usbManager *usb.Manager
201+
bluetooth *bluetooth.Bluetooth
197202

198203
accountsAndKeystoreLock locker.Locker
199204
accounts AccountsList
@@ -265,7 +270,6 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
265270
coins: map[coinpkg.Code]coinpkg.Coin{},
266271
accounts: []accounts.Interface{},
267272
aopp: AOPP{State: aoppStateInactive},
268-
269273
makeBtcAccount: func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, log *logrus.Entry) accounts.Interface {
270274
return btc.NewAccount(config, coin, gapLimits, log, hclient)
271275
},
@@ -297,6 +301,9 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
297301
backend.banners = banners.NewBanners()
298302
backend.banners.Observe(backend.Notify)
299303

304+
backend.bluetooth = bluetooth.New(log)
305+
backend.bluetooth.Observe(backend.Notify)
306+
300307
return backend, nil
301308
}
302309

@@ -1052,3 +1059,8 @@ func (backend *Backend) ExportLogs() error {
10521059
}
10531060
return nil
10541061
}
1062+
1063+
// Bluetooth returns the backend's bluetooth instance.
1064+
func (backend *Backend) Bluetooth() *bluetooth.Bluetooth {
1065+
return backend.bluetooth
1066+
}

backend/backend_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ func (e environment) Auth() {}
244244

245245
func (e environment) OnAuthSettingChanged(bool) {}
246246

247+
func (e environment) BluetoothConnect(string) {}
248+
247249
type mockTransactionsSource struct {
248250
}
249251

backend/bridgecommon/bridgecommon.go

+26
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package bridgecommon
1919
import (
2020
"bytes"
2121
"encoding/hex"
22+
"encoding/json"
2223
"net/http"
2324
"runtime"
2425
"runtime/debug"
@@ -28,6 +29,7 @@ import (
2829
"github.com/BitBoxSwiss/bitbox-wallet-app/backend"
2930
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/arguments"
3031
btctypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/types"
32+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bluetooth"
3133
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/usb"
3234
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/handlers"
3335
"github.com/BitBoxSwiss/bitbox-wallet-app/util/config"
@@ -192,6 +194,7 @@ type BackendEnvironment struct {
192194
DetectDarkThemeFunc func() bool
193195
AuthFunc func()
194196
OnAuthSettingChangedFunc func(bool)
197+
BluetoothConnectFunc func(string)
195198
}
196199

197200
// NotifyUser implements backend.Environment.
@@ -270,6 +273,13 @@ func (env *BackendEnvironment) OnAuthSettingChanged(enabled bool) {
270273
}
271274
}
272275

276+
// BluetoothConnect implements backend.Environment.
277+
func (env *BackendEnvironment) BluetoothConnect(identifier string) {
278+
if env.BluetoothConnectFunc != nil {
279+
env.BluetoothConnectFunc(identifier)
280+
}
281+
}
282+
273283
// Serve serves the BitBox API for use in a native client.
274284
func Serve(
275285
testnet bool,
@@ -369,3 +379,19 @@ func UsbUpdate() {
369379
}
370380
globalBackend.UsbUpdate()
371381
}
382+
383+
// BluetoothSetState wraps backend.Bluetooth().SetState.
384+
// The json byte are parsed according to `bluetooth.State`.
385+
func BluetoothSetState(jsonState string) error {
386+
mu.RLock()
387+
defer mu.RUnlock()
388+
if globalBackend == nil {
389+
return nil
390+
}
391+
var state bluetooth.State
392+
if err := json.Unmarshal([]byte(jsonState), &state); err != nil {
393+
return err
394+
}
395+
globalBackend.Bluetooth().SetState(&state)
396+
return nil
397+
}

backend/bridgecommon/bridgecommon_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ func (e environment) Auth() {}
7373

7474
func (e environment) OnAuthSettingChanged(bool) {}
7575

76+
func (e environment) BluetoothConnect(string) {}
77+
7678
// TestServeShutdownServe checks that you can call Serve twice in a row.
7779
func TestServeShutdownServe(t *testing.T) {
7880
bridgecommon.Serve(
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 bluetooth
16+
17+
import (
18+
"github.com/BitBoxSwiss/bitbox-wallet-app/util/locker"
19+
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable"
20+
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable/action"
21+
"github.com/sirupsen/logrus"
22+
)
23+
24+
// Peripheral is a bluetooth peripheral.
25+
type Peripheral struct {
26+
Identifier string `json:"identifier"`
27+
Name string `json:"name"`
28+
ConnectionError *string `json:"connectionError,omitempty"`
29+
}
30+
31+
// State contains everything needed to render bluetooth peripherals and other data in the frontend.
32+
type State struct {
33+
// BluetoothAvailable is false if bluetooth is powered off or otherwise unavailable.
34+
BluetoothAvailable bool `json:"bluetoothAvailable"`
35+
Peripherals []*Peripheral `json:"peripherals"`
36+
Connecting bool `json:"connecting"`
37+
}
38+
39+
// Bluetooth manages a list of peripherals.
40+
type Bluetooth struct {
41+
observable.Implementation
42+
43+
state *State
44+
stateLock locker.Locker
45+
46+
log *logrus.Entry
47+
}
48+
49+
// New creates a new instance of Bluetooth.
50+
func New(log *logrus.Entry) *Bluetooth {
51+
b := &Bluetooth{
52+
state: &State{
53+
Peripherals: []*Peripheral{},
54+
},
55+
log: log,
56+
}
57+
return b
58+
}
59+
60+
// SetState sets the current list of discovered peripherals and other state data.
61+
func (b *Bluetooth) SetState(state *State) {
62+
defer b.stateLock.Lock()()
63+
b.log.WithField("state", state).Info("bluetooth setstate")
64+
b.state = state
65+
b.Notify(observable.Event{
66+
Subject: "bluetooth/state",
67+
Action: action.Replace,
68+
Object: state,
69+
})
70+
}
71+
72+
// State returns the current list of discovered peripherals and other bluetooth state data.
73+
func (b *Bluetooth) State() *State {
74+
defer b.stateLock.RLock()()
75+
return b.state
76+
}

backend/handlers/handlers.go

+20
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import (
4545
bitbox02Handlers "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bitbox02/handlers"
4646
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bitbox02bootloader"
4747
bitbox02bootloaderHandlers "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bitbox02bootloader/handlers"
48+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bluetooth"
4849
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/device"
4950
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/exchanges"
5051
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/keystore"
@@ -117,6 +118,7 @@ type Backend interface {
117118
CancelConnectKeystore()
118119
SetWatchonly(rootFingerprint []byte, watchonly bool) error
119120
LookupEthAccountCode(address string) (accountsTypes.Code, string, error)
121+
Bluetooth() *bluetooth.Bluetooth
120122
}
121123

122124
// Handlers provides a web api to the backend.
@@ -262,6 +264,9 @@ func NewHandlers(
262264
getAPIRouterNoError(apiRouter)("/notes/export", handlers.postExportNotes).Methods("POST")
263265
getAPIRouterNoError(apiRouter)("/notes/import", handlers.postImportNotes).Methods("POST")
264266

267+
getAPIRouterNoError(apiRouter)("/bluetooth/state", handlers.getBluetoothState).Methods("GET")
268+
getAPIRouterNoError(apiRouter)("/bluetooth/connect", handlers.postBluetoothConnect).Methods("POST")
269+
265270
devicesRouter := getAPIRouterNoError(apiRouter.PathPrefix("/devices").Subrouter())
266271
devicesRouter("/registered", handlers.getDevicesRegistered).Methods("GET")
267272

@@ -1616,3 +1621,18 @@ func (handlers *Handlers) postImportNotes(r *http.Request) interface{} {
16161621
}
16171622
return result{Success: true, Data: data}
16181623
}
1624+
1625+
func (handlers *Handlers) getBluetoothState(r *http.Request) interface{} {
1626+
return handlers.backend.Bluetooth().State()
1627+
}
1628+
1629+
func (handlers *Handlers) postBluetoothConnect(r *http.Request) interface{} {
1630+
var identifier string
1631+
if err := json.NewDecoder(r.Body).Decode(&identifier); err != nil {
1632+
// We assume this will never fail to simplify handling in the frontend.
1633+
return nil
1634+
}
1635+
1636+
handlers.backend.Environment().BluetoothConnect(identifier)
1637+
return nil
1638+
}

backend/handlers/handlers_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func (e *backendEnv) SetDarkTheme(bool) {}
6060
func (e *backendEnv) DetectDarkTheme() bool { return false }
6161
func (e *backendEnv) Auth() {}
6262
func (e *backendEnv) OnAuthSettingChanged(bool) {}
63+
func (e *backendEnv) BluetoothConnect(string) {}
6364

6465
func TestGetNativeLocale(t *testing.T) {
6566
const ptLocale = "pt"

backend/mobileserver/mobileserver.go

+7
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ type GoEnvironmentInterface interface {
100100
DetectDarkTheme() bool
101101
Auth()
102102
OnAuthSettingChanged(bool)
103+
BluetoothConnect(string)
103104
}
104105

105106
// readWriteCloser implements io.ReadWriteCloser, translating from GoReadWriteCloserInterface. All methods
@@ -212,6 +213,7 @@ func Serve(dataDir string, testnet bool, environment GoEnvironmentInterface, goA
212213
DetectDarkThemeFunc: environment.DetectDarkTheme,
213214
AuthFunc: environment.Auth,
214215
OnAuthSettingChangedFunc: environment.OnAuthSettingChanged,
216+
BluetoothConnectFunc: environment.BluetoothConnect,
215217
},
216218
)
217219
}
@@ -242,3 +244,8 @@ func AuthResult(ok bool) {
242244
func ManualReconnect() {
243245
bridgecommon.ManualReconnect()
244246
}
247+
248+
// BluetoothSetState wraps bridgecommon.BluetoothSetState.
249+
func BluetoothSetState(jsonState string) error {
250+
return bridgecommon.BluetoothSetState(jsonState)
251+
}

cmd/servewallet/main.go

+4
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ func (webdevEnvironment) Auth() {
9696
func (webdevEnvironment) OnAuthSettingChanged(enabled bool) {
9797
}
9898

99+
// BluetoothConnect implements backend.Environment.
100+
func (webdevEnvironment) BluetoothConnect(identifier string) {
101+
}
102+
99103
// NativeLocale naively implements backend.Environment.
100104
// This version is unlikely to work on Windows.
101105
func (webdevEnvironment) NativeLocale() string {

frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/GoViewModel.java

+3
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ public void onAuthSettingChanged(boolean enabled) {
136136
authSetting.postValue(enabled);
137137
}
138138

139+
public void bluetoothConnect(String identifier) {
140+
}
141+
139142
public boolean usingMobileData() {
140143
// Adapted from https://stackoverflow.com/a/53243938
141144

frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift

+17-8
Original file line numberDiff line numberDiff line change
@@ -40,35 +40,34 @@ protocol SetMessageHandlersProtocol {
4040

4141
class GoEnvironment: NSObject, MobileserverGoEnvironmentInterfaceProtocol, UIDocumentInteractionControllerDelegate {
4242
private let bluetoothManager: BluetoothManager
43-
43+
4444
init(bluetoothManager: BluetoothManager) {
4545
self.bluetoothManager = bluetoothManager
4646
}
47-
47+
4848
func getSaveFilename(_ fileName: String?) -> String {
4949
let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
5050
// fileName cannot be nil this is called by Go and Go strings cannot be nil/null.
5151
let fileURL = tempDirectory.appendingPathComponent(fileName!)
5252
return fileURL.path
5353
}
54-
54+
5555
func auth() {
5656
authenticateUser { success in
5757
// TODO: enabling auth but entering wrong passcode does not remove auth screen
5858
MobileserverAuthResult(success)
5959
}
6060
}
61-
61+
6262
func detectDarkTheme() -> Bool {
6363
return UIScreen.main.traitCollection.userInterfaceStyle == .dark
6464
}
65-
65+
6666
func deviceInfo() -> MobileserverGoDeviceInfoInterfaceProtocol? {
6767
if !bluetoothManager.isConnected() {
6868
return nil
6969
}
70-
print("Bluetooth product string: \(bluetoothManager.productStr())")
71-
70+
7271
let productStr = bluetoothManager.productStr();
7372
if productStr == "" || productStr == "no connection" {
7473
// Not ready or explicitly not connected (waiting for the device to enter
@@ -89,6 +88,16 @@ class GoEnvironment: NSObject, MobileserverGoEnvironmentInterfaceProtocol, UIDoc
8988
// TODO: hide app window contents in app switcher, maybe always not just when auth is on.
9089
}
9190

91+
func bluetoothConnect(_ identifier: String?) {
92+
guard let identifier = identifier else {
93+
return
94+
}
95+
guard let uuid = UUID(uuidString: identifier) else {
96+
return
97+
}
98+
bluetoothManager.connect(to: uuid)
99+
}
100+
92101
func setDarkTheme(_ p0: Bool) {
93102
}
94103

@@ -159,7 +168,7 @@ class GoAPI: NSObject, MobileserverGoAPIInterfaceProtocol, SetMessageHandlersPro
159168
@main
160169
struct BitBoxAppApp: App {
161170
@StateObject private var bluetoothManager = BluetoothManager()
162-
171+
163172
var body: some Scene {
164173
WindowGroup {
165174
GridLayout(alignment: .leading) {

0 commit comments

Comments
 (0)