diff --git a/src/ble/reducers.test.ts b/src/ble/reducers.test.ts index deba2a9a..b9e89ff1 100644 --- a/src/ble/reducers.test.ts +++ b/src/ble/reducers.test.ts @@ -2,14 +2,6 @@ // Copyright (c) 2021-2025 The Pybricks Authors import { AnyAction } from 'redux'; -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 { didReceiveStatusReport } from '../ble-pybricks-service/actions'; -import { Status, statusToFlag } from '../ble-pybricks-service/protocol'; import { bleConnectPybricks, bleDidConnectPybricks, @@ -26,11 +18,6 @@ test('initial state', () => { expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(` { "connection": "ble.connection.state.disconnected", - "deviceBatteryCharging": false, - "deviceFirmwareVersion": "", - "deviceLowBatteryWarning": false, - "deviceName": "", - "deviceType": "", } `); }); @@ -73,87 +60,3 @@ test('connection', () => { ).connection, ).toBe(BleConnectionState.Connected); }); - -test('deviceName', () => { - const testId = 'test-id'; - const testName = 'Test Name'; - - expect( - reducers({ deviceName: '' } as State, bleDidConnectPybricks(testId, testName)) - .deviceName, - ).toBe(testName); - - expect( - reducers({ deviceName: testName } as State, bleDidDisconnectPybricks()) - .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, bleDidDisconnectPybricks()) - .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, - bleDidDisconnectPybricks(), - ).deviceFirmwareVersion, - ).toBe(''); -}); - -test('deviceLowBatteryWarning', () => { - expect( - reducers( - { deviceLowBatteryWarning: false } as State, - didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning), 0, 0), - ).deviceLowBatteryWarning, - ).toBeTruthy(); - - expect( - reducers( - { deviceLowBatteryWarning: true } as State, - didReceiveStatusReport( - ~statusToFlag(Status.BatteryLowVoltageWarning), - 0, - 0, - ), - ).deviceLowBatteryWarning, - ).toBeFalsy(); - - expect( - reducers({ deviceLowBatteryWarning: true } as State, bleDidDisconnectPybricks()) - .deviceLowBatteryWarning, - ).toBeFalsy(); -}); - -test('deviceBatteryCharging', () => { - expect( - reducers({ deviceBatteryCharging: true } as State, bleDidDisconnectPybricks()) - .deviceBatteryCharging, - ).toBeFalsy(); -}); diff --git a/src/ble/reducers.ts b/src/ble/reducers.ts index 808a42fa..753b3180 100644 --- a/src/ble/reducers.ts +++ b/src/ble/reducers.ts @@ -1,17 +1,10 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2020-2025 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 { - bleDIServiceDidReceiveFirmwareRevision, - bleDIServiceDidReceivePnPId, -} from '../ble-device-info-service/actions'; -import { getHubTypeName } from '../ble-device-info-service/protocol'; -import { didReceiveStatusReport } from '../ble-pybricks-service/actions'; -import { Status, statusToFlag } from '../ble-pybricks-service/protocol'; import { bleConnectPybricks, bleDidConnectPybricks, @@ -72,71 +65,6 @@ const connection: Reducer = ( return state; }; -const deviceName: Reducer = (state = '', action) => { - if (bleDidDisconnectPybricks.matches(action)) { - return ''; - } - - if (bleDidConnectPybricks.matches(action)) { - return action.name; - } - - return state; -}; - -const deviceType: Reducer = (state = '', action) => { - if (bleDidDisconnectPybricks.matches(action)) { - return ''; - } - - if (bleDIServiceDidReceivePnPId.matches(action)) { - return getHubTypeName(action.pnpId); - } - - return state; -}; - -const deviceFirmwareVersion: Reducer = (state = '', action) => { - if (bleDidDisconnectPybricks.matches(action)) { - return ''; - } - - if (bleDIServiceDidReceiveFirmwareRevision.matches(action)) { - return action.version; - } - - return state; -}; - -const deviceLowBatteryWarning: Reducer = (state = false, action) => { - if (bleDidDisconnectPybricks.matches(action)) { - return false; - } - - if (didReceiveStatusReport.matches(action)) { - return Boolean( - action.statusFlags & statusToFlag(Status.BatteryLowVoltageWarning), - ); - } - - return state; -}; - -const deviceBatteryCharging: Reducer = (state = false, action) => { - if (bleDidDisconnectPybricks.matches(action)) { - return false; - } - - // TODO: hub does not currently have a status flag for this - - return state; -}; - export default combineReducers({ connection, - deviceName, - deviceType, - deviceFirmwareVersion, - deviceLowBatteryWarning, - deviceBatteryCharging, }); diff --git a/src/hub/reducers.test.ts b/src/hub/reducers.test.ts index 7e7bbf83..87b081a2 100644 --- a/src/hub/reducers.test.ts +++ b/src/hub/reducers.test.ts @@ -7,9 +7,13 @@ import { bleDidDisconnectPybricks, bleDisconnectPybricks, } from '../ble/actions'; -import { bleDIServiceDidReceiveSoftwareRevision } from '../ble-device-info-service/actions'; -import { PnpId } from '../ble-device-info-service/protocol'; -import { HubType } from '../ble-lwp3-service/protocol'; +import { + bleDIServiceDidReceiveFirmwareRevision, + bleDIServiceDidReceivePnPId, + bleDIServiceDidReceiveSoftwareRevision, +} from '../ble-device-info-service/actions'; +import { PnpId, PnpIdVendorIdSource } from '../ble-device-info-service/protocol'; +import { HubType, LegoCompanyId } from '../ble-lwp3-service/protocol'; import { blePybricksServiceDidNotReceiveHubCapabilities, blePybricksServiceDidReceiveHubCapabilities, @@ -40,6 +44,11 @@ type State = ReturnType; test('initial state', () => { expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(` { + "deviceBatteryCharging": false, + "deviceFirmwareVersion": "", + "deviceLowBatteryWarning": false, + "deviceName": "", + "deviceType": "", "downloadProgress": null, "hasRepl": false, "maxBleWriteSize": 0, @@ -298,6 +307,90 @@ describe('runtime', () => { }); }); +test('deviceName', () => { + const testId = 'test-id'; + const testName = 'Test Name'; + + expect( + reducers({ deviceName: '' } as State, bleDidConnectPybricks(testId, testName)) + .deviceName, + ).toBe(testName); + + expect( + reducers({ deviceName: testName } as State, bleDidDisconnectPybricks()) + .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, bleDidDisconnectPybricks()) + .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, + bleDidDisconnectPybricks(), + ).deviceFirmwareVersion, + ).toBe(''); +}); + +test('deviceLowBatteryWarning', () => { + expect( + reducers( + { deviceLowBatteryWarning: false } as State, + didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning), 0, 0), + ).deviceLowBatteryWarning, + ).toBeTruthy(); + + expect( + reducers( + { deviceLowBatteryWarning: true } as State, + didReceiveStatusReport( + ~statusToFlag(Status.BatteryLowVoltageWarning), + 0, + 0, + ), + ).deviceLowBatteryWarning, + ).toBeFalsy(); + + expect( + reducers({ deviceLowBatteryWarning: true } as State, bleDidDisconnectPybricks()) + .deviceLowBatteryWarning, + ).toBeFalsy(); +}); + +test('deviceBatteryCharging', () => { + expect( + reducers({ deviceBatteryCharging: true } as State, bleDidDisconnectPybricks()) + .deviceBatteryCharging, + ).toBeFalsy(); +}); + describe('maxBleWriteSize', () => { test.each([100, 1000])('Pybricks Profile >= v1.2.0: %s', (size) => { expect( diff --git a/src/hub/reducers.ts b/src/hub/reducers.ts index 73824d23..eba4109e 100644 --- a/src/hub/reducers.ts +++ b/src/hub/reducers.ts @@ -8,7 +8,12 @@ import { bleDidDisconnectPybricks, bleDisconnectPybricks, } from '../ble/actions'; -import { bleDIServiceDidReceiveSoftwareRevision } from '../ble-device-info-service/actions'; +import { + bleDIServiceDidReceiveFirmwareRevision, + bleDIServiceDidReceivePnPId, + bleDIServiceDidReceiveSoftwareRevision, +} from '../ble-device-info-service/actions'; +import { getHubTypeName } from '../ble-device-info-service/protocol'; import { HubType } from '../ble-lwp3-service/protocol'; import { blePybricksServiceDidNotReceiveHubCapabilities, @@ -143,6 +148,66 @@ const runtime: Reducer = ( return state; }; +const deviceName: Reducer = (state = '', action) => { + if (bleDidDisconnectPybricks.matches(action)) { + return ''; + } + + if (bleDidConnectPybricks.matches(action)) { + return action.name; + } + + return state; +}; + +const deviceType: Reducer = (state = '', action) => { + if (bleDidDisconnectPybricks.matches(action)) { + return ''; + } + + if (bleDIServiceDidReceivePnPId.matches(action)) { + return getHubTypeName(action.pnpId); + } + + return state; +}; + +const deviceFirmwareVersion: Reducer = (state = '', action) => { + if (bleDidDisconnectPybricks.matches(action)) { + return ''; + } + + if (bleDIServiceDidReceiveFirmwareRevision.matches(action)) { + return action.version; + } + + return state; +}; + +const deviceLowBatteryWarning: Reducer = (state = false, action) => { + if (bleDidDisconnectPybricks.matches(action)) { + return false; + } + + if (didReceiveStatusReport.matches(action)) { + return Boolean( + action.statusFlags & statusToFlag(Status.BatteryLowVoltageWarning), + ); + } + + return state; +}; + +const deviceBatteryCharging: Reducer = (state = false, action) => { + if (bleDidDisconnectPybricks.matches(action)) { + return false; + } + + // TODO: hub does not currently have a status flag for this + + return state; +}; + const downloadProgress: Reducer = (state = null, action) => { if (didStartDownload.matches(action)) { return 0; @@ -299,6 +364,11 @@ const selectedSlot: Reducer = (state = 0, action) => { export default combineReducers({ runtime, + deviceName, + deviceType, + deviceFirmwareVersion, + deviceLowBatteryWarning, + deviceBatteryCharging, downloadProgress, maxBleWriteSize, maxUserProgramSize, diff --git a/src/status-bar/StatusBar.test.tsx b/src/status-bar/StatusBar.test.tsx index f5ae875d..fee0bcc6 100644 --- a/src/status-bar/StatusBar.test.tsx +++ b/src/status-bar/StatusBar.test.tsx @@ -4,7 +4,6 @@ import { act, waitFor } from '@testing-library/react'; import React from 'react'; import { testRender } from '../../test'; -import { BleConnectionState } from '../ble/reducers'; import { HubRuntimeState } from '../hub/reducers'; import StatusBar from './StatusBar'; @@ -12,15 +11,14 @@ it('should show popover when hub name is clicked', async () => { const testHubName = 'Test hub'; const [user, statusBar] = testRender(, { - ble: { - connection: BleConnectionState.Connected, + hub: { + runtime: HubRuntimeState.Idle, deviceName: testHubName, deviceType: 'hub type', deviceFirmwareVersion: 'v0.0.0', deviceLowBatteryWarning: false, deviceBatteryCharging: false, }, - hub: { runtime: HubRuntimeState.Idle }, }); await act(() => user.click(statusBar.getByText(testHubName))); @@ -32,15 +30,14 @@ it('should show popover when battery is clicked', async () => { const testHubName = 'Test hub'; const [user, statusBar] = testRender(, { - ble: { - connection: BleConnectionState.Connected, + hub: { + runtime: HubRuntimeState.Idle, deviceName: testHubName, deviceType: 'hub type', deviceFirmwareVersion: 'v0.0.0', deviceLowBatteryWarning: false, deviceBatteryCharging: false, }, - hub: { runtime: HubRuntimeState.Idle }, }); await act(() => user.click(statusBar.getByTitle('Battery'))); diff --git a/src/status-bar/StatusBar.tsx b/src/status-bar/StatusBar.tsx index 1756eb7c..c14c8885 100644 --- a/src/status-bar/StatusBar.tsx +++ b/src/status-bar/StatusBar.tsx @@ -72,9 +72,9 @@ const CompletionEngineIndicator: React.FunctionComponent = () => { const HubInfoButton: React.FunctionComponent = () => { const i18n = useI18n(); - const deviceName = useSelector((s) => s.ble.deviceName); - const deviceType = useSelector((s) => s.ble.deviceType); - const deviceFirmwareVersion = useSelector((s) => s.ble.deviceFirmwareVersion); + const deviceName = useSelector((s) => s.hub.deviceName); + const deviceType = useSelector((s) => s.hub.deviceType); + const deviceFirmwareVersion = useSelector((s) => s.hub.deviceFirmwareVersion); return ( { const BatteryIndicator: React.FunctionComponent = () => { const i18n = useI18n(); - const charging = useSelector((s) => s.ble.deviceBatteryCharging); - const lowBatteryWarning = useSelector((s) => s.ble.deviceLowBatteryWarning); + const charging = useSelector((s) => s.hub.deviceBatteryCharging); + const lowBatteryWarning = useSelector((s) => s.hub.deviceLowBatteryWarning); return (