mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
status-bar: add connected hub indicator
This adds an indicator to the status bar that shows the hub name. When clicked, a popover will be displayed that lists more detailed information about the connected hub.
This commit is contained in:
@@ -4,6 +4,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Status bar indicator for connected hub.
|
||||
|
||||
### Changed
|
||||
- Saving file now uses proper save dialog in Chromium browser ([support#84]).
|
||||
- Toolbar buttons now scale with screen size ([support#300]).
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
// Refer to Device Information Service (DIS) at https://www.bluetooth.com/specifications/specs/
|
||||
// and assigned numbers at https://www.bluetooth.com/specifications/assigned-numbers/
|
||||
|
||||
import {
|
||||
HubType,
|
||||
LegoCompanyId,
|
||||
TechnicLargeHubVariant,
|
||||
TechnicSmallHubVariant,
|
||||
} from '../ble-lwp3-service/protocol';
|
||||
|
||||
/** Device Information service UUID. */
|
||||
export const serviceUUID = 0x180a;
|
||||
|
||||
@@ -54,3 +61,42 @@ export function decodePnpId(data: DataView): PnpId {
|
||||
productVersion: data.getUint16(5, true),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns hub type as a string suitable for display to users.
|
||||
* @param pnpId The PnP ID
|
||||
*/
|
||||
export function getHubTypeName(pnpId: PnpId): string {
|
||||
if (pnpId.vendorIdSource !== PnpIdVendorIdSource.BluetoothSig) {
|
||||
return 'USB';
|
||||
}
|
||||
|
||||
if (pnpId.vendorId !== LegoCompanyId) {
|
||||
return 'non-LEGO';
|
||||
}
|
||||
|
||||
switch (pnpId.productId) {
|
||||
case HubType.MoveHub:
|
||||
return 'Move hub';
|
||||
case HubType.CityHub:
|
||||
return 'City hub';
|
||||
case HubType.TechnicHub:
|
||||
return 'Technic hub';
|
||||
case HubType.TechnicLargeHub:
|
||||
switch (pnpId.productVersion) {
|
||||
case TechnicLargeHubVariant.SpikePrimeHub:
|
||||
return 'Prime hub';
|
||||
case TechnicLargeHubVariant.MindstormsInventorHub:
|
||||
return 'Inventor hub';
|
||||
}
|
||||
break;
|
||||
case HubType.TechnicSmallHub:
|
||||
switch (pnpId.productVersion) {
|
||||
case TechnicSmallHubVariant.SpikeEssentialHub:
|
||||
return 'Essential hub';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return 'Unsupported';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { Action } from '../actions';
|
||||
import {
|
||||
bleDIServiceDidReceiveFirmwareRevision,
|
||||
bleDIServiceDidReceivePnPId,
|
||||
} from '../ble-device-info-service/actions';
|
||||
import { PnpIdVendorIdSource } from '../ble-device-info-service/protocol';
|
||||
import { HubType, LegoCompanyId } from '../ble-lwp3-service/protocol';
|
||||
import {
|
||||
BleDeviceDidFailToConnectReason,
|
||||
connect,
|
||||
@@ -19,6 +25,9 @@ test('initial state', () => {
|
||||
expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connection": "ble.connection.state.disconnected",
|
||||
"deviceFirmwareVersion": "",
|
||||
"deviceName": "",
|
||||
"deviceType": "",
|
||||
}
|
||||
`);
|
||||
});
|
||||
@@ -57,3 +66,50 @@ test('connection', () => {
|
||||
).connection,
|
||||
).toBe(BleConnectionState.Connected);
|
||||
});
|
||||
|
||||
test('deviceName', () => {
|
||||
const testId = 'test-id';
|
||||
const testName = 'Test Name';
|
||||
|
||||
expect(
|
||||
reducers({ deviceName: '' } as State, didConnect(testId, testName)).deviceName,
|
||||
).toBe(testName);
|
||||
|
||||
expect(
|
||||
reducers({ deviceName: testName } as State, didDisconnect()).deviceName,
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
test('deviceType', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ deviceType: '' } as State,
|
||||
bleDIServiceDidReceivePnPId({
|
||||
vendorIdSource: PnpIdVendorIdSource.BluetoothSig,
|
||||
vendorId: LegoCompanyId,
|
||||
productId: HubType.MoveHub,
|
||||
productVersion: 0,
|
||||
}),
|
||||
).deviceType,
|
||||
).toBe('Move hub');
|
||||
|
||||
expect(
|
||||
reducers({ deviceType: 'Move hub' } as State, didDisconnect()).deviceType,
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
test('deviceFirmwareVersion', () => {
|
||||
const testVersion = '3.0.0';
|
||||
|
||||
expect(
|
||||
reducers(
|
||||
{ deviceFirmwareVersion: '' } as State,
|
||||
bleDIServiceDidReceiveFirmwareRevision(testVersion),
|
||||
).deviceFirmwareVersion,
|
||||
).toBe(testVersion);
|
||||
|
||||
expect(
|
||||
reducers({ deviceFirmwareVersion: testVersion } as State, didDisconnect())
|
||||
.deviceFirmwareVersion,
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
+42
-2
@@ -1,11 +1,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
//
|
||||
// Manages state for the Bluetooth Low Energy connection.
|
||||
// This assumes that there is only one global connection to a single device.
|
||||
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { BleDIServiceActionType } from '../ble-device-info-service/actions';
|
||||
import { getHubTypeName } from '../ble-device-info-service/protocol';
|
||||
import { BleDeviceActionType } from './actions';
|
||||
|
||||
/**
|
||||
@@ -50,4 +52,42 @@ const connection: Reducer<BleConnectionState, Action> = (
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({ connection });
|
||||
const deviceName: Reducer<string, Action> = (state = '', action) => {
|
||||
switch (action.type) {
|
||||
case BleDeviceActionType.DidDisconnect:
|
||||
return '';
|
||||
case BleDeviceActionType.DidConnect:
|
||||
return action.name;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const deviceType: Reducer<string, Action> = (state = '', action) => {
|
||||
switch (action.type) {
|
||||
case BleDeviceActionType.DidDisconnect:
|
||||
return '';
|
||||
case BleDIServiceActionType.DidReceivePnPId:
|
||||
return getHubTypeName(action.pnpId);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const deviceFirmwareVersion: Reducer<string, Action> = (state = '', action) => {
|
||||
switch (action.type) {
|
||||
case BleDeviceActionType.DidDisconnect:
|
||||
return '';
|
||||
case BleDIServiceActionType.DidReceiveFirmwareRevision:
|
||||
return action.version;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
connection,
|
||||
deviceName,
|
||||
deviceType,
|
||||
deviceFirmwareVersion,
|
||||
});
|
||||
|
||||
@@ -30,6 +30,10 @@ body {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// global style tweaks
|
||||
|
||||
.#{$ns}-toast {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
import { BleConnectionState } from '../ble/reducers';
|
||||
import StatusBar from './StatusBar';
|
||||
|
||||
it('should prevent browser context menu', () => {
|
||||
const store = {
|
||||
getState: jest.fn(),
|
||||
getState: jest.fn(() => ({
|
||||
ble: { connection: BleConnectionState.Disconnected, deviceName: '' },
|
||||
})),
|
||||
dispatch: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
} as unknown as Store;
|
||||
|
||||
@@ -1,18 +1,82 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { Classes as Classes2, Popover2 } from '@blueprintjs/popover2';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { BleConnectionState } from '../ble/reducers';
|
||||
import { RootState } from '../reducers';
|
||||
import { MessageId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
import './status-bar.scss';
|
||||
|
||||
const HubInfoButton: React.VFC = (_props) => {
|
||||
const deviceName = useSelector((state: RootState) => state.ble.deviceName);
|
||||
const deviceType = useSelector((state: RootState) => state.ble.deviceType);
|
||||
const deviceFirmwareVersion = useSelector(
|
||||
(state: RootState) => state.ble.deviceFirmwareVersion,
|
||||
);
|
||||
|
||||
const [i18n] = useI18n({ id: 'statusBar', translations: { en }, fallback: en });
|
||||
|
||||
return (
|
||||
<Popover2
|
||||
popoverClassName={Classes2.POPOVER2_CONTENT_SIZING}
|
||||
rootBoundary="document"
|
||||
placement="top"
|
||||
content={
|
||||
<table className="no-wrap">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
{i18n.translate(MessageId.HubInfoConnectedTo)}
|
||||
</strong>
|
||||
</td>
|
||||
<td>{deviceName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
{i18n.translate(MessageId.HubInfoHubType)}
|
||||
</strong>
|
||||
</td>
|
||||
<td>{deviceType}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
{i18n.translate(MessageId.HubInfoFirmware)}
|
||||
</strong>
|
||||
</td>
|
||||
<td>v{deviceFirmwareVersion}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
>
|
||||
<Button minimal={true} onMouseDown={(e) => e.preventDefault()}>
|
||||
{deviceName}
|
||||
</Button>
|
||||
</Popover2>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBar: React.VFC = (_props) => {
|
||||
const connection = useSelector((state: RootState) => state.ble.connection);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pb-status-bar"
|
||||
role="status"
|
||||
aria-live="off"
|
||||
onContextMenu={(e): void => e.preventDefault()}
|
||||
></div>
|
||||
>
|
||||
{connection === BleConnectionState.Connected && <HubInfoButton />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"hubInfo": {
|
||||
"connectedTo": "Connected to:",
|
||||
"hubType": "Hub type:",
|
||||
"firmware": "Firmware:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { lookup } from '../../test';
|
||||
import { MessageId } from './i18n';
|
||||
import en from './i18n.en.json';
|
||||
|
||||
describe('Ensure .json file has matches for MessageIds', () => {
|
||||
test.each(Object.values(MessageId))('%s', (id) => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
//
|
||||
// Status bar translation keys.
|
||||
|
||||
export enum MessageId {
|
||||
HubInfoConnectedTo = 'hubInfo.connectedTo',
|
||||
HubInfoHubType = 'hubInfo.hubType',
|
||||
HubInfoFirmware = 'hubInfo.firmware',
|
||||
}
|
||||
@@ -14,3 +14,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// make status bar items right-aligned
|
||||
.pb-status-bar :first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ $pt-font-size-large: $pt-grid-size * 1.8;
|
||||
$pt-font-size-small: $pt-grid-size * 1.4;
|
||||
|
||||
$pb-toolbar-height: 72px;
|
||||
$pb-status-bar-height: 24px;
|
||||
$pb-status-bar-height: 30px;
|
||||
|
||||
$pb-pybricks-blue: #0088ce;
|
||||
$pt-app-background-color: #e8e8e8;
|
||||
|
||||
Reference in New Issue
Block a user