diff --git a/backend/backend.go b/backend/backend.go index dc12816ca0..aa8bccc0dc 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -405,7 +405,7 @@ func (backend *Backend) OnDeviceUninit(f func(string)) { // Start starts the background services. It returns a channel of events to handle by the library // client. func (backend *Backend) Start() <-chan interface{} { - go backend.listenHID() + usb.NewManager(backend.arguments.MainDirectoryPath(), backend.Register, backend.Deregister).Start() return backend.events } @@ -414,13 +414,9 @@ func (backend *Backend) Events() <-chan interface{} { return backend.events } -// DevicesRegistered returns a slice of device IDs of registered devices. -func (backend *Backend) DevicesRegistered() []string { - deviceIDs := []string{} - for deviceID := range backend.devices { - deviceIDs = append(deviceIDs, deviceID) - } - return deviceIDs +// DevicesRegistered returns a map of device IDs to device of registered devices. +func (backend *Backend) DevicesRegistered() map[string]device.Interface { + return backend.devices } func (backend *Backend) uninitAccounts() { @@ -516,10 +512,6 @@ func (backend *Backend) Deregister(deviceID string) { } } -func (backend *Backend) listenHID() { - usb.NewManager(backend.arguments.MainDirectoryPath(), backend.Register, backend.Deregister).ListenHID() -} - // Rates return the latest rates. func (backend *Backend) Rates() map[string]map[string]float64 { return backend.ratesUpdater.Last() diff --git a/backend/devices/device/device.go b/backend/devices/device/device.go index d4732e5a5f..0b830fd7e3 100644 --- a/backend/devices/device/device.go +++ b/backend/devices/device/device.go @@ -36,7 +36,7 @@ const ( type Interface interface { Init(testing bool) // ProductName returns the product name of the device in lowercase. - // ProductName() string + ProductName() string // Identifier returns the hash of the type and the serial number. Identifier() string diff --git a/backend/devices/usb/manager.go b/backend/devices/usb/manager.go index 276f84ca3b..038cfc33cc 100644 --- a/backend/devices/usb/manager.go +++ b/backend/devices/usb/manager.go @@ -16,7 +16,6 @@ package usb import ( "encoding/hex" - "fmt" "os" "regexp" "time" @@ -31,23 +30,26 @@ import ( ) const ( - vendorID = 0x03eb - productID = 0x2402 + bitboxVendorID = 0x03eb + bitboxProductID = 0x2402 ) +func isBitBox(deviceInfo hid.DeviceInfo) bool { + return deviceInfo.VendorID == bitboxVendorID && deviceInfo.ProductID == bitboxProductID && (deviceInfo.UsagePage == 0xffff || deviceInfo.Interface == 0) +} + // DeviceInfos returns a slice of all found bitbox devices. func DeviceInfos() []hid.DeviceInfo { deviceInfos := []hid.DeviceInfo{} - for _, deviceInfo := range hid.Enumerate(vendorID, productID) { - if deviceInfo.Interface != 0 && deviceInfo.UsagePage != 0xffff { - continue - } + for _, deviceInfo := range hid.Enumerate(0, 0) { // If Enumerate() is called too quickly after a device is inserted, the HID device input // report is not yet ready. if deviceInfo.Serial == "" || deviceInfo.Product == "" { continue } - deviceInfos = append(deviceInfos, deviceInfo) + if isBitBox(deviceInfo) { + deviceInfos = append(deviceInfos, deviceInfo) + } } return deviceInfos } @@ -78,36 +80,33 @@ func NewManager( channelConfigDir: channelConfigDir, onRegister: onRegister, onUnregister: onUnregister, - log: logging.Get().WithGroup("manager"), + + log: logging.Get().WithGroup("manager"), } } -func deviceIdentifier(productName string, path string) string { - return hex.EncodeToString([]byte(fmt.Sprintf("%s%s", productName, path))) +func deviceIdentifier(deviceInfo hid.DeviceInfo) string { + return hex.EncodeToString([]byte(deviceInfo.Path)) } -func (manager *Manager) register(deviceInfo hid.DeviceInfo) error { - deviceID := deviceIdentifier(bitbox.ProductName, deviceInfo.Path) - // Skip if already registered. - if _, ok := manager.devices[deviceID]; ok { - return nil - } - manager.log.WithField("device-id", deviceID).Info("Registering device") +func (manager *Manager) makeBitBox(deviceInfo hid.DeviceInfo) (*bitbox.Device, error) { + deviceID := deviceIdentifier(deviceInfo) + manager.log.WithField("device-id", deviceID).Info("Registering BitBox") bootloader := deviceInfo.Product == "bootloader" || deviceInfo.Product == "Digital Bitbox bootloader" match := regexp.MustCompile(`v([0-9]+\.[0-9]+\.[0-9]+)`).FindStringSubmatch(deviceInfo.Serial) if len(match) != 2 { manager.log.WithField("serial", deviceInfo.Serial).Error("Serial number is malformed") - return errp.Newf("Could not find the firmware version in '%s'.", deviceInfo.Serial) + return nil, errp.Newf("Could not find the firmware version in '%s'.", deviceInfo.Serial) } firmwareVersion, err := semver.NewSemVerFromString(match[1]) if err != nil { - return errp.WithContext(errp.WithMessage(err, "Failed to read version from serial number"), + return nil, errp.WithContext(errp.WithMessage(err, "Failed to read version from serial number"), errp.Context{"serial": deviceInfo.Serial}) } hidDevice, err := deviceInfo.Open() if err != nil { - return errp.WithMessage(err, "Failed to open device") + return nil, errp.WithMessage(err, "Failed to open device") } usbWriteReportSize := 64 @@ -128,23 +127,19 @@ func (manager *Manager) register(deviceInfo hid.DeviceInfo) error { NewCommunication(hidDevice, usbWriteReportSize, usbReadReportSize), ) if err != nil { - return errp.WithMessage(err, "Failed to establish communication to device") - } - if err := manager.onRegister(device); err != nil { - return errp.WithMessage(err, "Failed to execute on-register") + return nil, errp.WithMessage(err, "Failed to establish communication to device") } - manager.devices[deviceID] = device // Unlock the device automatically if the user set the PIN as an environment variable. pin := os.Getenv("BITBOX_PIN") if pin != "" { if _, _, err := device.Login(pin); err != nil { - return errp.WithMessage(err, "Failed to unlock the BitBox with the provided PIN.") + return nil, errp.WithMessage(err, "Failed to unlock the BitBox with the provided PIN.") } manager.log.Info("Successfully unlocked the device with the PIN from the environment.") } - return nil + return device, nil } // checkIfRemoved returns true if a device was plugged in, but is not plugged in anymore. @@ -154,7 +149,7 @@ func (manager *Manager) checkIfRemoved(deviceID string) bool { // multiple times. for i := 0; i < 5; i++ { for _, deviceInfo := range DeviceInfos() { - if deviceIdentifier(bitbox.ProductName, deviceInfo.Path) == deviceID { + if deviceIdentifier(deviceInfo) == deviceID { return false } } @@ -163,8 +158,7 @@ func (manager *Manager) checkIfRemoved(deviceID string) bool { return true } -// ListenHID listens for inserted/removed devices forever. Run this in a goroutine. -func (manager *Manager) ListenHID() { +func (manager *Manager) listen() { for { for deviceID, device := range manager.devices { // Check if device was removed. @@ -179,10 +173,32 @@ func (manager *Manager) ListenHID() { // Check if device was inserted. deviceInfos := DeviceInfos() for _, deviceInfo := range deviceInfos { - if err := manager.register(deviceInfo); err != nil { - manager.log.WithError(err).Error("Failed to register device") + deviceID := deviceIdentifier(deviceInfo) + // Skip if already registered. + if _, ok := manager.devices[deviceID]; ok { + continue + } + var device device.Interface + if isBitBox(deviceInfo) { + var err error + device, err = manager.makeBitBox(deviceInfo) + if err != nil { + manager.log.WithError(err).Error("Failed to register bitbox") + continue + } + } else { + panic("unrecognized device") + } + manager.devices[deviceID] = device + if err := manager.onRegister(device); err != nil { + manager.log.WithError(err).Error("Failed to execute on-register") } } time.Sleep(time.Second) } } + +// Start listens for inserted/removed devices forever. Run this in a goroutine. +func (manager *Manager) Start() { + go manager.listen() +} diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 62f254ce40..25fdebd071 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -58,7 +58,7 @@ type Backend interface { OnAccountUninit(f func(btc.Interface)) OnDeviceInit(f func(device.Interface)) OnDeviceUninit(f func(deviceID string)) - DevicesRegistered() []string + DevicesRegistered() map[string]device.Interface Start() <-chan interface{} Keystores() keystore.Keystores RegisterKeystore(keystore.Keystore) @@ -286,7 +286,11 @@ func (handlers *Handlers) getAccountsStatusHandler(_ *http.Request) (interface{} } func (handlers *Handlers) getDevicesRegisteredHandler(_ *http.Request) (interface{}, error) { - return handlers.backend.DevicesRegistered(), nil + jsonDevices := map[string]string{} + for deviceID, device := range handlers.backend.DevicesRegistered() { + jsonDevices[deviceID] = device.ProductName() + } + return jsonDevices, nil } func (handlers *Handlers) registerTestKeyStoreHandler(r *http.Request) (interface{}, error) { diff --git a/frontends/web/src/app.jsx b/frontends/web/src/app.jsx index 74cd380abb..6b0b903f23 100644 --- a/frontends/web/src/app.jsx +++ b/frontends/web/src/app.jsx @@ -19,13 +19,12 @@ import { i18nEditorActive } from './i18n/i18n'; import TranslationHelper from './components/translationhelper/translationhelper'; import { Component, h } from 'preact'; -import { route } from 'preact-router'; import { apiGet } from './utils/request'; import { apiWebsocket } from './utils/websocket'; import { Update } from './components/update/update'; import Sidebar from './components/sidebar/sidebar'; import Container from './components/container/Container'; -import Device from './routes/device/device'; +import { DeviceSwitch } from './routes/device/deviceswitch'; import Account from './routes/account/account'; import Send from './routes/account/send/send'; import Receive from './routes/account/receive/receive'; @@ -38,9 +37,10 @@ import { Confirm } from './components/confirm/Confirm'; export class App extends Component { state = { - accounts: [], + accounts: null, accountsInitialized: false, deviceIDs: [], + devices: {}, activeSidebar: false, } @@ -95,22 +95,16 @@ export class App extends Component { } onDevicesRegisteredChanged = () => { - apiGet('devices/registered').then(deviceIDs => { - this.setState({ deviceIDs }); + apiGet('devices/registered').then(devices => { + const deviceIDs = Object.keys(devices); + this.setState({ devices, deviceIDs }); }); } onAccountsStatusChanged = () => { apiGet('accounts-status').then(status => { const accountsInitialized = status === 'initialized'; - this.setState({ - accountsInitialized - }); - if (!accountsInitialized) { - console.log('app.jsx route /'); // eslint-disable-line no-console - route('/', true); - } - + this.setState({ accountsInitialized }); apiGet('accounts').then(accounts => this.setState({ accounts })); }); } @@ -121,6 +115,7 @@ export class App extends Component { render({}, { accounts, + devices, deviceIDs, accountsInitialized, activeSidebar, @@ -143,8 +138,7 @@ export class App extends Component { accounts={accounts} /> + deviceIDs={deviceIDs} /> @@ -162,13 +156,15 @@ export class App extends Component { // showCreate={true} // Does not exist! // deviceIDs={deviceIDs} // Does not exist! /> - - + + key={devices} + deviceID={null} + devices={devices} /> diff --git a/frontends/web/src/routes/account/account.jsx b/frontends/web/src/routes/account/account.jsx index 835cdb6f5d..0cc8e6965f 100644 --- a/frontends/web/src/routes/account/account.jsx +++ b/frontends/web/src/routes/account/account.jsx @@ -71,6 +71,10 @@ export default class Account extends Component { } componentDidUpdate(prevProps) { + if (this.props.code && (!this.props.accounts || this.props.accounts.length === 0)) { + console.log('account.jsx route /'); // eslint-disable-line no-console + route('/', true); + } if (!this.props.code) { if (this.props.accounts && this.props.accounts.length) { console.log('route', `/account/${this.props.accounts[0].code}`); // eslint-disable-line no-console diff --git a/frontends/web/src/routes/device/device.jsx b/frontends/web/src/routes/device/device.jsx index 05b1c01642..c44be3202a 100644 --- a/frontends/web/src/routes/device/device.jsx +++ b/frontends/web/src/routes/device/device.jsx @@ -15,11 +15,9 @@ */ import { Component, h } from 'preact'; -import { route } from 'preact-router'; import { translate } from 'react-i18next'; import { apiGet } from '../../utils/request'; import { apiWebsocket } from '../../utils/websocket'; -import Waiting from './waiting'; import Unlock from './unlock'; import Bootloader from './upgrade/bootloader'; import RequireUpgrade from './upgrade/require_upgrade'; @@ -54,7 +52,6 @@ export default class Device extends Component { firmwareVersion: null, deviceRegistered: false, deviceStatus: null, - accountsStatus: null, goal: null, success: null, } @@ -62,11 +59,7 @@ export default class Device extends Component { componentDidMount() { this.onDevicesRegisteredChanged(); this.onDeviceStatusChanged(); - this.onAccountsStatusChanged(); this.unsubscribe = apiWebsocket(({ type, data, deviceID }) => { - if (type === 'backend' && data === 'accountsStatusChanged') { - this.onAccountsStatusChanged(); - } if (type === 'devices' && data === 'registeredChanged') { this.onDevicesRegisteredChanged(); } @@ -88,26 +81,11 @@ export default class Device extends Component { } } - onAccountsStatusChanged = () => { - apiGet('accounts-status').then(status => { - if (status === 'initialized' && this.props.default) { - console.log('device.jsx route to /account'); // eslint-disable-line no-console - route('/account', true); - } - this.setState({ - accountsStatus: status === 'initialized' - }); - }); - } - onDevicesRegisteredChanged = () => { - apiGet('devices/registered').then(deviceIDs => { + apiGet('devices/registered').then(devices => { + const deviceIDs = Object.keys(devices); const deviceRegistered = deviceIDs.includes(this.getDeviceID()); - if (this.props.default && deviceIDs.length === 1) { - console.log('device.jsx route to', '/device/' + deviceIDs[0]); // eslint-disable-line no-console - route('/device/' + deviceIDs[0], true); - } this.setState({ deviceRegistered, deviceStatus: null @@ -128,7 +106,7 @@ export default class Device extends Component { } getDeviceID() { - return this.props.deviceID || this.props.deviceIDs[0] || null; + return this.props.deviceID || null; } handleCreate = () => { @@ -150,17 +128,12 @@ export default class Device extends Component { render({ t, deviceID, - deviceIDs, }, { deviceRegistered, deviceStatus, - accountsStatus, goal, success, }) { - if (!deviceIDs.length && !accountsStatus) { - return ; - } if (!deviceRegistered || !deviceStatus) { return null; } diff --git a/frontends/web/src/routes/device/deviceswitch.tsx b/frontends/web/src/routes/device/deviceswitch.tsx new file mode 100644 index 0000000000..42866c16dd --- /dev/null +++ b/frontends/web/src/routes/device/deviceswitch.tsx @@ -0,0 +1,53 @@ +/** + * Copyright 2018 Shift Devices AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, h, RenderableProps } from 'preact'; +import { route } from 'preact-router'; +import Device from './device'; +import Waiting from './waiting'; + +interface Props { + devices: { + [deviceID: string]: string, + }; + deviceID: string | null; +} + +class DeviceSwitch extends Component { + public componentDidMount() { + const deviceIDs = Object.keys(this.props.devices); + if (this.props.deviceID !== null && !deviceIDs.includes(this.props.deviceID)) { + route('/', true); + } + if (this.props.deviceID === null && deviceIDs.length > 0) { + route(`/device/${deviceIDs[0]}`, true); + } + } + + public render({ deviceID, devices }: RenderableProps) { + if (this.props.default || deviceID === null || !Object.keys(devices).includes(deviceID)) { + return ; + } + switch (devices[deviceID]) { + case 'bitbox': + return ; + default: + return ; + } + } +} + +export { DeviceSwitch }; diff --git a/frontends/web/src/routes/device/waiting.jsx b/frontends/web/src/routes/device/waiting.jsx index 8142c727dd..5161edc276 100644 --- a/frontends/web/src/routes/device/waiting.jsx +++ b/frontends/web/src/routes/device/waiting.jsx @@ -40,7 +40,6 @@ export default class Waiting extends Component { } } - render({ t, }, {