diff --git a/CHANGELOG.md b/CHANGELOG.md index 180aac0b..594f72c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/ble/reducers.test.ts b/src/ble/reducers.test.ts index 8427caff..06a0662a 100644 --- a/src/ble/reducers.test.ts +++ b/src/ble/reducers.test.ts @@ -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(); +}); diff --git a/src/ble/reducers.ts b/src/ble/reducers.ts index 449fe567..c14e089b 100644 --- a/src/ble/reducers.ts +++ b/src/ble/reducers.ts @@ -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 = (state = '', action) => { } }; +const deviceLowBatteryWarning: Reducer = (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 = (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, }); diff --git a/src/status-bar/StatusBar.tsx b/src/status-bar/StatusBar.tsx index 08a4830a..017133c1 100644 --- a/src/status-bar/StatusBar.tsx +++ b/src/status-bar/StatusBar.tsx @@ -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 = { + 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 ( @@ -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 ( + + {i18n.translate( + lowBatteryWarning ? MessageId.BatteryLow : MessageId.BatteryOk, + )} + + } + > +
+
+ +
+
+
+ + ); +}; + 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 && } + {connection === BleConnectionState.Connected && ( + <> + + + + )}
); }; diff --git a/src/status-bar/i18n.en.json b/src/status-bar/i18n.en.json index 1222b2ff..61d9161a 100644 --- a/src/status-bar/i18n.en.json +++ b/src/status-bar/i18n.en.json @@ -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." } } diff --git a/src/status-bar/i18n.ts b/src/status-bar/i18n.ts index 5e128865..48076c76 100644 --- a/src/status-bar/i18n.ts +++ b/src/status-bar/i18n.ts @@ -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', diff --git a/src/status-bar/status-bar.scss b/src/status-bar/status-bar.scss index 52be658d..e823749a 100644 --- a/src/status-bar/status-bar.scss +++ b/src/status-bar/status-bar.scss @@ -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; +}