status-bar: add basic battery indicator

This adds a basic battery indicator that just shows green for OK and red
for low battery warning. This is all we can do for now since that is
the only information that the hub sends currently.

Fixes: https://github.com/pybricks/support/issues/559
This commit is contained in:
David Lechner
2021-12-27 17:05:40 -06:00
parent 4c4061561d
commit 42051ba924
7 changed files with 153 additions and 6 deletions
+2
View File
@@ -6,6 +6,7 @@
### Added
- Status bar indicator for connected hub.
- Basic battery OK/low indicator ([support#559]).
### Changed
- Saving file now uses proper save dialog in Chromium browser ([support#84]).
@@ -17,6 +18,7 @@
[support#84]: https://github.com/pybricks/support/issues/84
[support#300]: https://github.com/pybricks/support/issues/300
[support#369]: https://github.com/pybricks/support/issues/369
[support#559]: https://github.com/pybricks/support/issues/559
## [1.1.0] - 2021-12-16
+32
View File
@@ -8,6 +8,8 @@ import {
} from '../ble-device-info-service/actions';
import { PnpIdVendorIdSource } from '../ble-device-info-service/protocol';
import { HubType, LegoCompanyId } from '../ble-lwp3-service/protocol';
import { didReceiveStatusReport } from '../ble-pybricks-service/actions';
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
import {
BleDeviceDidFailToConnectReason,
connect,
@@ -25,7 +27,9 @@ test('initial state', () => {
expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
Object {
"connection": "ble.connection.state.disconnected",
"deviceBatteryCharging": false,
"deviceFirmwareVersion": "",
"deviceLowBatteryWarning": false,
"deviceName": "",
"deviceType": "",
}
@@ -113,3 +117,31 @@ test('deviceFirmwareVersion', () => {
.deviceFirmwareVersion,
).toBe('');
});
test('deviceLowBatteryWarning', () => {
expect(
reducers(
{ deviceLowBatteryWarning: false } as State,
didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning)),
).deviceLowBatteryWarning,
).toBeTruthy();
expect(
reducers(
{ deviceLowBatteryWarning: true } as State,
didReceiveStatusReport(~statusToFlag(Status.BatteryLowVoltageWarning)),
).deviceLowBatteryWarning,
).toBeFalsy();
expect(
reducers({ deviceLowBatteryWarning: true } as State, didDisconnect())
.deviceLowBatteryWarning,
).toBeFalsy();
});
test('deviceBatteryCharging', () => {
expect(
reducers({ deviceBatteryCharging: true } as State, didDisconnect())
.deviceBatteryCharging,
).toBeFalsy();
});
+27
View File
@@ -8,6 +8,8 @@ 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 { BlePybricksServiceEventActionType } from '../ble-pybricks-service/actions';
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
import { BleDeviceActionType } from './actions';
/**
@@ -85,9 +87,34 @@ const deviceFirmwareVersion: Reducer<string, Action> = (state = '', action) => {
}
};
const deviceLowBatteryWarning: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case BleDeviceActionType.DidDisconnect:
return false;
case BlePybricksServiceEventActionType.DidReceiveStatusReport:
return Boolean(
action.statusFlags & statusToFlag(Status.BatteryLowVoltageWarning),
);
default:
return state;
}
};
const deviceBatteryCharging: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case BleDeviceActionType.DidDisconnect:
return false;
// TODO: hub does not currently have a status flag for this
default:
return state;
}
};
export default combineReducers({
connection,
deviceName,
deviceType,
deviceFirmwareVersion,
deviceLowBatteryWarning,
deviceBatteryCharging,
});
+47 -6
View File
@@ -1,8 +1,8 @@
// 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 { Button, Intent, ProgressBar } from '@blueprintjs/core';
import { Classes as Classes2, Popover2, Popover2Props } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useSelector } from 'react-redux';
@@ -13,6 +13,11 @@ import en from './i18n.en.json';
import './status-bar.scss';
const commonPopoverProps: Partial<Popover2Props> = {
popoverClassName: Classes2.POPOVER2_CONTENT_SIZING,
placement: 'top',
};
const HubInfoButton: React.VFC = (_props) => {
const deviceName = useSelector((state: RootState) => state.ble.deviceName);
const deviceType = useSelector((state: RootState) => state.ble.deviceType);
@@ -24,9 +29,7 @@ const HubInfoButton: React.VFC = (_props) => {
return (
<Popover2
popoverClassName={Classes2.POPOVER2_CONTENT_SIZING}
rootBoundary="document"
placement="top"
{...commonPopoverProps}
content={
<table className="no-wrap">
<tbody>
@@ -65,6 +68,39 @@ const HubInfoButton: React.VFC = (_props) => {
);
};
const BatteryIndicator: React.VFC = (_props) => {
const charging = useSelector((state: RootState) => state.ble.deviceBatteryCharging);
const lowBatteryWarning = useSelector(
(state: RootState) => state.ble.deviceLowBatteryWarning,
);
const [i18n] = useI18n({ id: 'statusBar', translations: { en }, fallback: en });
return (
<Popover2
{...commonPopoverProps}
content={
<span className="no-wrap">
{i18n.translate(
lowBatteryWarning ? MessageId.BatteryLow : MessageId.BatteryOk,
)}
</span>
}
>
<div className="pb-battery-indicator" style={{ cursor: 'pointer' }}>
<div className="pb-battery-indicator-body">
<ProgressBar
animate={charging}
stripes={charging}
intent={lowBatteryWarning ? Intent.DANGER : Intent.SUCCESS}
/>
</div>
<div className="pb-battery-indicator-tip" />
</div>
</Popover2>
);
};
const StatusBar: React.VFC = (_props) => {
const connection = useSelector((state: RootState) => state.ble.connection);
@@ -75,7 +111,12 @@ const StatusBar: React.VFC = (_props) => {
aria-live="off"
onContextMenu={(e): void => e.preventDefault()}
>
{connection === BleConnectionState.Connected && <HubInfoButton />}
{connection === BleConnectionState.Connected && (
<>
<HubInfoButton />
<BatteryIndicator />
</>
)}
</div>
);
};
+4
View File
@@ -3,5 +3,9 @@
"connectedTo": "Connected to:",
"hubType": "Hub type:",
"firmware": "Firmware:"
},
"battery": {
"low": "Battery is low. Hub will turn off soon.",
"ok": "Battery level is OK."
}
}
+2
View File
@@ -4,6 +4,8 @@
// Status bar translation keys.
export enum MessageId {
BatteryLow = 'battery.low',
BatteryOk = 'battery.ok',
HubInfoConnectedTo = 'hubInfo.connectedTo',
HubInfoHubType = 'hubInfo.hubType',
HubInfoFirmware = 'hubInfo.firmware',
+39
View File
@@ -11,6 +11,7 @@
background-color: $pb-pybricks-blue;
height: $pb-status-bar-height;
width: 100vw;
padding-inline: 5px;
display: flex;
align-items: center;
}
@@ -19,3 +20,41 @@
.pb-status-bar :first-child {
margin-left: auto;
}
.pb-battery-indicator {
display: flex;
flex-direction: row;
align-items: center;
margin: 5px;
}
.pb-battery-indicator-body {
border-radius: 3px;
border: 2px solid $pt-text-color;
background-color: $pt-text-color;
width: 30px;
}
.pb-battery-indicator-body .#{$ns}-progress-bar {
height: 12px;
}
.pb-battery-indicator-body .#{$ns}-progress-bar,
.pb-battery-indicator-body .#{$ns}-progress-meter {
border-radius: 3px;
}
.pb-battery-indicator-tip {
border-top-right-radius: 1px;
border-bottom-right-radius: 1px;
border: 1px solid $pt-text-color;
background-color: $pt-text-color;
width: 3px;
height: 8px;
}
.#{$ns}-dark .pb-battery-indicator-body,
.#{$ns}-dark .pb-battery-indicator-tip {
border-color: $pt-dark-text-color;
background-color: $pt-dark-text-color;
}