Skip to content

Commit

Permalink
frontend: improve routing and introduce device switch
Browse files Browse the repository at this point in the history
  • Loading branch information
benma committed Oct 27, 2018
1 parent e9c18f2 commit c8f9176
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 99 deletions.
16 changes: 4 additions & 12 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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() {
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion backend/devices/device/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 49 additions & 33 deletions backend/devices/usb/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package usb

import (
"encoding/hex"
"fmt"
"os"
"regexp"
"time"
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
}
}
Expand All @@ -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.
Expand All @@ -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()
}
8 changes: 6 additions & 2 deletions backend/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
36 changes: 16 additions & 20 deletions frontends/web/src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
}

Expand Down Expand Up @@ -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 }));
});
}
Expand All @@ -121,6 +115,7 @@ export class App extends Component {

render({}, {
accounts,
devices,
deviceIDs,
accountsInitialized,
activeSidebar,
Expand All @@ -143,8 +138,7 @@ export class App extends Component {
accounts={accounts} />
<Receive
path="/account/:code/receive"
deviceIDs={deviceIDs}
accounts={accounts} />
deviceIDs={deviceIDs} />
<Info
path="/account/:code/info"
accounts={accounts} />
Expand All @@ -162,13 +156,15 @@ export class App extends Component {
// showCreate={true} // Does not exist!
// deviceIDs={deviceIDs} // Does not exist!
/>
<Device
<DeviceSwitch
path="/device/:deviceID"
deviceIDs={deviceIDs} />
<Device
key={devices}
devices={devices} />
<DeviceSwitch
default
deviceID={deviceIDs[0]}
deviceIDs={deviceIDs} />
key={devices}
deviceID={null}
devices={devices} />
</Container>
</div>
<Alert />
Expand Down
4 changes: 4 additions & 0 deletions frontends/web/src/routes/account/account.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c8f9176

Please sign in to comment.