Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/alerts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
// Copyright (c) 2022-2025 The Pybricks Authors

import { ToastProps } from '@blueprintjs/core';
import alerts from './alerts/alerts';
Expand All @@ -11,6 +11,7 @@ import hub from './hub/alerts';
import mpy from './mpy/alerts';
import sponsor from './sponsor/alerts';
import type { CreateToast } from './toasterTypes';
import usb from './usb/alerts';

/** This collects alerts from all of the subsystems of the app */
const alertDomains = {
Expand All @@ -22,6 +23,7 @@ const alertDomains = {
hub,
mpy,
sponsor,
usb,
};

/** Gets the type of available alert domains. */
Expand Down
10 changes: 9 additions & 1 deletion src/ble-device-info-service/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
// Copyright (c) 2021-2025 The Pybricks Authors
//
// Pybricks uses the standard Device Info service.
// Refer to Device Information Service (DIS) at https://www.bluetooth.com/specifications/specs/
Expand All @@ -15,6 +15,14 @@ import {
/** Device Information service UUID. */
export const deviceInformationServiceUUID = 0x180a;

/**
* Device Name characteristic UUID.
*
* NB: Some OSes block this, so we just get the device name from the BLE
* advertisement data instead. But we need this UUID for USB support.
*/
export const deviceNameUUID = 0x2a00;

/** Firmware Revision String characteristic UUID. */
export const firmwareRevisionStringUUID = 0x2a26;

Expand Down
97 changes: 0 additions & 97 deletions src/ble/reducers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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": "",
}
`);
});
Expand Down Expand Up @@ -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();
});
74 changes: 1 addition & 73 deletions src/ble/reducers.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -72,71 +65,6 @@ const connection: Reducer<BleConnectionState> = (
return state;
};

const deviceName: Reducer<string> = (state = '', action) => {
if (bleDidDisconnectPybricks.matches(action)) {
return '';
}

if (bleDidConnectPybricks.matches(action)) {
return action.name;
}

return state;
};

const deviceType: Reducer<string> = (state = '', action) => {
if (bleDidDisconnectPybricks.matches(action)) {
return '';
}

if (bleDIServiceDidReceivePnPId.matches(action)) {
return getHubTypeName(action.pnpId);
}

return state;
};

const deviceFirmwareVersion: Reducer<string> = (state = '', action) => {
if (bleDidDisconnectPybricks.matches(action)) {
return '';
}

if (bleDIServiceDidReceiveFirmwareRevision.matches(action)) {
return action.version;
}

return state;
};

const deviceLowBatteryWarning: Reducer<boolean> = (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<boolean> = (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,
});
99 changes: 96 additions & 3 deletions src/hub/reducers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -40,6 +44,11 @@ type State = ReturnType<typeof reducers>;
test('initial state', () => {
expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
{
"deviceBatteryCharging": false,
"deviceFirmwareVersion": "",
"deviceLowBatteryWarning": false,
"deviceName": "",
"deviceType": "",
"downloadProgress": null,
"hasRepl": false,
"maxBleWriteSize": 0,
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading