diff --git a/src/actions.ts b/src/actions.ts
index 5e3f6087..d88d7564 100644
--- a/src/actions.ts
+++ b/src/actions.ts
@@ -1,58 +1,39 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2022 The Pybricks Authors
+// Copyright (c) 2022 The Pybricks Authors
-import { AppAction } from './app/actions';
-import { BleDIServiceAction } from './ble-device-info-service/actions';
-import { BleUartAction } from './ble-nordic-uart-service/actions';
-import {
- BlePybricksServiceAction,
- BlePybricksServiceCommandAction,
- BlePybricksServiceEventAction,
-} from './ble-pybricks-service/actions';
-import { BLEAction, BLEConnectAction } from './ble/actions';
-import { EditorAction } from './editor/actions';
-import { FileStorageAction } from './fileStorage/actions';
-import { FlashFirmwareAction } from './firmware/actions';
-import { HubAction, HubMessageAction } from './hub/actions';
-import { LicenseAction } from './licenses/actions';
-import {
- BootloaderConnectionAction,
- BootloaderDidFailToRequestAction,
- BootloaderDidRequestAction,
- BootloaderRequestAction,
- BootloaderResponseAction,
-} from './lwp3-bootloader/actions';
-import { MpyAction } from './mpy/actions';
-import { NotificationAction } from './notifications/actions';
-import { ServiceWorkerAction } from './service-worker/actions';
-import { SettingsAction } from './settings/actions';
-import { TerminalDataAction } from './terminal/actions';
+import { AnyAction } from 'redux';
+
+/** A function that creates action objects. */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type ActionCreationFunction = (...args: any[]) => A;
+
+/** A function that performs type discrimination on an action. */
+type MatchFunction = (action: AnyAction) => action is A;
+
+/** The extra members that are attached to a function by createAction(). */
+type MatchableExtensions, A extends AnyAction> = {
+ toString(): ReturnType['type'];
+ matches: MatchFunction>;
+};
+
+/** An action creation function that includes MatchableExtensions. */
+type Matchable, A extends AnyAction> = F &
+ MatchableExtensions;
/**
- * Common type for all actions.
+ * Adds additional members to an action creation function.
+ *
+ * @param actionCreator The action creation function.
+ * @returns actionCreator with type property and match method added.
*/
-export type Action =
- | AppAction
- | BLEAction
- | BLEConnectAction
- | BleDIServiceAction
- | BlePybricksServiceAction
- | BlePybricksServiceCommandAction
- | BlePybricksServiceEventAction
- | BleUartAction
- | BootloaderConnectionAction
- | BootloaderDidRequestAction
- | BootloaderDidFailToRequestAction
- | BootloaderRequestAction
- | BootloaderResponseAction
- | FileStorageAction
- | EditorAction
- | FlashFirmwareAction
- | HubAction
- | HubMessageAction
- | LicenseAction
- | MpyAction
- | NotificationAction
- | ServiceWorkerAction
- | SettingsAction
- | TerminalDataAction;
+export function createAction, A extends AnyAction>(
+ actionCreator: T,
+): Matchable {
+ // create a default action so we can get the type string.
+ const type = actionCreator().type;
+
+ return Object.assign(actionCreator, >{
+ toString: () => type,
+ matches: (action) => action.type === type,
+ });
+}
diff --git a/src/app/actions.ts b/src/app/actions.ts
index b5f2d32c..5863bd77 100644
--- a/src/app/actions.ts
+++ b/src/app/actions.ts
@@ -1,117 +1,56 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
//
// Actions for the app in general.
-import { Action } from 'redux';
+import { createAction } from '../actions';
import { BeforeInstallPromptEvent } from '../utils/dom';
-/** App action types. */
-export enum AppActionType {
- /** Reload the app. */
- Reload = 'app.action.reload',
- /** Checks for an available update. */
- CheckForUpdate = 'app.action.checkForUpdate',
- /** Indicates that checking for update finished. */
- DidCheckForUpdate = 'app.action.didCheckForUpdate',
- /* Indicates the browser wants to prompt the use to install the app. */
- DidBeforeInstallPrompt = 'app.action.didBeforeInstallPrompt',
- /* Requests to prompt the user to install the app. */
- InstallPrompt = 'app.action.installPrompt',
- /* Indicates that the user responded to the install prompt. */
- DidInstallPrompt = 'app.action.didInstallPrompt',
- /* Indicates that the app was installed. */
- DidInstall = 'app.action.didInstall',
- /** The app has just ben started. */
- DidStart = 'app.action.didStart',
-}
-
-/** Action that requests the app to reload. */
-export type AppReloadAction = Action & {
- registration: ServiceWorkerRegistration;
-};
-
/** Creates an action that requests the app to reload. */
-export function reload(registration: ServiceWorkerRegistration): AppReloadAction {
- return { type: AppActionType.Reload, registration };
-}
+export const reload = createAction((registration: ServiceWorkerRegistration) => ({
+ type: 'app.action.reload',
+ registration,
+}));
/** Action that requests to check for updates. */
-export type AppCheckForUpdatesAction = Action & {
- registration: ServiceWorkerRegistration;
-};
-
-/** Action that requests to check for updates. */
-export function checkForUpdate(
- registration: ServiceWorkerRegistration,
-): AppCheckForUpdatesAction {
- return { type: AppActionType.CheckForUpdate, registration };
-}
+export const checkForUpdate = createAction(
+ (registration: ServiceWorkerRegistration) => ({
+ type: 'app.action.checkForUpdate',
+ registration,
+ }),
+);
/** Action that indicates that checking for an update has completed. */
-export type AppDidCheckForUpdateAction = Action & {
- updateFound: boolean;
-};
-
-/** Action that indicates that checking for an update has completed. */
-export function didCheckForUpdate(updateFound: boolean): AppDidCheckForUpdateAction {
- return { type: AppActionType.DidCheckForUpdate, updateFound };
-}
+export const didCheckForUpdate = createAction((updateFound: boolean) => ({
+ type: 'app.action.didCheckForUpdate',
+ updateFound,
+}));
/* Action that indicates the browser wants to prompt the use to install the app. */
-export type AppDidBeforeInstallPromptAction =
- Action & {
- event: BeforeInstallPromptEvent;
- };
-
-/* Action that indicates the browser wants to prompt the use to install the app. */
-export function didBeforeInstallPrompt(
- event: BeforeInstallPromptEvent,
-): AppDidBeforeInstallPromptAction {
- return { type: AppActionType.DidBeforeInstallPrompt, event };
-}
+export const didBeforeInstallPrompt = createAction(
+ (event: BeforeInstallPromptEvent) => ({
+ type: 'app.action.didBeforeInstallPrompt',
+ event,
+ }),
+);
/* Action that requests to prompt the user to install the app. */
-export type AppInstallPromptAction = Action & {
- event: BeforeInstallPromptEvent;
-};
-
-/* Action that requests to prompt the user to install the app. */
-export function installPrompt(event: BeforeInstallPromptEvent): AppInstallPromptAction {
- return { type: AppActionType.InstallPrompt, event };
-}
+export const installPrompt = createAction((event: BeforeInstallPromptEvent) => ({
+ type: 'app.action.installPrompt',
+ event,
+}));
/* Action that indicates that the user responded to the install prompt. */
-export type AppDidInstallPromptAction = Action;
-
-/* Action that indicates that the user responded to the install prompt. */
-export function didInstallPrompt(): AppDidInstallPromptAction {
- return { type: AppActionType.DidInstallPrompt };
-}
+export const didInstallPrompt = createAction(() => ({
+ type: 'app.action.didInstallPrompt',
+}));
/* Action that indicates app was installed. */
-export type AppDidInstallAction = Action;
-
-/* Action that indicates app was installed. */
-export function didInstall(): AppDidInstallAction {
- return { type: AppActionType.DidInstall };
-}
-
-/** Action that indicates the app has just started. */
-export type AppDidStartAction = Action;
+export const didInstall = createAction(() => ({
+ type: 'app.action.didInstall',
+}));
/** Creates an action that indicates the app has just started. */
-export function didStart(): AppDidStartAction {
- return { type: AppActionType.DidStart };
-}
-
-/** common type for all app actions. */
-export type AppAction =
- | AppReloadAction
- | AppCheckForUpdatesAction
- | AppDidCheckForUpdateAction
- | AppDidBeforeInstallPromptAction
- | AppInstallPromptAction
- | AppDidInstallPromptAction
- | AppDidInstallAction
- | AppDidStartAction;
+export const didStart = createAction(() => ({
+ type: 'app.action.didStart',
+}));
diff --git a/src/app/reducers.test.ts b/src/app/reducers.test.ts
index 95914627..eb80f8df 100644
--- a/src/app/reducers.test.ts
+++ b/src/app/reducers.test.ts
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
-import { Action } from '../actions';
+import { AnyAction } from 'redux';
import { didSucceed, didUpdate } from '../service-worker/actions';
import { BeforeInstallPromptEvent } from '../utils/dom';
import {
@@ -17,7 +17,7 @@ import reducers from './reducers';
type State = ReturnType;
test('initial state', () => {
- expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
+ expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
"beforeInstallPrompt": null,
"checkingForUpdate": false,
diff --git a/src/app/reducers.ts b/src/app/reducers.ts
index 3237f046..a9824f31 100644
--- a/src/app/reducers.ts
+++ b/src/app/reducers.ts
@@ -1,84 +1,92 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
//
// Manages state the app in general.
import { Reducer, combineReducers } from 'redux';
-import { Action } from '../actions';
-import { ServiceWorkerActionType } from '../service-worker/actions';
+import { didSucceed, didUpdate } from '../service-worker/actions';
import { BeforeInstallPromptEvent } from '../utils/dom';
-import { AppActionType } from './actions';
+import {
+ checkForUpdate,
+ didBeforeInstallPrompt,
+ didCheckForUpdate,
+ didInstall,
+ didInstallPrompt,
+ installPrompt,
+} from './actions';
-const serviceWorker: Reducer = (
+const serviceWorker: Reducer = (
state = null,
action,
) => {
- switch (action.type) {
- case ServiceWorkerActionType.DidSucceed:
- return action.registration;
- default:
- return state;
+ if (didSucceed.matches(action)) {
+ return action.registration;
}
+
+ return state;
};
-const checkingForUpdate: Reducer = (state = false, action) => {
- switch (action.type) {
- case AppActionType.CheckForUpdate:
- return true;
- case AppActionType.DidCheckForUpdate:
- if (!action.updateFound) {
- return false;
- }
- // otherwise we wait for service worker to download everything
- return state;
- case ServiceWorkerActionType.DidUpdate:
+const checkingForUpdate: Reducer = (state = false, action) => {
+ if (checkForUpdate.matches(action)) {
+ return true;
+ }
+
+ if (didCheckForUpdate.matches(action)) {
+ if (!action.updateFound) {
return false;
- default:
- return state;
+ }
+ // otherwise we wait for service worker to download everything
+ return state;
}
+
+ if (didUpdate.matches(action)) {
+ return false;
+ }
+
+ return state;
};
-const updateAvailable: Reducer = (state = false, action) => {
- switch (action.type) {
- case ServiceWorkerActionType.DidUpdate:
- return true;
- default:
- return state;
+const updateAvailable: Reducer = (state = false, action) => {
+ if (didUpdate.matches(action)) {
+ return true;
}
+
+ return state;
};
-const beforeInstallPrompt: Reducer = (
+const beforeInstallPrompt: Reducer = (
state = null,
action,
) => {
- switch (action.type) {
- case AppActionType.DidBeforeInstallPrompt:
- return action.event;
- case AppActionType.DidInstall:
- return null;
- default:
- return state;
+ if (didBeforeInstallPrompt.matches(action)) {
+ return action.event;
}
+
+ if (didInstall.matches(action)) {
+ return null;
+ }
+
+ return state;
};
-const promptingInstall: Reducer = (state = false, action) => {
- switch (action.type) {
- case AppActionType.InstallPrompt:
- return true;
- case AppActionType.DidInstallPrompt:
- return false;
- default:
- return state;
+const promptingInstall: Reducer = (state = false, action) => {
+ if (installPrompt.matches(action)) {
+ return true;
}
+
+ if (didInstallPrompt.matches(action)) {
+ return false;
+ }
+
+ return state;
};
-const readyForOfflineUse: Reducer = (state = false, action) => {
- switch (action.type) {
- case ServiceWorkerActionType.DidSucceed:
- return true;
- default:
- return state;
+const readyForOfflineUse: Reducer = (state = false, action) => {
+ if (didSucceed.matches(action)) {
+ return true;
}
+
+ return state;
};
export default combineReducers({
diff --git a/src/app/sagas.ts b/src/app/sagas.ts
index 8319a687..8bc82484 100644
--- a/src/app/sagas.ts
+++ b/src/app/sagas.ts
@@ -1,18 +1,17 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
import { eventChannel } from 'redux-saga';
import { call, fork, put, take, takeEvery } from 'typed-redux-saga/macro';
import { BeforeInstallPromptEvent } from '../utils/dom';
import {
- AppActionType,
- AppCheckForUpdatesAction,
- AppInstallPromptAction,
- AppReloadAction,
+ checkForUpdate,
didBeforeInstallPrompt,
didCheckForUpdate,
didInstall,
didInstallPrompt,
+ installPrompt,
+ reload,
} from './actions';
function* monitorAppInstalled(): Generator {
@@ -50,18 +49,18 @@ function* monitorBeforeInstallPrompt(): Generator {
}
}
-function* reload(action: AppReloadAction): Generator {
+function* handleReload(action: ReturnType): Generator {
yield* call(() => action.registration.unregister());
location.reload();
}
-function* checkForUpdate(action: AppCheckForUpdatesAction): Generator {
+function* handleCheckForUpdate(action: ReturnType): Generator {
yield* call(() => action.registration.update());
const updateFound = action.registration.installing !== null;
yield* put(didCheckForUpdate(updateFound));
}
-function* installPrompt(action: AppInstallPromptAction): Generator {
+function* handleInstallPrompt(action: ReturnType): Generator {
yield* call(() => action.event.prompt());
yield* call(() => action.event.userChoice);
yield* put(didInstallPrompt());
@@ -70,7 +69,7 @@ function* installPrompt(action: AppInstallPromptAction): Generator {
export default function* app(): Generator {
yield* fork(monitorAppInstalled);
yield* fork(monitorBeforeInstallPrompt);
- yield* takeEvery(AppActionType.Reload, reload);
- yield* takeEvery(AppActionType.CheckForUpdate, checkForUpdate);
- yield* takeEvery(AppActionType.InstallPrompt, installPrompt);
+ yield* takeEvery(reload, handleReload);
+ yield* takeEvery(checkForUpdate, handleCheckForUpdate);
+ yield* takeEvery(installPrompt, handleInstallPrompt);
}
diff --git a/src/ble-device-info-service/actions.ts b/src/ble-device-info-service/actions.ts
index e7d7b404..15e9da24 100644
--- a/src/ble-device-info-service/actions.ts
+++ b/src/ble-device-info-service/actions.ts
@@ -1,56 +1,27 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
-import { Action } from 'redux';
+import { createAction } from '../actions';
import { PnpId } from './protocol';
-export enum BleDIServiceActionType {
- DidReceiveFirmwareRevision = 'action.bleDIService.didReceiveFirmwareRevision',
- DidReceiveSoftwareRevision = 'action.bleDIService.didReceiveSoftwareRevision',
- DidReceivePnPId = 'action.bleDIService.didReceivePnPId',
-}
-
/** Action that indicates the firmware revision characteristic was read. */
-export type BleDIServiceDidReceiveFirmwareRevisionAction =
- Action & {
- version: string;
- };
-
-/** Action that indicates the firmware revision characteristic was read. */
-export function bleDIServiceDidReceiveFirmwareRevision(
- version: string,
-): BleDIServiceDidReceiveFirmwareRevisionAction {
- return { type: BleDIServiceActionType.DidReceiveFirmwareRevision, version };
-}
+export const bleDIServiceDidReceiveFirmwareRevision = createAction(
+ (version: string) => ({
+ type: 'action.bleDIService.didReceiveFirmwareRevision',
+ version,
+ }),
+);
/** Action that indicates the software revision characteristic was read. */
-export type BleDIServiceDidReceiveSoftwareRevisionAction =
- Action & {
- version: string;
- };
-
-/** Action that indicates the software revision characteristic was read. */
-export function bleDIServiceDidReceiveSoftwareRevision(
- version: string,
-): BleDIServiceDidReceiveSoftwareRevisionAction {
- return { type: BleDIServiceActionType.DidReceiveSoftwareRevision, version };
-}
+export const bleDIServiceDidReceiveSoftwareRevision = createAction(
+ (version: string) => ({
+ type: 'action.bleDIService.didReceiveSoftwareRevision',
+ version,
+ }),
+);
/** Action that indicates the PnP ID characteristic was read. */
-export type BleDIServiceDidReceivePnPIdAction =
- Action & {
- pnpId: PnpId;
- };
-
-/** Action that indicates the PnP ID characteristic was read. */
-export function bleDIServiceDidReceivePnPId(
- pnpId: PnpId,
-): BleDIServiceDidReceivePnPIdAction {
- return { type: BleDIServiceActionType.DidReceivePnPId, pnpId };
-}
-
-/** Common type for all device info service actions. */
-export type BleDIServiceAction =
- | BleDIServiceDidReceiveFirmwareRevisionAction
- | BleDIServiceDidReceiveSoftwareRevisionAction
- | BleDIServiceDidReceivePnPIdAction;
+export const bleDIServiceDidReceivePnPId = createAction((pnpId: PnpId) => ({
+ type: 'action.bleDIService.didReceivePnPId',
+ pnpId,
+}));
diff --git a/src/ble-nordic-uart-service/actions.ts b/src/ble-nordic-uart-service/actions.ts
index 6756aa61..97dc1726 100644
--- a/src/ble-nordic-uart-service/actions.ts
+++ b/src/ble-nordic-uart-service/actions.ts
@@ -1,69 +1,27 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
//
// Actions for Bluetooth Low Energy Nordic UART service
-import { Action } from 'redux';
+import { createAction } from '../actions';
-/**
- * BLE nRF UART service actions types.
- */
-export enum BleUartActionType {
- /**
- * Write data.
- */
- Write = 'bleUart.action.write',
- /**
- * Writing completed successfully.
- */
- DidWrite = 'bleUart.didWrite',
- /**
- * Writing failed.
- */
- DidFailToWrite = 'bleUart.action.didFailToWrite',
- /**
- * Notify that data was received.
- */
- DidNotify = 'bleUart.action.didNotify',
-}
+export const write = createAction((id: number, value: Uint8Array) => ({
+ type: 'bleUart.action.write',
+ id,
+ value,
+}));
-export type BleUartWriteAction = Action & {
- id: number;
- value: Uint8Array;
-};
+export const didWrite = createAction((id: number) => ({
+ type: 'bleUart.action.didWrite',
+ id,
+}));
-export function write(id: number, value: Uint8Array): BleUartWriteAction {
- return { type: BleUartActionType.Write, id, value };
-}
-
-export type BleUartDidWriteAction = Action & {
- id: number;
-};
-
-export function didWrite(id: number): BleUartDidWriteAction {
- return { type: BleUartActionType.DidWrite, id };
-}
-
-export type BleUartDidFailToWriteAction = Action & {
- id: number;
- err: Error;
-};
-
-export function didFailToWrite(id: number, err: Error): BleUartDidFailToWriteAction {
- return { type: BleUartActionType.DidFailToWrite, id, err };
-}
-
-export type BleUartDidNotifyAction = Action & {
- value: DataView;
-};
-
-export function didNotify(value: DataView): BleUartDidNotifyAction {
- return { type: BleUartActionType.DidNotify, value };
-}
-
-/** Common type for low-level BLE data actions. */
-export type BleUartAction =
- | BleUartWriteAction
- | BleUartDidWriteAction
- | BleUartDidFailToWriteAction
- | BleUartDidNotifyAction;
+export const didFailToWrite = createAction((id: number, err: Error) => ({
+ type: 'bleUart.action.didFailToWrite',
+ id,
+ err,
+}));
+export const didNotify = createAction((value: DataView) => ({
+ type: 'bleUart.action.didNotify',
+ value,
+}));
diff --git a/src/ble-pybricks-service/actions.ts b/src/ble-pybricks-service/actions.ts
index 33d1b2e6..ecddb2b3 100644
--- a/src/ble-pybricks-service/actions.ts
+++ b/src/ble-pybricks-service/actions.ts
@@ -1,238 +1,93 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
-// actions/blePybricksService.ts: Actions for Bluetooth Low Energy Pybricks service
+// Copyright (c) 2021-2022 The Pybricks Authors
+//
+// Actions for Bluetooth Low Energy Pybricks service
-import { Action } from 'redux';
+import { createAction } from '../actions';
-/**
- * BLE Pybricks service actions types.
- */
-export enum BlePybricksServiceActionType {
- /**
- * Write command to control characteristic.
- */
- WriteCommand = 'blePybricksService.action.writeCommand',
- /**
- * Writing command to control characteristic completed successfully.
- */
- DidWriteCommand = 'blePybricksService.action.didWriteCommand',
- /**
- * Writing command to control characteristic failed.
- */
- DidFailToWriteCommand = 'blePybricksService.action.didFailToWriteCommand',
- /**
- * Event notification was received from the control characteristic.
- */
- DidNotifyEvent = 'blePybricksService.action.didNotifyEvent',
-}
+// Low-level connection actions.
/**
* Action that request to write a command to the Pybricks service control characteristic.
*/
-export type BlePybricksServiceWriteCommandAction =
- Action & {
- id: number;
- value: Uint8Array;
- };
-
-/**
- * Action that request to write a command to the Pybricks service control characteristic.
- */
-export function writeCommand(
- id: number,
- value: Uint8Array,
-): BlePybricksServiceWriteCommandAction {
- return {
- type: BlePybricksServiceActionType.WriteCommand,
- id,
- value,
- };
-}
+export const writeCommand = createAction((id: number, value: Uint8Array) => ({
+ type: 'blePybricksService.action.writeCommand',
+ id,
+ value,
+}));
/**
* Action that indicates sending a command to the Pybricks service control characteristic was successful.
*/
-export type BlePybricksServiceDidWriteCommandAction =
- Action & {
- id: number;
- };
-
-/**
- * Action that indicates sending a command to the Pybricks service control characteristic was successful.
- */
-export function didWriteCommand(id: number): BlePybricksServiceDidWriteCommandAction {
- return {
- type: BlePybricksServiceActionType.DidWriteCommand,
- id,
- };
-}
+export const didWriteCommand = createAction((id: number) => ({
+ type: 'blePybricksService.action.didWriteCommand',
+ id,
+}));
/**
* Action that indicates sending a command to the Pybricks service control characteristic failed.
*/
-export type BlePybricksServiceDidFailToWriteCommandAction =
- Action & {
- id: number;
- err: Error;
- };
-
-/**
- * Action that indicates sending a command to the Pybricks service control characteristic failed.
- */
-export function didFailToWriteCommand(
- id: number,
- err: Error,
-): BlePybricksServiceDidFailToWriteCommandAction {
- return {
- type: BlePybricksServiceActionType.DidFailToWriteCommand,
- id,
- err,
- };
-}
+export const didFailToWriteCommand = createAction((id: number, err: Error) => ({
+ type: 'blePybricksService.action.didFailToWriteCommand',
+ id,
+ err,
+}));
/**
* Action that indicates an event notification was received on the Pybricks service control characteristic.
*/
-export type BlePybricksServiceDidNotifyEventAction =
- Action & {
- value: DataView;
- };
-
-/**
- * Action that indicates an event notification was received on the Pybricks service control characteristic.
- */
-export function didNotifyEvent(
- value: DataView,
-): BlePybricksServiceDidNotifyEventAction {
- return {
- type: BlePybricksServiceActionType.DidNotifyEvent,
- value,
- };
-}
-
-/** Common type for BLE Pybricks service actions. */
-export type BlePybricksServiceAction =
- | BlePybricksServiceWriteCommandAction
- | BlePybricksServiceDidWriteCommandAction
- | BlePybricksServiceDidFailToWriteCommandAction
- | BlePybricksServiceDidNotifyEventAction;
+export const didNotifyEvent = createAction((value: DataView) => ({
+ type: 'blePybricksService.action.didNotifyEvent',
+ value,
+}));
/** Action types for commands sent via the Pybricks service control characteristic. */
-export enum BlePybricksServiceCommandActionType {
- SendStopUserProgram = 'blePybricksServiceCommand.action.sendStopUserProgram',
- DidSend = 'blePybricksServiceCommand.action.didSend',
- DidFailToSend = 'blePybricksServiceCommand.action.didFailToSend',
-}
-
-type TransactionId = {
- /** Unique identifier for the transaction set in the "send" command. */
- id: number;
-};
-
-/** Action that requests a stop user program to be sent. */
-export type BlePybricksServiceCommandSendStopUserProgram =
- Action & TransactionId;
/**
* Action that requests a stop user program to be sent.
* @param id Unique identifier for this transaction.
*/
-export function sendStopUserProgramCommand(
- id: number,
-): BlePybricksServiceCommandSendStopUserProgram {
- return { type: BlePybricksServiceCommandActionType.SendStopUserProgram, id };
-}
-
-/**
- * Action that indicates that a command was successfully sent.
- */
-export type BlePybricksServiceCommandDidSendAction =
- Action & TransactionId;
+export const sendStopUserProgramCommand = createAction((id: number) => ({
+ type: 'blePybricksServiceCommand.action.sendStopUserProgram',
+ id,
+}));
/**
* Action that indicates that a command was successfully sent.
* @param id Unique identifier for the transaction from the corresponding "send" command.
*/
-export function didSendCommand(id: number): BlePybricksServiceCommandDidSendAction {
- return { type: BlePybricksServiceCommandActionType.DidSend, id };
-}
-
-/**
- * Action that indicates that a command was not sent.
- */
-export type BlePybricksServiceCommandDidFailToSendAction =
- Action &
- TransactionId & {
- /** The error that was raised. */
- err: Error;
- };
+export const didSendCommand = createAction((id: number) => ({
+ type: 'blePybricksServiceCommand.action.didSend',
+ id,
+}));
/**
* Action that indicates that a command was not sent.
* @param id Unique identifier for the transaction from the corresponding "send" command.
* @param err The error that was raised.
*/
-export function didFailToSendCommand(
- id: number,
- err: Error,
-): BlePybricksServiceCommandDidFailToSendAction {
- return { type: BlePybricksServiceCommandActionType.DidFailToSend, id, err };
-}
-
-/** Common type for Pybricks control characteristic send command actions. */
-export type BlePybricksServiceCommandAction =
- | BlePybricksServiceCommandSendStopUserProgram
- | BlePybricksServiceCommandDidSendAction
- | BlePybricksServiceCommandDidFailToSendAction;
+export const didFailToSendCommand = createAction((id: number, err: Error) => ({
+ type: 'blePybricksServiceCommand.action.didFailToSend',
+ id,
+ err,
+}));
/** Action types for events received from the Pybricks service control characteristic. */
-export enum BlePybricksServiceEventActionType {
- /** A status report event was received. */
- DidReceiveStatusReport = 'blePybricksServiceEvent.action.didReceiveStatusReport',
- /** A pseudo-event indicating there was a protocol error (not directly received from the hub). */
- ProtocolError = 'blePybricksServiceEvent.action.protocolError',
-}
-
-/**
- * Action that represents a status report event received from the hub.
- */
-export type BlePybricksServiceEventStatusReportAction =
- Action & {
- statusFlags: number;
- };
/**
* Action that represents a status report event received from the hub.
* @param statusFlags The status flags.
*/
-export function didReceiveStatusReport(
- statusFlags: number,
-): BlePybricksServiceEventStatusReportAction {
- return {
- type: BlePybricksServiceEventActionType.DidReceiveStatusReport,
- statusFlags,
- };
-}
+export const didReceiveStatusReport = createAction((statusFlags: number) => ({
+ type: 'blePybricksServiceEvent.action.didReceiveStatusReport',
+ statusFlags,
+}));
/**
- * Pseudo-event (not received from hub) indicating that there was a protocol error.
- */
-export type BlePybricksServiceEventProtocolErrorAction =
- Action & {
- err: Error;
- };
-
-/**
- * Pseudo-event (not received from hub) indicating that there was a protocol error.
+ * Pseudo-event = actionCreator((not received from hub) indicating that there was a protocol error.
* @param err The error that was caught.
*/
-export function eventProtocolError(
- err: Error,
-): BlePybricksServiceEventProtocolErrorAction {
- return { type: BlePybricksServiceEventActionType.ProtocolError, err };
-}
-
-/** Common type for Pybricks control characteristic event actions. */
-export type BlePybricksServiceEventAction =
- | BlePybricksServiceEventStatusReportAction
- | BlePybricksServiceEventProtocolErrorAction;
+export const eventProtocolError = createAction((err: Error) => ({
+ type: 'blePybricksServiceEvent.action.protocolError',
+ err,
+}));
diff --git a/src/ble-pybricks-service/sagas.ts b/src/ble-pybricks-service/sagas.ts
index 2f4b81cc..a8f50c37 100644
--- a/src/ble-pybricks-service/sagas.ts
+++ b/src/ble-pybricks-service/sagas.ts
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
//
// Handles Pybricks protocol.
+import { AnyAction } from 'redux';
import {
actionChannel,
fork,
@@ -11,19 +12,16 @@ import {
take,
takeEvery,
} from 'typed-redux-saga/macro';
-import { Action } from '../actions';
import { ensureError, hex } from '../utils';
import {
- BlePybricksServiceActionType,
- BlePybricksServiceCommandAction,
- BlePybricksServiceCommandActionType,
- BlePybricksServiceDidFailToWriteCommandAction,
- BlePybricksServiceDidNotifyEventAction,
- BlePybricksServiceDidWriteCommandAction,
didFailToSendCommand,
+ didFailToWriteCommand,
+ didNotifyEvent,
didReceiveStatusReport,
didSendCommand,
+ didWriteCommand,
eventProtocolError,
+ sendStopUserProgramCommand,
writeCommand,
} from './actions';
import {
@@ -41,38 +39,27 @@ import {
function* encodeRequest(): Generator {
// Using a while loop to serialize sending data to avoid "busy" errors.
- const sendCommands: readonly BlePybricksServiceCommandActionType[] = Object.values(
- BlePybricksServiceCommandActionType,
- ).filter(
- (x) =>
- x !== BlePybricksServiceCommandActionType.DidSend &&
- x != BlePybricksServiceCommandActionType.DidFailToSend,
- );
-
- const chan = yield* actionChannel((a: Action) =>
- sendCommands.includes(a.type as BlePybricksServiceCommandActionType),
+ const chan = yield* actionChannel(
+ (a: AnyAction) =>
+ typeof a.type === 'string' &&
+ a.type.startsWith('blePybricksServiceCommand.action.send'),
);
while (true) {
const action = yield* take(chan);
- switch (action.type) {
- case BlePybricksServiceCommandActionType.SendStopUserProgram:
- yield* put(writeCommand(action.id, createStopUserProgramCommand()));
- break;
- /* istanbul ignore next: should not be possible to reach */
- default:
- console.error(`Unknown Pybricks service command ${action.type}`);
- continue;
+ /* istanbul ignore else: should not be possible to reach */
+ if (sendStopUserProgramCommand.matches(action)) {
+ yield* put(writeCommand(action.id, createStopUserProgramCommand()));
+ } else {
+ console.error(`Unknown Pybricks service command ${action.type}`);
+ continue;
}
const { failedToSend } = yield* race({
- sent: take(
- BlePybricksServiceActionType.DidWriteCommand,
- ),
- failedToSend: take(
- BlePybricksServiceActionType.DidFailToWriteCommand,
- ),
+ sent: take>(didWriteCommand),
+ failedToSend:
+ take>(didFailToWriteCommand),
});
if (failedToSend) {
@@ -87,7 +74,7 @@ function* encodeRequest(): Generator {
* Converts an incoming connection message to a response action.
* @param action The received response action.
*/
-function* decodeResponse(action: BlePybricksServiceDidNotifyEventAction): Generator {
+function* decodeResponse(action: ReturnType): Generator {
try {
const responseType = getEventType(action.value);
switch (responseType) {
@@ -107,5 +94,5 @@ function* decodeResponse(action: BlePybricksServiceDidNotifyEventAction): Genera
export default function* (): Generator {
yield* fork(encodeRequest);
- yield* takeEvery(BlePybricksServiceActionType.DidNotifyEvent, decodeResponse);
+ yield* takeEvery(didNotifyEvent, decodeResponse);
}
diff --git a/src/ble/actions.ts b/src/ble/actions.ts
index 19f39181..0c85d711 100644
--- a/src/ble/actions.ts
+++ b/src/ble/actions.ts
@@ -1,62 +1,24 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
//
// Actions for managing Bluetooth Low Energy connections.
-import { Action } from 'redux';
-
-/**
- * Bluetooth low energy device action types.
- */
-export enum BleDeviceActionType {
- /**
- * Connecting to a device has been requested.
- */
- Connect = 'ble.device.action.connect',
- /**
- * The connection completed successfully.
- */
- DidConnect = 'ble.device.action.didConnect',
- /**
- * The connection did not complete successfully.
- */
- DidFailToConnect = 'ble.device.action.didFailToConnect',
- /**
- * Disconnecting from a device has been requested.
- */
- Disconnect = 'ble.device.action.disconnect',
- /**
- * The device was disconnected.
- */
- DidDisconnect = 'ble.device.action.didDisconnect',
- /**
- * The device fail to disconnect.
- */
- DidFailToDisconnect = 'ble.device.action.didFailToDisconnect',
-}
-
-export type BleDeviceConnectAction = Action;
-
+import { createAction } from '../actions';
/**
* Creates an action that indicates connecting has been requested.
*/
-export function connect(): BleDeviceConnectAction {
- return { type: BleDeviceActionType.Connect };
-}
-
-export type BleDeviceDidConnectAction = Action & {
- /** A unique identifier for the connected hub. */
- id: string;
- /** A user-displayable name for the connected hub. */
- name: string;
-};
+export const connect = createAction(() => ({
+ type: 'ble.device.action.connect',
+}));
/**
* Creates an action that indicates a device was connected.
*/
-export function didConnect(id: string, name: string): BleDeviceDidConnectAction {
- return { type: BleDeviceActionType.DidConnect, id, name };
-}
+export const didConnect = createAction((id: string, name: string) => ({
+ type: 'ble.device.action.didConnect',
+ id,
+ name,
+}));
export enum BleDeviceFailToConnectReasonType {
NoWebBluetooth = 'ble.device.didFailToConnect.noWebBluetooth',
@@ -104,69 +66,41 @@ export type BleDeviceDidFailToConnectReason =
| BleDeviceFailToConnectNoPybricksServiceReason
| BleDeviceFailToConnectUnknownReason;
-export type BleDeviceDidFailToConnectAction =
- Action & BleDeviceDidFailToConnectReason;
-
/**
* Creates an action that indicates a device failed to connect.
*/
-export function didFailToConnect(
- reason: BleDeviceDidFailToConnectReason,
-): BleDeviceDidFailToConnectAction {
- return { type: BleDeviceActionType.DidFailToConnect, ...reason };
-}
-
-export type BleDeviceDisconnectAction = Action;
+export const didFailToConnect = createAction(
+ (reason: BleDeviceDidFailToConnectReason) => ({
+ type: 'ble.device.action.didFailToConnect',
+ ...reason,
+ }),
+);
/**
* Creates an action that indicates disconnecting was requested.
*/
-export function disconnect(): BleDeviceDisconnectAction {
- return { type: BleDeviceActionType.Disconnect };
-}
-
-export type BleDeviceDidDisconnectAction = Action;
+export const disconnect = createAction(() => ({
+ type: 'ble.device.action.disconnect',
+}));
/**
* Creates an action that indicates a device was disconnected.
*/
-export function didDisconnect(): BleDeviceDidDisconnectAction {
- return { type: BleDeviceActionType.DidDisconnect };
-}
-
-export type BleDeviceDidFailToDisconnectAction =
- Action;
+export const didDisconnect = createAction(() => ({
+ type: 'ble.device.action.didDisconnect',
+}));
/**
* Creates an action that indicates a device failed to disconnect.
*/
-export function didFailToDisconnect(): BleDeviceDidFailToDisconnectAction {
- return { type: BleDeviceActionType.DidFailToDisconnect };
-}
-
-/**
- * Common type for all BLE connection actions.
- */
-export type BLEConnectAction =
- | BleDeviceConnectAction
- | BleDeviceDidConnectAction
- | BleDeviceDidFailToConnectAction
- | BleDeviceDisconnectAction
- | BleDeviceDidDisconnectAction
- | BleDeviceDidFailToDisconnectAction;
+export const didFailToDisconnect = createAction(() => ({
+ type: 'ble.device.action.didFailToDisconnect',
+}));
/**
* High-level BLE actions.
*/
-export enum BLEActionType {
- Toggle = 'ble.action.toggle',
-}
-export type BLEToggleAction = Action;
-
-export function toggleBluetooth(): BLEToggleAction {
- return { type: BLEActionType.Toggle };
-}
-
-/** Common type for high-level BLE actions */
-export type BLEAction = BLEToggleAction;
+export const toggleBluetooth = createAction(() => ({
+ type: 'ble.action.toggle',
+}));
diff --git a/src/ble/reducers.test.ts b/src/ble/reducers.test.ts
index 06a0662a..742c5ed3 100644
--- a/src/ble/reducers.test.ts
+++ b/src/ble/reducers.test.ts
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
-import { Action } from '../actions';
+import { AnyAction } from 'redux';
import {
bleDIServiceDidReceiveFirmwareRevision,
bleDIServiceDidReceivePnPId,
@@ -24,7 +24,7 @@ import reducers, { BleConnectionState } from './reducers';
type State = ReturnType;
test('initial state', () => {
- expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
+ expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
"connection": "ble.connection.state.disconnected",
"deviceBatteryCharging": false,
diff --git a/src/ble/reducers.ts b/src/ble/reducers.ts
index c14e089b..f69d9eaf 100644
--- a/src/ble/reducers.ts
+++ b/src/ble/reducers.ts
@@ -1,16 +1,25 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 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 {
+ bleDIServiceDidReceiveFirmwareRevision,
+ bleDIServiceDidReceivePnPId,
+} from '../ble-device-info-service/actions';
import { getHubTypeName } from '../ble-device-info-service/protocol';
-import { BlePybricksServiceEventActionType } from '../ble-pybricks-service/actions';
+import { didReceiveStatusReport } from '../ble-pybricks-service/actions';
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
-import { BleDeviceActionType } from './actions';
+import {
+ connect,
+ didConnect,
+ didDisconnect,
+ didFailToConnect,
+ didFailToDisconnect,
+ disconnect,
+} from './actions';
/**
* Describes the state of the BLE connection.
@@ -34,80 +43,87 @@ export enum BleConnectionState {
Disconnecting = 'ble.connection.state.disconnecting',
}
-const connection: Reducer = (
+const connection: Reducer = (
state = BleConnectionState.Disconnected,
action,
) => {
- switch (action.type) {
- case BleDeviceActionType.Connect:
- return BleConnectionState.Connecting;
- case BleDeviceActionType.DidConnect:
- case BleDeviceActionType.DidFailToDisconnect:
- return BleConnectionState.Connected;
- case BleDeviceActionType.Disconnect:
- return BleConnectionState.Disconnecting;
- case BleDeviceActionType.DidFailToConnect:
- case BleDeviceActionType.DidDisconnect:
- return BleConnectionState.Disconnected;
- default:
- return state;
+ if (connect.matches(action)) {
+ return BleConnectionState.Connecting;
}
+
+ if (didConnect.matches(action) || didFailToDisconnect.matches(action)) {
+ return BleConnectionState.Connected;
+ }
+
+ if (disconnect.matches(action)) {
+ return BleConnectionState.Disconnecting;
+ }
+
+ if (didFailToConnect.matches(action) || didDisconnect.matches(action)) {
+ return BleConnectionState.Disconnected;
+ }
+
+ return state;
};
-const deviceName: Reducer = (state = '', action) => {
- switch (action.type) {
- case BleDeviceActionType.DidDisconnect:
- return '';
- case BleDeviceActionType.DidConnect:
- return action.name;
- default:
- return state;
+const deviceName: Reducer = (state = '', action) => {
+ if (didDisconnect.matches(action)) {
+ return '';
}
+
+ if (didConnect.matches(action)) {
+ return action.name;
+ }
+
+ return state;
};
-const deviceType: Reducer = (state = '', action) => {
- switch (action.type) {
- case BleDeviceActionType.DidDisconnect:
- return '';
- case BleDIServiceActionType.DidReceivePnPId:
- return getHubTypeName(action.pnpId);
- default:
- return state;
+const deviceType: Reducer = (state = '', action) => {
+ if (didDisconnect.matches(action)) {
+ return '';
}
+
+ if (bleDIServiceDidReceivePnPId.matches(action)) {
+ return getHubTypeName(action.pnpId);
+ }
+
+ return state;
};
-const deviceFirmwareVersion: Reducer = (state = '', action) => {
- switch (action.type) {
- case BleDeviceActionType.DidDisconnect:
- return '';
- case BleDIServiceActionType.DidReceiveFirmwareRevision:
- return action.version;
- default:
- return state;
+const deviceFirmwareVersion: Reducer = (state = '', action) => {
+ if (didDisconnect.matches(action)) {
+ return '';
}
+
+ if (bleDIServiceDidReceiveFirmwareRevision.matches(action)) {
+ return action.version;
+ }
+
+ return state;
};
-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 deviceLowBatteryWarning: Reducer = (state = false, action) => {
+ if (didDisconnect.matches(action)) {
+ return false;
}
+
+ if (didReceiveStatusReport.matches(action)) {
+ return Boolean(
+ action.statusFlags & statusToFlag(Status.BatteryLowVoltageWarning),
+ );
+ }
+
+ 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;
+const deviceBatteryCharging: Reducer = (state = false, action) => {
+ if (didDisconnect.matches(action)) {
+ return false;
}
+
+ // TODO: hub does not currently have a status flag for this
+
+ return state;
};
export default combineReducers({
diff --git a/src/ble/sagas.ts b/src/ble/sagas.ts
index d591a798..b2edee5a 100644
--- a/src/ble/sagas.ts
+++ b/src/ble/sagas.ts
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
//
// Manages connection to a Bluetooth Low Energy device running Pybricks firmware.
@@ -28,11 +28,10 @@ import {
softwareRevisionStringUUID,
} from '../ble-device-info-service/protocol';
import {
- BleUartActionType,
- BleUartWriteAction,
didFailToWrite as didFailToWriteUart,
didNotify as didNotifyUart,
didWrite as didWriteUart,
+ write as writeUart,
} from '../ble-nordic-uart-service/actions';
import {
RxCharUUID as uartRxCharUUID,
@@ -40,10 +39,10 @@ import {
TxCharUUID as uartTxCharUUID,
} from '../ble-nordic-uart-service/protocol';
import {
- BlePybricksServiceActionType,
didFailToWriteCommand,
didNotifyEvent,
didWriteCommand,
+ writeCommand,
} from '../ble-pybricks-service/actions';
import {
ControlCharacteristicUUID as pybricksCommandCharacteristicUUID,
@@ -52,25 +51,21 @@ import {
import { RootState } from '../reducers';
import { ensureError } from '../utils';
import {
- BLEActionType,
- BleDeviceActionType as BLEDeviceActionType,
- BLEToggleAction,
- BleDeviceConnectAction,
- BleDeviceDisconnectAction,
BleDeviceFailToConnectReasonType as Reason,
- connect as connectAction,
+ connect,
didConnect,
didDisconnect,
didFailToConnect,
- disconnect as disconnectAction,
+ disconnect,
+ toggleBluetooth,
} from './actions';
import { BleConnectionState } from './reducers';
const decoder = new TextDecoder();
-function disconnect(
+function handleDisconnect(
server: BluetoothRemoteGATTServer,
- _action: BleDeviceDisconnectAction,
+ _action: ReturnType,
): void {
server.disconnect();
}
@@ -79,9 +74,9 @@ function* handlePybricksControlValueChanged(data: DataView): Generator {
yield* put(didNotifyEvent(data));
}
-function* writePybricksCommand(
+function* handleWriteCommand(
char: BluetoothRemoteGATTCharacteristic,
- action: BleUartWriteAction,
+ action: ReturnType,
): Generator {
try {
yield* call(() => char.writeValueWithoutResponse(action.value.buffer));
@@ -95,9 +90,9 @@ function* handleUartValueChanged(data: DataView): Generator {
yield* put(didNotifyUart(data));
}
-function* writeUart(
+function* handleWriteUart(
char: BluetoothRemoteGATTCharacteristic,
- action: BleUartWriteAction,
+ action: ReturnType,
): Generator {
try {
yield* call(() => char.writeValueWithoutResponse(action.value.buffer));
@@ -107,7 +102,7 @@ function* writeUart(
}
}
-function* connect(_action: BleDeviceConnectAction): Generator {
+function* handleConnect(): Generator {
if (navigator.bluetooth === undefined) {
yield* put(didFailToConnect({ reason: Reason.NoWebBluetooth }));
return;
@@ -164,7 +159,7 @@ function* connect(_action: BleDeviceConnectAction): Generator {
return;
}
- yield* takeEvery(BLEDeviceActionType.Disconnect, disconnect, server);
+ yield* takeEvery(disconnect, handleDisconnect, server);
let deviceInfoService: BluetoothRemoteGATTService;
try {
@@ -325,13 +320,7 @@ function* connect(_action: BleDeviceConnectAction): Generator {
return;
}
- tasks.push(
- yield* takeEvery(
- BlePybricksServiceActionType.WriteCommand,
- writePybricksCommand,
- pybricksControlChar,
- ),
- );
+ tasks.push(yield* takeEvery(writeCommand, handleWriteCommand, pybricksControlChar));
let uartService: BluetoothRemoteGATTService;
try {
@@ -407,7 +396,7 @@ function* connect(_action: BleDeviceConnectAction): Generator {
return;
}
- tasks.push(yield* takeEvery(BleUartActionType.Write, writeUart, uartRxChar));
+ tasks.push(yield* takeEvery(writeUart, handleWriteUart, uartRxChar));
yield* put(didConnect(device.id, device.name || ''));
@@ -421,22 +410,22 @@ function* connect(_action: BleDeviceConnectAction): Generator {
yield* put(didDisconnect());
}
-function* toggle(_action: BLEToggleAction): Generator {
+function* handleToggleBluetooth(): Generator {
const connectionState = (yield select(
(s: RootState) => s.ble.connection,
)) as BleConnectionState;
switch (connectionState) {
case BleConnectionState.Connected:
- yield* put(disconnectAction());
+ yield* put(disconnect());
break;
case BleConnectionState.Disconnected:
- yield* put(connectAction());
+ yield* put(connect());
break;
}
}
export default function* (): Generator {
- yield* takeEvery(BLEDeviceActionType.Connect, connect);
- yield* takeEvery(BLEActionType.Toggle, toggle);
+ yield* takeEvery(connect, handleConnect);
+ yield* takeEvery(toggleBluetooth, handleToggleBluetooth);
}
diff --git a/src/editor/actions.ts b/src/editor/actions.ts
index ce25fade..867d55ce 100644
--- a/src/editor/actions.ts
+++ b/src/editor/actions.ts
@@ -1,88 +1,43 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { monaco } from 'react-monaco-editor';
-import { Action } from 'redux';
-
-export enum EditorActionType {
- /** The current (active) editor changed. */
- Current = 'editor.action.current',
- /** Save the current file to disk. */
- SaveAs = 'editor.action.saveAs',
- /** Saving the file succeeded. */
- DidSaveAs = 'editor.action.didSaveAs',
- /** Saving the file failed. */
- DidFailToSaveAs = 'editor.action.didFailToSaveAs',
- /** Open a file. */
- Open = 'editor.action.open',
-}
-
-export type CurrentEditorAction = Action & {
- editSession: monaco.editor.ICodeEditor | undefined;
-};
+import { createAction } from '../actions';
/**
* Sets the current (active) edit session.
* @param editSession The new edit session.
*/
-export function setEditSession(
- editSession: monaco.editor.ICodeEditor | undefined,
-): CurrentEditorAction {
- return { type: EditorActionType.Current, editSession };
-}
-
-/**
- * Action that saves the current file.
- */
-export type EditorSaveAsAction = Action;
+export const setEditSession = createAction(
+ (editSession: monaco.editor.ICodeEditor | undefined) => ({
+ type: 'editor.action.setEditSession',
+ editSession,
+ }),
+);
/**
* Creates an action to save the current file
*/
-export function saveAs(): EditorSaveAsAction {
- return { type: EditorActionType.SaveAs };
-}
+export const saveAs = createAction(() => ({
+ type: 'editor.action.saveAs',
+}));
/** Action that indicates saving a file succeeded. */
-export type EditorDidSaveAsAction = Action;
-
-/** Action that indicates saving a file succeeded. */
-export function didSaveAs(): EditorDidSaveAsAction {
- return { type: EditorActionType.DidSaveAs };
-}
+export const didSaveAs = createAction(() => ({
+ type: 'editor.action.didSaveAs',
+}));
/** Action that indicates saving a file failed. */
-export type EditorDidFailToSaveAsAction = Action & {
- err: Error;
-};
-
-/** Action that indicates saving a file failed. */
-export function didFailToSaveAs(err: Error): EditorDidFailToSaveAsAction {
- return { type: EditorActionType.DidFailToSaveAs, err };
-}
-
-/**
- * Action that opens a file.
- */
-export type EditorOpenAction = Action & {
- /** The data to save */
- data: ArrayBuffer;
-};
+export const didFailToSaveAs = createAction((err: Error) => ({
+ type: 'editor.action.didFailToSaveAs',
+ err,
+}));
/**
* Creates an action to save a file
* @param data The file data
*/
-export function open(data: ArrayBuffer): EditorOpenAction {
- return { type: EditorActionType.Open, data };
-}
-
-/**
- * Common type for all editor actions.
- */
-export type EditorAction =
- | CurrentEditorAction
- | EditorOpenAction
- | EditorSaveAsAction
- | EditorDidSaveAsAction
- | EditorDidFailToSaveAsAction;
+export const open = createAction((data: ArrayBuffer) => ({
+ type: 'editor.action.open',
+ data,
+}));
diff --git a/src/editor/reducers.test.ts b/src/editor/reducers.test.ts
index 142962b3..48d88bf7 100644
--- a/src/editor/reducers.test.ts
+++ b/src/editor/reducers.test.ts
@@ -2,14 +2,14 @@
// Copyright (c) 2021 The Pybricks Authors
import { monaco } from 'react-monaco-editor';
-import { Action } from '../actions';
+import { AnyAction } from 'redux';
import { setEditSession } from './actions';
import reducers from './reducers';
type State = ReturnType;
test('initial state', () => {
- expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
+ expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
"current": null,
}
diff --git a/src/editor/reducers.ts b/src/editor/reducers.ts
index a2e5fb82..e6b8fe65 100644
--- a/src/editor/reducers.ts
+++ b/src/editor/reducers.ts
@@ -1,21 +1,16 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { monaco } from 'react-monaco-editor';
import { Reducer, combineReducers } from 'redux';
-import { Action } from '../actions';
-import { EditorActionType } from './actions';
+import { setEditSession } from './actions';
-const current: Reducer = (
- state = null,
- action,
-) => {
- switch (action.type) {
- case EditorActionType.Current:
- return action.editSession || null;
- default:
- return state;
+const current: Reducer = (state = null, action) => {
+ if (setEditSession.matches(action)) {
+ return action.editSession || null;
}
+
+ return state;
};
export default combineReducers({ current });
diff --git a/src/editor/sagas.ts b/src/editor/sagas.ts
index 696f99eb..5a6afd00 100644
--- a/src/editor/sagas.ts
+++ b/src/editor/sagas.ts
@@ -2,6 +2,7 @@
// Copyright (c) 2020-2022 The Pybricks Authors
import FileSaver from 'file-saver';
+import { AnyAction } from 'redux';
import {
call,
put,
@@ -11,27 +12,19 @@ import {
takeEvery,
takeLatest,
} from 'typed-redux-saga/macro';
-import { Action } from '../actions';
import {
- FileStorageActionType,
- FileStorageDidFailToReadFileAction,
- FileStorageDidReadFileAction,
+ fileStorageDidFailToReadFile,
+ fileStorageDidInitialize,
+ fileStorageDidReadFile,
fileStorageReadFile,
} from '../fileStorage/actions';
import { RootState } from '../reducers';
import { ensureError } from '../utils';
-import {
- CurrentEditorAction,
- EditorActionType,
- EditorOpenAction,
- EditorSaveAsAction,
- didFailToSaveAs,
- didSaveAs,
-} from './actions';
+import { didFailToSaveAs, didSaveAs, open, saveAs, setEditSession } from './actions';
const decoder = new TextDecoder();
-function* open(action: EditorOpenAction): Generator {
+function* handleOpen(action: ReturnType): Generator {
const editor = yield* select((s: RootState) => s.editor.current);
// istanbul ignore next: it is a bug to dispatch this action with no current editor
@@ -44,7 +37,7 @@ function* open(action: EditorOpenAction): Generator {
editor.setValue(text);
}
-function* saveAs(_action: EditorSaveAsAction): Generator {
+function* handleSaveAs(): Generator {
const editor = yield* select((s: RootState) => s.editor.current);
// istanbul ignore next: it is a bug to dispatch this action with no current editor
@@ -93,7 +86,7 @@ function* saveAs(_action: EditorSaveAsAction): Generator {
yield* put(didSaveAs());
}
-function* handleEditSession(action: CurrentEditorAction): Generator {
+function* handleSetEditSession(action: ReturnType): Generator {
if (action.editSession === null) {
// there is not current edit session, nothing to do
return;
@@ -106,7 +99,7 @@ function* handleEditSession(action: CurrentEditorAction): Generator {
);
if (!isStorageInitialized) {
- yield* take(FileStorageActionType.DidInitialize);
+ yield* take(fileStorageDidInitialize);
}
// TODO: get current file from state
@@ -116,14 +109,13 @@ function* handleEditSession(action: CurrentEditorAction): Generator {
yield* put(fileStorageReadFile(currentFileName));
const { result } = yield* race({
- result: take(
- (a: Action) =>
- a.type === FileStorageActionType.DidReadFile &&
- a.fileName === currentFileName,
+ result: take>(
+ (a: AnyAction) =>
+ fileStorageDidReadFile.matches(a) && a.fileName === currentFileName,
),
- error: take(
- (a: Action) =>
- a.type === FileStorageActionType.DidFailToReadFile &&
+ error: take>(
+ (a: AnyAction) =>
+ fileStorageDidFailToReadFile.matches(a) &&
a.fileName === currentFileName,
),
});
@@ -134,7 +126,7 @@ function* handleEditSession(action: CurrentEditorAction): Generator {
}
export default function* (): Generator {
- yield* takeEvery(EditorActionType.Open, open);
- yield* takeEvery(EditorActionType.SaveAs, saveAs);
- yield* takeLatest(EditorActionType.Current, handleEditSession);
+ yield* takeEvery(open, handleOpen);
+ yield* takeEvery(saveAs, handleSaveAs);
+ yield* takeLatest(setEditSession, handleSetEditSession);
}
diff --git a/src/error-log/sagas.ts b/src/error-log/sagas.ts
index b918f7db..c9792a6f 100644
--- a/src/error-log/sagas.ts
+++ b/src/error-log/sagas.ts
@@ -1,74 +1,63 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { takeEvery } from 'typed-redux-saga/macro';
+import { didFailToWrite as bleUartDidFailToWrite } from '../ble-nordic-uart-service/actions';
+import { eventProtocolError as pybricksEventProtocolError } from '../ble-pybricks-service/actions';
import {
- BleUartActionType,
- BleUartDidFailToWriteAction,
-} from '../ble-nordic-uart-service/actions';
-import {
- BlePybricksServiceEventActionType,
- BlePybricksServiceEventProtocolErrorAction,
-} from '../ble-pybricks-service/actions';
-import {
- BleDeviceActionType,
- BleDeviceDidFailToConnectAction,
BleDeviceFailToConnectReasonType,
+ didFailToConnect as bleDeviceDidFailToConnect,
} from '../ble/actions';
+import { didFailToFetchList } from '../licenses/actions';
import {
- LicenseActionType,
- LicenseDidFailToFetchListAction,
-} from '../licenses/actions';
-import {
- BootloaderConnectionActionType,
- BootloaderConnectionDidErrorAction,
- BootloaderConnectionDidFailToConnectAction,
BootloaderConnectionFailureReason,
+ didError as bootloaderDidError,
+ didFailToConnect as bootloaderDidFailToConnect,
} from '../lwp3-bootloader/actions';
-function bleDeviceDidFailToConnect(action: BleDeviceDidFailToConnectAction): void {
+function handleBleDeviceDidFailToConnect(
+ action: ReturnType,
+): void {
if (action.reason === BleDeviceFailToConnectReasonType.Unknown) {
console.error(action.err);
}
}
-function pybricksProtocolError(
- action: BlePybricksServiceEventProtocolErrorAction,
+function handlePybricksEventProtocolError(
+ action: ReturnType,
): void {
console.error(action.err);
}
-function bleDataDidFailToWrite(action: BleUartDidFailToWriteAction): void {
+function handleBleUartDidFailToWrite(
+ action: ReturnType,
+): void {
console.error(action.err);
}
-function bootloaderDidFailToConnect(
- action: BootloaderConnectionDidFailToConnectAction,
+function handleBootloaderDidFailToConnect(
+ action: ReturnType,
): void {
if (action.reason === BootloaderConnectionFailureReason.Unknown) {
console.error(action.err);
}
}
-function bootloaderDidError(action: BootloaderConnectionDidErrorAction): void {
+function handleBootloaderDidError(action: ReturnType): void {
console.error(action.err);
}
-function licenseDidFailToFetch(action: LicenseDidFailToFetchListAction): void {
+function handleLicenseDidFailToFetch(
+ action: ReturnType,
+): void {
console.error(`Failed to fetch licenses: ${action.reason.statusText}`);
}
export default function* (): Generator {
- yield* takeEvery(BleDeviceActionType.DidFailToConnect, bleDeviceDidFailToConnect);
- yield* takeEvery(
- BlePybricksServiceEventActionType.ProtocolError,
- pybricksProtocolError,
- );
- yield* takeEvery(BleUartActionType.DidFailToWrite, bleDataDidFailToWrite);
- yield* takeEvery(
- BootloaderConnectionActionType.DidFailToConnect,
- bootloaderDidFailToConnect,
- );
- yield* takeEvery(BootloaderConnectionActionType.DidError, bootloaderDidError);
- yield* takeEvery(LicenseActionType.DidFailToFetchList, licenseDidFailToFetch);
+ yield* takeEvery(bleDeviceDidFailToConnect, handleBleDeviceDidFailToConnect);
+ yield* takeEvery(pybricksEventProtocolError, handlePybricksEventProtocolError);
+ yield* takeEvery(bleUartDidFailToWrite, handleBleUartDidFailToWrite);
+ yield* takeEvery(bootloaderDidFailToConnect, handleBootloaderDidFailToConnect);
+ yield* takeEvery(bootloaderDidError, handleBootloaderDidError);
+ yield* takeEvery(didFailToFetchList, handleLicenseDidFailToFetch);
}
diff --git a/src/fileStorage/actions.ts b/src/fileStorage/actions.ts
index a3b5f392..63e00b62 100644
--- a/src/fileStorage/actions.ts
+++ b/src/fileStorage/actions.ts
@@ -1,169 +1,75 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
-import { Action } from 'redux';
-
-export enum FileStorageActionType {
- /** Action that indicates that the storage backend is ready to use. */
- DidInitialize = 'fileStorage.action.didInitialize',
- /** Action that indicates that the storage backend failed to initialize. */
- DidFailToInitialize = 'fileStorage.action.didFailToInitialize',
- /** Action that indicates that an item in the storage was created or changed by us or in another tab. */
- DidChangeItem = 'fileStorage.action.didChangeItem',
- /** Action that indicates that an item in the storage was removed by us or in another tab. */
- DidRemoveItem = 'fileStorage.action.didRemoveItem',
- /** Requests to read a file from storage. */
- ReadFile = 'fileStorage.action.readFile',
- /** Response to read file request indicating success. */
- DidReadFile = 'fileStorage.action.didReadFile',
- /** Response to read file request indicating failure. */
- DidFailToReadFile = 'fileStorage.action.didFailToReadFile',
- /** Requests to write a file to storage. */
- WriteFile = 'fileStorage.action.writeFile',
- /** Response to write file request indicating success. */
- DidWriteFile = 'fileStorage.action.didWriteFile',
- /** Response to write file request indicating failure. */
- DidFailToWriteFile = 'fileStorage.action.didFailToWriteFile',
-}
+import { createAction } from '../actions';
/** Action that indicates that the storage backend is ready to use. */
-export type FileStorageDidInitializeAction =
- Action;
-
-/** Action that indicates that the storage backend is ready to use. */
-export function fileStorageDidInitialize(): FileStorageDidInitializeAction {
- return { type: FileStorageActionType.DidInitialize };
-}
+export const fileStorageDidInitialize = createAction(() => ({
+ type: 'fileStorage.action.didInitialize',
+}));
/** Action that indicates that the storage backend failed to initialize. */
-export type FileStorageDidFailToInitializeAction =
- Action & { error: Error };
-
-/** Action that indicates that the storage backend failed to initialize. */
-export function fileStorageDidFailToInitialize(
- err: Error,
-): FileStorageDidFailToInitializeAction {
- return { type: FileStorageActionType.DidFailToInitialize, error: err };
-}
+export const fileStorageDidFailToInitialize = createAction((error: Error) => ({
+ type: 'fileStorage.action.didFailToInitialize',
+ error,
+}));
/** Action that indicates that an item in the storage was created or changed by us or in another tab. */
-export type FileStorageDidChangeItemAction =
- Action & {
- fileName: string;
- };
-
-/** Action that indicates that an item in the storage was created or changed by us or in another tab. */
-export function fileStorageDidChangeItem(
- fileName: string,
-): FileStorageDidChangeItemAction {
- return { type: FileStorageActionType.DidChangeItem, fileName };
-}
+export const fileStorageDidChangeItem = createAction((fileName: string) => ({
+ type: 'fileStorage.action.didChangeItem',
+ fileName,
+}));
/** Action that indicates that an item in the storage was removed by us or in another tab. */
-export type FileStorageDidRemoveItemAction =
- Action & {
- fileName: string;
- };
-
-/** Action that indicates that an item in the storage was removed by us or in another tab. */
-export function fileStorageDidRemoveItem(
- fileName: string,
-): FileStorageDidRemoveItemAction {
- return { type: FileStorageActionType.DidRemoveItem, fileName };
-}
+export const fileStorageDidRemoveItem = createAction((fileName: string) => ({
+ type: 'fileStorage.action.didRemoveItem',
+ fileName,
+}));
/** Requests to read a file from storage. */
-export type FileStorageReadFileAction = Action & {
- fileName: string;
-};
-
-/** Requests to read a file from storage. */
-export function fileStorageReadFile(fileName: string): FileStorageReadFileAction {
- return { type: FileStorageActionType.ReadFile, fileName };
-}
+export const fileStorageReadFile = createAction((fileName: string) => ({
+ type: 'fileStorage.action.readFile',
+ fileName,
+}));
/** Response to read file request indicating success. */
-export type FileStorageDidReadFileAction = Action & {
- fileName: string;
- fileContents: string;
-};
-
-/** Response to read file request indicating success. */
-export function fileStorageDidReadFile(
- fileName: string,
- fileContents: string,
-): FileStorageDidReadFileAction {
- return { type: FileStorageActionType.DidReadFile, fileName, fileContents };
-}
+export const fileStorageDidReadFile = createAction(
+ (fileName: string, fileContents: string) => ({
+ type: 'fileStorage.action.didReadFile',
+ fileName,
+ fileContents,
+ }),
+);
/** Response to read file request indicating failure. */
-export type FileStorageDidFailToReadFileAction =
- Action & {
- fileName: string;
- error: Error;
- };
-
-/** Response to read file request indicating failure. */
-export function fileStorageDidFailToReadFile(
- fileName: string,
- error: Error,
-): FileStorageDidFailToReadFileAction {
- return { type: FileStorageActionType.DidFailToReadFile, fileName, error };
-}
+export const fileStorageDidFailToReadFile = createAction(
+ (fileName: string, error: Error) => ({
+ type: 'fileStorage.action.didFailToReadFile',
+ fileName,
+ error,
+ }),
+);
/** Requests to write a file to storage. */
-export type FileStorageWriteFileAction = Action & {
- fileName: string;
- fileContents: string;
-};
-
-/** Requests to write a file to storage. */
-export function fileStorageWriteFile(
- fileName: string,
- fileContents: string,
-): FileStorageWriteFileAction {
- return { type: FileStorageActionType.WriteFile, fileName, fileContents };
-}
+export const fileStorageWriteFile = createAction(
+ (fileName: string, fileContents: string) => ({
+ type: 'fileStorage.action.writeFile',
+ fileName,
+ fileContents,
+ }),
+);
/** Response to write file request indicating success. */
-export type FileStorageDidWriteFileAction =
- Action & {
- fileName: string;
- };
-
-/** Response to write file request indicating success. */
-export function fileStorageDidWriteFile(
- fileName: string,
-): FileStorageDidWriteFileAction {
- return { type: FileStorageActionType.DidWriteFile, fileName };
-}
+export const fileStorageDidWriteFile = createAction((fileName: string) => ({
+ type: 'fileStorage.action.didWriteFile',
+ fileName,
+}));
/** Response to write file request indicating failure. */
-export type FileStorageDidFailToWriteFileAction =
- Action & {
- fileName: string;
- error: Error;
- };
-
-/** Response to write file request indicating failure. */
-export function fileStorageDidFailToWriteFile(
- fileName: string,
- error: Error,
-): FileStorageDidFailToWriteFileAction {
- return { type: FileStorageActionType.DidFailToWriteFile, fileName, error };
-}
-
-/**
- * Common type for all file storage actions.
- */
-export type FileStorageAction =
- | FileStorageDidInitializeAction
- | FileStorageDidFailToInitializeAction
- | FileStorageDidChangeItemAction
- | FileStorageDidRemoveItemAction
- | FileStorageReadFileAction
- | FileStorageDidReadFileAction
- | FileStorageDidFailToReadFileAction
- | FileStorageWriteFileAction
- | FileStorageDidWriteFileAction
- | FileStorageDidFailToWriteFileAction;
+export const fileStorageDidFailToWriteFile = createAction(
+ (fileName: string, error: Error) => ({
+ type: 'fileStorage.action.didFailToWriteFile',
+ fileName,
+ error,
+ }),
+);
diff --git a/src/fileStorage/reducers.test.ts b/src/fileStorage/reducers.test.ts
index 2587bb96..cd01389a 100644
--- a/src/fileStorage/reducers.test.ts
+++ b/src/fileStorage/reducers.test.ts
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
-import { Action } from '../actions';
+import { AnyAction } from 'redux';
import {
fileStorageDidChangeItem,
fileStorageDidInitialize,
@@ -12,7 +12,7 @@ import reducers from './reducers';
type State = ReturnType;
test('initial state', () => {
- expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
+ expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
"fileNames": Set {},
"isInitialized": false,
diff --git a/src/fileStorage/reducers.ts b/src/fileStorage/reducers.ts
index 8fcdd3ee..fd624628 100644
--- a/src/fileStorage/reducers.ts
+++ b/src/fileStorage/reducers.ts
@@ -2,27 +2,30 @@
// Copyright (c) 2022 The Pybricks Authors
import { Reducer, combineReducers } from 'redux';
-import { Action } from '../actions';
-import { FileStorageActionType } from './actions';
+import {
+ fileStorageDidChangeItem,
+ fileStorageDidInitialize,
+ fileStorageDidRemoveItem,
+} from './actions';
-const isInitialized: Reducer = (state = false, action) => {
- switch (action.type) {
- case FileStorageActionType.DidInitialize:
- return true;
- default:
- return state;
+const isInitialized: Reducer = (state = false, action) => {
+ if (fileStorageDidInitialize.matches(action)) {
+ return true;
}
+
+ return state;
};
-const fileNames: Reducer, Action> = (state = new Set(), action) => {
- switch (action.type) {
- case FileStorageActionType.DidChangeItem:
- return new Set([...state, action.fileName]);
- case FileStorageActionType.DidRemoveItem:
- return new Set([...state].filter((value) => value !== action.fileName));
- default:
- return state;
+const fileNames: Reducer> = (state = new Set(), action) => {
+ if (fileStorageDidChangeItem.matches(action)) {
+ return new Set([...state, action.fileName]);
}
+
+ if (fileStorageDidRemoveItem.matches(action)) {
+ return new Set([...state].filter((value) => value !== action.fileName));
+ }
+
+ return state;
};
export default combineReducers({ isInitialized, fileNames });
diff --git a/src/fileStorage/sagas.test.ts b/src/fileStorage/sagas.test.ts
index af10aaa1..29ee3bdc 100644
--- a/src/fileStorage/sagas.test.ts
+++ b/src/fileStorage/sagas.test.ts
@@ -1,7 +1,10 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
import { AsyncSaga } from '../../test';
import {
- FileStorageActionType,
fileStorageDidChangeItem,
+ fileStorageDidFailToReadFile,
fileStorageDidInitialize,
fileStorageDidReadFile,
fileStorageDidWriteFile,
@@ -79,7 +82,7 @@ it('should dispatch fail action if file does not exist', async () => {
saga.put(fileStorageReadFile(testFileName));
action = await saga.take();
- expect(action).toHaveProperty('type', FileStorageActionType.DidFailToReadFile);
+ expect(action).toHaveProperty('type', fileStorageDidFailToReadFile.toString());
await saga.end();
});
diff --git a/src/fileStorage/sagas.ts b/src/fileStorage/sagas.ts
index 5bc59715..3158c266 100644
--- a/src/fileStorage/sagas.ts
+++ b/src/fileStorage/sagas.ts
@@ -8,9 +8,6 @@ import { call, fork, put, takeEvery } from 'typed-redux-saga/macro';
import Observable from 'zen-observable';
import { ensureError } from '../utils';
import {
- FileStorageActionType,
- FileStorageReadFileAction,
- FileStorageWriteFileAction,
fileStorageDidChangeItem,
fileStorageDidFailToInitialize,
fileStorageDidFailToReadFile,
@@ -19,6 +16,8 @@ import {
fileStorageDidReadFile,
fileStorageDidRemoveItem,
fileStorageDidWriteFile,
+ fileStorageReadFile,
+ fileStorageWriteFile,
} from './actions';
/**
@@ -47,7 +46,7 @@ function* handleFileStorageDidChange(change: LocalForageObservableChange): Gener
*/
function* handleReadFile(
files: LocalForage,
- action: FileStorageReadFileAction,
+ action: ReturnType,
): Generator {
try {
const value = yield* call(() => files.getItem(action.fileName));
@@ -67,7 +66,10 @@ function* handleReadFile(
* @param files The localForage instance.
* @param action The action that triggered this saga.
*/
-function* handleWriteFile(files: LocalForage, action: FileStorageWriteFileAction) {
+function* handleWriteFile(
+ files: LocalForage,
+ action: ReturnType,
+) {
try {
yield* call(() => files.setItem(action.fileName, action.fileContents));
yield* put(fileStorageDidWriteFile(action.fileName));
@@ -116,8 +118,8 @@ function* initialize(): Generator {
// subscribe to events
yield* takeEvery(localForageChannel, handleFileStorageDidChange);
- yield* takeEvery(FileStorageActionType.ReadFile, handleReadFile, files);
- yield* takeEvery(FileStorageActionType.WriteFile, handleWriteFile, files);
+ yield* takeEvery(fileStorageReadFile, handleReadFile, files);
+ yield* takeEvery(fileStorageWriteFile, handleWriteFile, files);
// migrate from old storage
diff --git a/src/firmware/actions.ts b/src/firmware/actions.ts
index 8839f575..eaebea53 100644
--- a/src/firmware/actions.ts
+++ b/src/firmware/actions.ts
@@ -1,25 +1,8 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { FirmwareMetadata, FirmwareReaderError } from '@pybricks/firmware';
-import { Action } from 'redux';
-import { assert } from '../utils';
-
-/**
- * High-level bootloader actions.
- */
-export enum FlashFirmwareActionType {
- /** Request to flash new firmware to the device. */
- FlashFirmware = 'flashFirmware.action.flashFirmware',
- /** Actual modification of the flash memory on the device started. */
- DidStart = 'flashFirmware.action.didStart',
- /** Firmware flash progress. */
- DidProgress = 'flashFirmware.action.didProgress',
- /** Flashing finished successfully. */
- DidFinish = 'flashFirmware.action.didFinish',
- /** Flashing firmware failed. */
- DidFailToFinish = 'flashFirmware.action.didFailToFinish',
-}
+import { createAction } from '../actions';
export enum MetadataProblem {
Missing = 'metadata.missing',
@@ -132,122 +115,149 @@ export type FailToFinishReason =
| FailToFinishReasonFailedToCompile
| FailToFinishReasonUnknown;
-/**
- * Action that flashes firmware to a hub.
- */
-export type FlashFirmwareFlashAction = Action & {
- /** The firmware zip file data or `null` to get firmware later. */
- data: ArrayBuffer | null;
-};
+// High-level bootloader actions.
/**
* Creates a new action to flash firmware to a hub.
* @param data The firmware zip file data or `null` to get firmware later.
*/
-export function flashFirmware(data: ArrayBuffer | null): FlashFirmwareFlashAction {
- return { type: FlashFirmwareActionType.FlashFirmware, data };
-}
-
-/** Action that indicates flashing firmware started. */
-export type FlashFirmwareDidStartAction = Action;
+export const flashFirmware = createAction((data: ArrayBuffer | null) => ({
+ type: 'flashFirmware.action.flashFirmware',
+ data,
+}));
/**
* Action that indicates flashing firmware started.
* @param total The total number of bytes to be flashed.
*/
-export function didStart(): FlashFirmwareDidStartAction {
- return { type: FlashFirmwareActionType.DidStart };
-}
-
-/** Action that indicates current firmware flashing progress. */
-export type FlashFirmwareDidProgressAction =
- Action & {
- /** The current progress (0 to 1). */
- value: number;
- };
+export const didStart = createAction(() => ({
+ type: 'flashFirmware.action.didStart',
+}));
/**
* Action that indicates current firmware flashing progress.
* @param value The current progress (0 to 1).
*/
-export function didProgress(value: number): FlashFirmwareDidProgressAction {
- assert(value >= 0 && value <= 1, 'value out of range');
- return { type: FlashFirmwareActionType.DidProgress, value };
-}
+export const didProgress = createAction((value: number) => {
+ // assert(value >= 0 && value <= 1, 'value out of range');
+ return { type: 'flashFirmware.action.didProgress', value };
+});
/** Action that indicates that flashing firmware completed successfully. */
-export type FlashFirmwareDidFinishAction = Action;
+export const didFinish = createAction(() => ({
+ type: 'flashFirmware.action.didFinish',
+}));
-/** Action that indicates that flashing firmware completed successfully. */
-export function didFinish(): FlashFirmwareDidFinishAction {
- return { type: FlashFirmwareActionType.DidFinish };
-}
+const didFailToFinishType = 'flashFirmware.action.didFailToFinish';
-/** Action that indicates that flashing failed. */
-export type FlashFirmwareDidFailToFinishAction =
- Action & {
- reason: FailToFinishReason;
- };
+function didFailToFinishCreator(reason: FailToFinishReasonType.FailedToConnect): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonFailedToConnect;
+};
-export function didFailToFinish(
+function didFailToFinishCreator(reason: FailToFinishReasonType.TimedOut): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonTimedOut;
+};
+
+function didFailToFinishCreator(
reason: FailToFinishReasonType.BleError,
err: Error,
-): FlashFirmwareDidFailToFinishAction;
+): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonBleError;
+};
-export function didFailToFinish(
+function didFailToFinishCreator(reason: FailToFinishReasonType.Disconnected): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonDisconnected;
+};
+
+function didFailToFinishCreator(
reason: FailToFinishReasonType.HubError,
hubError: HubError,
-): FlashFirmwareDidFailToFinishAction;
+): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonHubError;
+};
-export function didFailToFinish(
+function didFailToFinishCreator(reason: FailToFinishReasonType.NoFirmware): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonNoFirmware;
+};
+
+function didFailToFinishCreator(reason: FailToFinishReasonType.DeviceMismatch): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonDeviceMismatch;
+};
+
+function didFailToFinishCreator(
reason: FailToFinishReasonType.FailedToFetch,
response: Response,
-): FlashFirmwareDidFailToFinishAction;
+): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonFailedToFetch;
+};
-export function didFailToFinish(
+function didFailToFinishCreator(
reason: FailToFinishReasonType.ZipError,
err: FirmwareReaderError,
-): FlashFirmwareDidFailToFinishAction;
+): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonZipError;
+};
-export function didFailToFinish(
+function didFailToFinishCreator(
reason: FailToFinishReasonType.BadMetadata,
property: keyof FirmwareMetadata,
problem: MetadataProblem,
-): FlashFirmwareDidFailToFinishAction;
+): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonBadMetadata;
+};
-export function didFailToFinish(
+function didFailToFinishCreator(reason: FailToFinishReasonType.FirmwareSize): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonFirmwareSize;
+};
+
+function didFailToFinishCreator(reason: FailToFinishReasonType.FailedToCompile): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonFailedToCompile;
+};
+
+function didFailToFinishCreator(
reason: FailToFinishReasonType.Unknown,
err: Error,
-): FlashFirmwareDidFailToFinishAction;
+): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReasonUnknown;
+};
-export function didFailToFinish(
- reason: Exclude<
- FailToFinishReasonType,
- | FailToFinishReasonType.BleError
- | FailToFinishReasonType.HubError
- | FailToFinishReasonType.FailedToFetch
- | FailToFinishReasonType.ZipError
- | FailToFinishReasonType.BadMetadata
- | FailToFinishReasonType.Unknown
- >,
-): FlashFirmwareDidFailToFinishAction;
+function didFailToFinishCreator(
+ reason: T,
+ arg1?: string | HubError | Error | Response,
+ arg2?: MetadataProblem,
+): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReason;
+};
-/**
- * Action that indicates flashing did not start because of an error.
- * @param total The total number of bytes to be flashed.
- */
-export function didFailToFinish(
+function didFailToFinishCreator(
reason: FailToFinishReasonType,
arg1?: string | HubError | Error | Response,
arg2?: MetadataProblem,
-): FlashFirmwareDidFailToFinishAction {
+): {
+ type: typeof didFailToFinishType;
+ reason: FailToFinishReason;
+} {
if (reason === FailToFinishReasonType.BleError) {
// istanbul ignore if: programmer error give wrong arg
if (!(arg1 instanceof Error)) {
throw new Error('missing or invalid err');
}
return {
- type: FlashFirmwareActionType.DidFailToFinish,
+ type: didFailToFinishType,
reason: { reason, err: arg1 },
};
}
@@ -258,7 +268,7 @@ export function didFailToFinish(
throw new Error('missing or invalid hubError');
}
return {
- type: FlashFirmwareActionType.DidFailToFinish,
+ type: didFailToFinishType,
reason: { reason, hubError: arg1 },
};
}
@@ -269,7 +279,7 @@ export function didFailToFinish(
throw new Error('missing or invalid response');
}
return {
- type: FlashFirmwareActionType.DidFailToFinish,
+ type: didFailToFinishType,
reason: { reason, response: arg1 },
};
}
@@ -280,7 +290,7 @@ export function didFailToFinish(
throw new Error('missing or invalid err');
}
return {
- type: FlashFirmwareActionType.DidFailToFinish,
+ type: didFailToFinishType,
reason: { reason, err: arg1 },
};
}
@@ -304,7 +314,7 @@ export function didFailToFinish(
throw new Error('missing or invalid problem');
}
return {
- type: FlashFirmwareActionType.DidFailToFinish,
+ type: didFailToFinishType,
reason: { reason, property: arg1, problem: arg2 },
};
}
@@ -315,20 +325,16 @@ export function didFailToFinish(
throw new Error('missing or invalid err');
}
return {
- type: FlashFirmwareActionType.DidFailToFinish,
+ type: didFailToFinishType,
reason: { reason, err: arg1 },
};
}
- return { type: FlashFirmwareActionType.DidFailToFinish, reason: { reason } };
+ return { type: didFailToFinishType, reason: { reason } };
}
/**
- * Common type for all high-level bootloader actions.
+ * Action that indicates flashing did not start because of an error.
+ * @param total The total number of bytes to be flashed.
*/
-export type FlashFirmwareAction =
- | FlashFirmwareFlashAction
- | FlashFirmwareDidStartAction
- | FlashFirmwareDidProgressAction
- | FlashFirmwareDidFinishAction
- | FlashFirmwareDidFailToFinishAction;
+export const didFailToFinish = createAction(didFailToFinishCreator);
diff --git a/src/firmware/reducers.test.ts b/src/firmware/reducers.test.ts
index c47b0f04..a910f8e0 100644
--- a/src/firmware/reducers.test.ts
+++ b/src/firmware/reducers.test.ts
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
-import { Action } from '../actions';
+import { AnyAction } from 'redux';
import {
FailToFinishReasonType,
didFailToFinish,
@@ -14,7 +14,7 @@ import reducers from './reducers';
type State = ReturnType;
test('initial state', () => {
- expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
+ expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
"flashing": false,
"progress": null,
diff --git a/src/firmware/reducers.ts b/src/firmware/reducers.ts
index 831b1e19..97bd6c12 100644
--- a/src/firmware/reducers.ts
+++ b/src/firmware/reducers.ts
@@ -1,31 +1,31 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
import { Reducer, combineReducers } from 'redux';
-import { Action } from '../actions';
-import { FlashFirmwareActionType } from './actions';
+import { didFailToFinish, didFinish, didProgress, didStart } from './actions';
-const flashing: Reducer = (state = false, action) => {
- switch (action.type) {
- case FlashFirmwareActionType.DidStart:
- return true;
- case FlashFirmwareActionType.DidFinish:
- case FlashFirmwareActionType.DidFailToFinish:
- return false;
- default:
- return state;
+const flashing: Reducer = (state = false, action) => {
+ if (didStart.matches(action)) {
+ return true;
}
+
+ if (didFinish.matches(action) || didFailToFinish.matches(action)) {
+ return false;
+ }
+
+ return state;
};
-const progress: Reducer = (state = null, action) => {
- switch (action.type) {
- case FlashFirmwareActionType.DidStart:
- return null;
- case FlashFirmwareActionType.DidProgress:
- return action.value;
- default:
- return state;
+const progress: Reducer = (state = null, action) => {
+ if (didStart.matches(action)) {
+ return null;
}
+
+ if (didProgress.matches(action)) {
+ return action.value;
+ }
+
+ return state;
};
export default combineReducers({ flashing, progress });
diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts
index c96d8e3d..22923d1f 100644
--- a/src/firmware/sagas.ts
+++ b/src/firmware/sagas.ts
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import {
FirmwareReader,
@@ -10,6 +10,7 @@ import {
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
import technicHubZip from '@pybricks/firmware/build/technichub.zip';
+import { AnyAction } from 'redux';
import {
SagaGenerator,
all,
@@ -23,54 +24,43 @@ import {
take,
takeEvery,
} from 'typed-redux-saga/macro';
-import { Action } from '../actions';
import {
- BootloaderChecksumResponseAction,
- BootloaderConnectionAction,
- BootloaderConnectionActionType,
- BootloaderDidFailToRequestAction,
- BootloaderDidFailToRequestType,
- BootloaderDidRequestAction,
- BootloaderDidRequestType,
- BootloaderEraseResponseAction,
- BootloaderErrorResponseAction,
- BootloaderInfoResponseAction,
- BootloaderInitResponseAction,
- BootloaderProgramResponseAction,
- BootloaderResponseAction,
- BootloaderResponseActionType,
checksumRequest,
+ checksumResponse,
connect,
+ didConnect,
+ didDisconnect,
+ didFailToConnect,
+ didFailToRequest,
+ didRequest,
disconnect,
eraseRequest,
eraseResponse,
+ errorResponse,
infoRequest,
+ infoResponse,
initRequest,
+ initResponse,
programRequest,
+ programResponse,
rebootRequest,
} from '../lwp3-bootloader/actions';
import { MaxProgramFlashSize, Result } from '../lwp3-bootloader/protocol';
import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
-import {
- MpyActionType,
- MpyDidCompileAction,
- MpyDidFailToCompileAction,
- compile,
-} from '../mpy/actions';
+import { compile, didCompile, didFailToCompile } from '../mpy/actions';
import { RootState } from '../reducers';
import { defined, ensureError, hex, maybe } from '../utils';
import { fmod, sumComplement32 } from '../utils/math';
import { isAndroid } from '../utils/os';
import {
FailToFinishReasonType,
- FlashFirmwareActionType,
- FlashFirmwareFlashAction,
HubError,
MetadataProblem,
didFailToFinish,
didFinish,
didProgress,
didStart,
+ flashFirmware,
} from './actions';
const firmwareZipMap = new Map([
@@ -93,13 +83,13 @@ function* disconnectAndCancel(): SagaGenerator {
yield* cancel();
}
-function* waitForDidRequest(id: number): SagaGenerator {
+function* waitForDidRequest(id: number): SagaGenerator> {
const { requested, failedToRequest } = yield* race({
- requested: take(
- (a: Action) => a.type === BootloaderDidRequestType && a.id === id,
+ requested: take>(
+ (a: AnyAction) => didRequest.matches(a) && a.id === id,
),
- failedToRequest: take(
- (a: Action) => a.type === BootloaderDidFailToRequestType && a.id === id,
+ failedToRequest: take>(
+ (a: AnyAction) => didFailToRequest.matches(a) && a.id === id,
),
});
@@ -121,26 +111,26 @@ function* waitForDidRequest(id: number): SagaGenerator(
- type: BootloaderResponseActionType,
+function* waitForResponse(
+ type: string,
timeout = 500,
): SagaGenerator {
const { response, error, disconnected, timedOut } = yield* race({
response: take(type),
- error: take(BootloaderResponseActionType.Error),
- disconnected: take(BootloaderConnectionActionType.DidDisconnect),
+ error: take>(errorResponse),
+ disconnected: take(didDisconnect),
timedOut: delay(timeout),
});
if (timedOut) {
// istanbul ignore if: this hacks around a hardware/OS issue
- if (type === BootloaderResponseActionType.Erase) {
+ if (type === errorResponse.toString()) {
// It has been observed that sometimes this response is not received
// or gets stuck in the Bluetooth stack until another request is sent.
// So, we ignore the timeout and continue. If there really was a
// problem, then the next request should fail anyway.
console.warn('Timeout waiting for erase response, continuing anyway.');
- return eraseResponse(Result.OK) as T;
+ return eraseResponse(Result.OK) as unknown as T;
}
yield* put(didFailToFinish(FailToFinishReasonType.TimedOut));
yield* disconnectAndCancel();
@@ -219,8 +209,8 @@ function* loadFirmware(
yield* put(compile(program, metadata['mpy-cross-options']));
const { mpy, mpyFail } = yield* race({
- mpy: take(MpyActionType.DidCompile),
- mpyFail: take(MpyActionType.DidFailToCompile),
+ mpy: take>(didCompile),
+ mpyFail: take>(didFailToCompile),
});
if (mpyFail) {
@@ -280,7 +270,7 @@ function* loadFirmware(
* Flashes firmware to a Powered Up device.
* @param action The action that triggered this saga.
*/
-function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
+function* handleFlashFirmware(action: ReturnType): Generator {
try {
let firmware: Uint8Array | undefined = undefined;
let deviceId: HubType | undefined = undefined;
@@ -308,12 +298,9 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
}
yield* put(connect());
- const connectResult = yield* take([
- BootloaderConnectionActionType.DidConnect,
- BootloaderConnectionActionType.DidFailToConnect,
- ]);
+ const connectResult = yield* take([didConnect, didFailToConnect]);
- if (connectResult.type === BootloaderConnectionActionType.DidFailToConnect) {
+ if (didFailToConnect.matches(connectResult)) {
yield* put(didFailToFinish(FailToFinishReasonType.FailedToConnect));
return;
}
@@ -323,8 +310,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
const infoAction = yield* put(infoRequest(nextMessageId()));
const { info } = yield* all({
sent: waitForDidRequest(infoAction.id),
- info: waitForResponse(
- BootloaderResponseActionType.Info,
+ info: waitForResponse>(
+ infoResponse.toString(),
),
});
@@ -366,8 +353,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
);
const { erase } = yield* all({
sent: waitForDidRequest(eraseAction.id),
- erase: waitForResponse(
- BootloaderResponseActionType.Erase,
+ erase: waitForResponse>(
+ eraseResponse.toString(),
5000,
),
});
@@ -381,8 +368,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
const initAction = yield* put(initRequest(nextMessageId(), firmware.length));
const { init } = yield* all({
sent: waitForDidRequest(initAction.id),
- init: waitForResponse(
- BootloaderResponseActionType.Init,
+ init: waitForResponse>(
+ initResponse.toString(),
),
});
if (init.result) {
@@ -433,8 +420,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
const { response } = yield* all({
sent: waitForDidRequest(checksumAction.id),
- response: waitForResponse(
- BootloaderResponseActionType.Checksum,
+ response: waitForResponse>(
+ checksumResponse.toString(),
5000,
),
});
@@ -460,8 +447,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
}
}
- const flash = yield* waitForResponse(
- BootloaderResponseActionType.Program,
+ const flash = yield* waitForResponse>(
+ programResponse.toString(),
5000,
);
@@ -508,5 +495,5 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
}
export default function* (): Generator {
- yield* takeEvery(FlashFirmwareActionType.FlashFirmware, flashFirmware);
+ yield* takeEvery(flashFirmware, handleFlashFirmware);
}
diff --git a/src/hub/actions.ts b/src/hub/actions.ts
index 6480cb72..a6ac04ef 100644
--- a/src/hub/actions.ts
+++ b/src/hub/actions.ts
@@ -1,97 +1,43 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
-import { Action } from 'redux';
-
-export enum HubMessageActionType {
- /**
- * The hub has sent a checksum.
- */
- Checksum = 'hub.message.action.runtime.checksum',
-}
-
-export type HubChecksumMessageAction = Action & {
- readonly checksum: number;
-};
-
-export function checksum(checksum: number): HubChecksumMessageAction {
- return {
- type: HubMessageActionType.Checksum,
- checksum,
- };
-}
+import { createAction } from '../actions';
/**
- * Common type for low-level hub message actions.
+ * Action that indicates the hub has sent a checksum.
*/
-export type HubMessageAction = HubChecksumMessageAction;
+export const checksum = createAction((checksum: number) => ({
+ type: 'hub.message.action.runtime.checksum',
+ checksum,
+}));
-/**
- * High-level hub actions.
- */
-export enum HubActionType {
- DownloadAndRun = 'hub.action.downloadAndRun',
- DidStartDownload = 'hub.action.didStartDownload',
- DidProgressDownload = 'hub.action.didProgressDownload',
- DidFinishDownload = 'hub.action.didFinishDownload',
- DidFailToFinishDownload = 'hub.action.didFailToFinishDownload',
- Stop = 'hub.action.stop',
- Repl = 'hub.action.repl',
-}
+// High-level hub actions.
-export type HubDownloadAndRunAction = Action;
+export const downloadAndRun = createAction(() => ({
+ type: 'hub.action.downloadAndRun',
+}));
-export function downloadAndRun(): HubDownloadAndRunAction {
- return { type: HubActionType.DownloadAndRun };
-}
+export const didStartDownload = createAction(() => ({
+ type: 'hub.action.didStartDownload',
+}));
-export type HubDidStartDownloadAction = Action;
+export const didProgressDownload = createAction((progress: number) => ({
+ type: 'hub.action.didProgressDownload',
+ progress,
+}));
-export function didStartDownload(): HubDidStartDownloadAction {
- return { type: HubActionType.DidStartDownload };
-}
+export const didFinishDownload = createAction(() => ({
+ type: 'hub.action.didFinishDownload',
+}));
-export type HubDidProgressDownloadAction = Action & {
- progress: number;
-};
+export const didFailToFinishDownload = createAction(() => ({
+ type: 'hub.action.didFailToFinishDownload',
+}));
-export function didProgressDownload(progress: number): HubDidProgressDownloadAction {
- return { type: HubActionType.DidProgressDownload, progress };
-}
+export const stop = createAction(() => ({
+ type: 'hub.action.stop',
+}));
-export type HubDidFinishDownloadAction = Action;
-
-export function didFinishDownload(): HubDidFinishDownloadAction {
- return { type: HubActionType.DidFinishDownload };
-}
-
-export type HubDidFailToFinishDownloadAction =
- Action;
-
-export function didFailToFinishDownload(): HubDidFailToFinishDownloadAction {
- return { type: HubActionType.DidFailToFinishDownload };
-}
-
-export type HubStopAction = Action;
-
-export function stop(): HubStopAction {
- return { type: HubActionType.Stop };
-}
-
-export type HubReplAction = Action;
-
-export function repl(): HubReplAction {
- return { type: HubActionType.Repl };
-}
-
-/**
- * Common type for all high-level hub actions.
- */
-export type HubAction =
- | HubDownloadAndRunAction
- | HubDidStartDownloadAction
- | HubDidProgressDownloadAction
- | HubDidFinishDownloadAction
- | HubDidFailToFinishDownloadAction
- | HubStopAction
- | HubReplAction;
+export const repl = createAction(() => ({
+ type: 'hub.action.repl',
+}));
diff --git a/src/hub/reducers.test.ts b/src/hub/reducers.test.ts
index 8820ee49..15dc7293 100644
--- a/src/hub/reducers.test.ts
+++ b/src/hub/reducers.test.ts
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
-import { Action } from '../actions';
+import { AnyAction } from 'redux';
import { didReceiveStatusReport } from '../ble-pybricks-service/actions';
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
import { didConnect, didDisconnect } from '../ble/actions';
@@ -16,7 +16,7 @@ import reducers, { HubRuntimeState } from './reducers';
type State = ReturnType;
test('initial state', () => {
- expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
+ expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
"downloadProgress": null,
"runtime": "hub.runtime.disconnected",
diff --git a/src/hub/reducers.ts b/src/hub/reducers.ts
index d0e88036..c4a70ae2 100644
--- a/src/hub/reducers.ts
+++ b/src/hub/reducers.ts
@@ -1,12 +1,16 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { Reducer, combineReducers } from 'redux';
-import { Action } from '../actions';
-import { BlePybricksServiceEventActionType } from '../ble-pybricks-service/actions';
+import { didReceiveStatusReport } from '../ble-pybricks-service/actions';
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
-import { BleDeviceActionType } from '../ble/actions';
-import { HubActionType } from './actions';
+import { didConnect, didDisconnect } from '../ble/actions';
+import {
+ didFailToFinishDownload,
+ didFinishDownload,
+ didProgressDownload,
+ didStartDownload,
+} from './actions';
/**
* Describes the state of the MicroPython runtime on the hub.
@@ -38,66 +42,80 @@ export enum HubRuntimeState {
Running = 'hub.runtime.running',
}
-const runtime: Reducer = (
+const runtime: Reducer = (
state = HubRuntimeState.Disconnected,
action,
) => {
- switch (action.type) {
- case BleDeviceActionType.DidConnect:
- return HubRuntimeState.Unknown;
- case BleDeviceActionType.DidDisconnect:
- return HubRuntimeState.Disconnected;
- case HubActionType.DidStartDownload:
- // disconnected overrides download
- if (state === HubRuntimeState.Disconnected) {
- return state;
- }
- return HubRuntimeState.Loading;
- case HubActionType.DidFinishDownload:
- // disconnected overrides download
- if (state === HubRuntimeState.Disconnected) {
- return state;
- }
- return HubRuntimeState.Loaded;
- case HubActionType.DidFailToFinishDownload:
- // disconnected overrides download
- if (state === HubRuntimeState.Disconnected) {
- return state;
- }
- return HubRuntimeState.Idle;
- case BlePybricksServiceEventActionType.DidReceiveStatusReport:
- // The loading state is determined solely by the IDE, so we can't
- // let the hub status interfere with it.
- if (
- state === HubRuntimeState.Disconnected ||
- state === HubRuntimeState.Loading
- ) {
- return state;
- }
-
- if (action.statusFlags & statusToFlag(Status.UserProgramRunning)) {
- return HubRuntimeState.Running;
- }
-
- return HubRuntimeState.Idle;
- default:
- return state;
+ if (didConnect.matches(action)) {
+ return HubRuntimeState.Unknown;
}
+
+ if (didDisconnect.matches(action)) {
+ return HubRuntimeState.Disconnected;
+ }
+
+ if (didStartDownload.matches(action)) {
+ // disconnected overrides download
+ if (state === HubRuntimeState.Disconnected) {
+ return state;
+ }
+ return HubRuntimeState.Loading;
+ }
+
+ if (didFinishDownload.matches(action)) {
+ // disconnected overrides download
+ if (state === HubRuntimeState.Disconnected) {
+ return state;
+ }
+ return HubRuntimeState.Loaded;
+ }
+
+ if (didFailToFinishDownload.matches(action)) {
+ // disconnected overrides download
+ if (state === HubRuntimeState.Disconnected) {
+ return state;
+ }
+ return HubRuntimeState.Idle;
+ }
+
+ if (didReceiveStatusReport.matches(action)) {
+ // The loading state is determined solely by the IDE, so we can't
+ // let the hub status interfere with it.
+ if (
+ state === HubRuntimeState.Disconnected ||
+ state === HubRuntimeState.Loading
+ ) {
+ return state;
+ }
+
+ if (action.statusFlags & statusToFlag(Status.UserProgramRunning)) {
+ return HubRuntimeState.Running;
+ }
+
+ return HubRuntimeState.Idle;
+ }
+
+ return state;
};
-const downloadProgress: Reducer = (state = null, action) => {
- switch (action.type) {
- case HubActionType.DidStartDownload:
- return 0;
- case HubActionType.DidProgressDownload:
- return action.progress;
- case HubActionType.DidFinishDownload:
- return 1;
- case HubActionType.DidFailToFinishDownload:
- return null;
- default:
- return state;
+const downloadProgress: Reducer = (state = null, action) => {
+ if (didStartDownload.matches(action)) {
+ return 0;
}
+
+ if (didProgressDownload.matches(action)) {
+ return action.progress;
+ }
+
+ if (didFinishDownload.matches(action)) {
+ return 1;
+ }
+
+ if (didFailToFinishDownload.matches(action)) {
+ return null;
+ }
+
+ return state;
};
export default combineReducers({ runtime, downloadProgress });
diff --git a/src/hub/sagas.test.ts b/src/hub/sagas.test.ts
index 1e2b5ff9..9dd738d5 100644
--- a/src/hub/sagas.test.ts
+++ b/src/hub/sagas.test.ts
@@ -1,25 +1,21 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { mock } from 'jest-mock-extended';
import { monaco } from 'react-monaco-editor';
import { AsyncSaga } from '../../test';
+import { didWrite, write } from '../ble-nordic-uart-service/actions';
import {
- BleUartActionType,
- BleUartWriteAction,
- didWrite,
-} from '../ble-nordic-uart-service/actions';
-import {
- BlePybricksServiceCommandActionType,
- BlePybricksServiceCommandSendStopUserProgram,
didSendCommand,
+ sendStopUserProgramCommand,
} from '../ble-pybricks-service/actions';
-import { MpyActionType, didCompile } from '../mpy/actions';
+import { compile, didCompile } from '../mpy/actions';
import { createCountFunc } from '../utils/iter';
import {
- HubActionType,
- HubDidProgressDownloadAction,
checksum,
+ didFinishDownload,
+ didProgressDownload,
+ didStartDownload,
downloadAndRun,
repl,
stop,
@@ -41,49 +37,51 @@ describe('downloadAndRun', () => {
// first, it tries to compile the program in the current editor
const compileAction = await saga.take();
- expect(compileAction.type).toBe(MpyActionType.Compile);
+ expect(compileAction.type).toBe(compile.toString());
saga.put(didCompile(new Uint8Array(30)));
// then it notifies that loading has begun
const loadingStatusAction = await saga.take();
- expect(loadingStatusAction.type).toBe(HubActionType.DidStartDownload);
+ expect(loadingStatusAction.type).toBe(didStartDownload.toString());
// first message is the length
const writeAction = await saga.take();
- expect(writeAction.type).toBe(BleUartActionType.Write);
- expect((writeAction as BleUartWriteAction).value.length).toBe(4);
- saga.put(didWrite((writeAction as BleUartWriteAction).id));
+ expect(writeAction.type).toBe(write.toString());
+ expect((writeAction as ReturnType).value.length).toBe(4);
+ saga.put(didWrite((writeAction as ReturnType).id));
saga.put(checksum(30));
// then progress is updated
const progressAction = await saga.take();
- expect(progressAction.type).toBe(HubActionType.DidProgressDownload);
- expect((progressAction as HubDidProgressDownloadAction).progress).toBe(0);
+ expect(progressAction.type).toBe(didProgressDownload.toString());
+ expect(
+ (progressAction as ReturnType).progress,
+ ).toBe(0);
// then the first chunk of 20 bytes
const writeAction2 = await saga.take();
- expect(writeAction2.type).toBe(BleUartActionType.Write);
- expect((writeAction2 as BleUartWriteAction).value.length).toBe(20);
- saga.put(didWrite((writeAction2 as BleUartWriteAction).id));
+ expect(writeAction2.type).toBe(write.toString());
+ expect((writeAction2 as ReturnType).value.length).toBe(20);
+ saga.put(didWrite((writeAction2 as ReturnType).id));
saga.put(checksum(0));
// then progress is updated
const progress2Action = await saga.take();
- expect(progress2Action.type).toBe(HubActionType.DidProgressDownload);
- expect((progress2Action as HubDidProgressDownloadAction).progress).toBe(
- 20 / 30,
- );
+ expect(progress2Action.type).toBe(didProgressDownload.toString());
+ expect(
+ (progress2Action as ReturnType).progress,
+ ).toBe(20 / 30);
// then last chunk
const writeAction3 = await saga.take();
- expect(writeAction3.type).toBe(BleUartActionType.Write);
- expect((writeAction3 as BleUartWriteAction).value.length).toBe(10);
- saga.put(didWrite((writeAction3 as BleUartWriteAction).id));
+ expect(writeAction3.type).toBe(write.toString());
+ expect((writeAction3 as ReturnType).value.length).toBe(10);
+ saga.put(didWrite((writeAction3 as ReturnType).id));
saga.put(checksum(0));
// Then a status message saying that we are done
const loadedStatusAction = await saga.take();
- expect(loadedStatusAction.type).toBe(HubActionType.DidFinishDownload);
+ expect(loadedStatusAction.type).toBe(didFinishDownload.toString());
await saga.end();
});
@@ -97,7 +95,7 @@ test('repl', async () => {
saga.put(repl());
const action = await saga.take();
- expect(action.type).toBe(BleUartActionType.Write);
+ expect(action.type).toBe(write.toString());
await saga.end();
});
@@ -108,13 +106,11 @@ test('stop', async () => {
saga.put(stop());
const pybricksServiceAction = await saga.take();
- expect(pybricksServiceAction.type).toBe(
- BlePybricksServiceCommandActionType.SendStopUserProgram,
- );
+ expect(pybricksServiceAction.type).toBe(sendStopUserProgramCommand.toString());
saga.put(
didSendCommand(
- (pybricksServiceAction as BlePybricksServiceCommandSendStopUserProgram).id,
+ (pybricksServiceAction as ReturnType).id,
),
);
diff --git a/src/hub/sagas.ts b/src/hub/sagas.ts
index ac855a52..77ce0511 100644
--- a/src/hub/sagas.ts
+++ b/src/hub/sagas.ts
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
+import { AnyAction } from 'redux';
import {
SagaGenerator,
actionChannel,
@@ -12,60 +13,46 @@ import {
take,
takeEvery,
} from 'typed-redux-saga/macro';
-import { Action } from '../actions';
-import {
- BleUartActionType,
- BleUartDidFailToWriteAction,
- BleUartDidWriteAction,
- write,
-} from '../ble-nordic-uart-service/actions';
+import { didFailToWrite, didWrite, write } from '../ble-nordic-uart-service/actions';
import { SafeTxCharLength } from '../ble-nordic-uart-service/protocol';
import {
- BlePybricksServiceCommandActionType,
- BlePybricksServiceCommandDidFailToSendAction,
- BlePybricksServiceCommandDidSendAction,
+ didFailToSendCommand,
+ didSendCommand,
sendStopUserProgramCommand,
} from '../ble-pybricks-service/actions';
-import { BleDeviceActionType } from '../ble/actions';
-import {
- MpyActionType,
- MpyDidCompileAction,
- MpyDidFailToCompileAction,
- compile,
-} from '../mpy/actions';
+import { didConnect } from '../ble/actions';
+import { compile, didCompile, didFailToCompile } from '../mpy/actions';
import { RootState } from '../reducers';
import { defined } from '../utils';
import { xor8 } from '../utils/math';
import {
- HubActionType,
- HubChecksumMessageAction,
- HubDownloadAndRunAction,
- HubMessageActionType,
- HubReplAction,
- HubStopAction,
- didFailToFinishDownload as didFailToFinishDownload,
+ checksum,
+ didFailToFinishDownload,
didFinishDownload,
didProgressDownload,
didStartDownload,
+ downloadAndRun,
+ repl,
+ stop,
} from './actions';
const downloadChunkSize = 100;
function* waitForWrite(id: number): SagaGenerator<{
- didWrite: BleUartDidWriteAction | undefined;
- didFailToWrite: BleUartDidFailToWriteAction | undefined;
+ didWrite: ReturnType | undefined;
+ didFailToWrite: ReturnType | undefined;
}> {
return yield* race({
- didWrite: take(
- (a: Action) => a.type === BleUartActionType.DidWrite && a.id === id,
+ didWrite: take>(
+ (a: AnyAction) => didWrite.matches(a) && a.id === id,
),
- didFailToWrite: take(
- (a: Action) => a.type === BleUartActionType.DidFailToWrite && a.id === id,
+ didFailToWrite: take>(
+ (a: AnyAction) => didFailToWrite.matches(a) && a.id === id,
),
});
}
-function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
+function* handleDownloadAndRun(): Generator {
const editor = yield* select((s: RootState) => s.editor.current);
// istanbul ignore next: it is a bug to dispatch this action with no current editor
@@ -77,8 +64,8 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
const script = editor.getValue();
yield* put(compile(script, ['-mno-unicode']));
const { mpy, mpyFail } = yield* race({
- mpy: take(MpyActionType.DidCompile),
- mpyFail: take(MpyActionType.DidFailToCompile),
+ mpy: take>(didCompile),
+ mpyFail: take>(didFailToCompile),
});
if (mpyFail) {
@@ -95,9 +82,7 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
console.log(`Downloading ${mpy.data.byteLength} bytes`);
}
- const checksumChannel = yield* actionChannel(
- HubMessageActionType.Checksum,
- );
+ const checksumChannel = yield* actionChannel>(checksum);
const nextMessageId = yield* getContext<() => number>('nextMessageId');
@@ -183,26 +168,23 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
// SPACE, SPACE, SPACE, SPACE
const startReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
-function* startRepl(_action: HubReplAction): Generator {
+function* handleRepl(): Generator {
const nextMessageId = yield* getContext<() => number>('nextMessageId');
yield* put(write(nextMessageId(), startReplCommand));
}
-function* stop(_action: HubStopAction): Generator {
+function* handleStop(): Generator {
const nextMessageId = yield* getContext<() => number>('nextMessageId');
const id = nextMessageId();
yield* put(sendStopUserProgramCommand(id));
// REVISIT: may want to disable button while attempting to send command
// this would mean didSendStop() and didFailToSendStop() actions here
const { failedToSend } = yield* race({
- sent: take(
- (a: Action) =>
- a.type === BlePybricksServiceCommandActionType.DidSend && a.id === id,
+ sent: take>(
+ (a: AnyAction) => didSendCommand.matches(a) && a.id === id,
),
- failedToSend: take(
- (a: Action) =>
- a.type === BlePybricksServiceCommandActionType.DidFailToSend &&
- a.id === id,
+ failedToSend: take>(
+ (a: AnyAction) => didFailToSendCommand.matches(a) && a.id === id,
),
});
if (failedToSend) {
@@ -213,9 +195,9 @@ function* stop(_action: HubStopAction): Generator {
}
export default function* (): Generator {
- yield* takeEvery(HubActionType.DownloadAndRun, downloadAndRun);
- yield* takeEvery(HubActionType.Repl, startRepl);
- yield* takeEvery(HubActionType.Stop, stop);
+ yield* takeEvery(downloadAndRun, handleDownloadAndRun);
+ yield* takeEvery(repl, handleRepl);
+ yield* takeEvery(stop, handleStop);
// calling stop right after connecting should get the hub into a known state
- yield* takeEvery(BleDeviceActionType.DidConnect, stop);
+ yield* takeEvery(didConnect, handleStop);
}
diff --git a/src/licenses/actions.ts b/src/licenses/actions.ts
index 58b99521..5674e586 100644
--- a/src/licenses/actions.ts
+++ b/src/licenses/actions.ts
@@ -1,49 +1,24 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
-import { Action } from 'redux';
+import { createAction } from '../actions';
import { LicenseInfo, LicenseList } from './reducers';
-export enum LicenseActionType {
- FetchList = 'license.action.fetchList',
- DidFetchList = 'license.action.didFetchList',
- DidFailToFetchList = 'license.action.didFailToFetchList',
- Select = 'license.action.select',
-}
+export const fetchList = createAction(() => ({
+ type: 'license.action.fetchList',
+}));
-export type LicenseFetchListAction = Action;
+export const didFetchList = createAction((list: LicenseList) => ({
+ type: 'license.action.didFetchList',
+ list,
+}));
-export function fetchList(): LicenseFetchListAction {
- return { type: LicenseActionType.FetchList };
-}
+export const didFailToFetchList = createAction((reason: Response) => ({
+ type: 'license.action.didFailToFetchList',
+ reason,
+}));
-export type LicenseDidFetchListAction = Action & {
- list: LicenseList;
-};
-
-export function didFetchList(list: LicenseList): LicenseDidFetchListAction {
- return { type: LicenseActionType.DidFetchList, list };
-}
-
-export type LicenseDidFailToFetchListAction =
- Action & {
- reason: Response;
- };
-
-export function didFailToFetchList(reason: Response): LicenseDidFailToFetchListAction {
- return { type: LicenseActionType.DidFailToFetchList, reason };
-}
-
-export type LicenseSelectAction = Action & {
- info: LicenseInfo;
-};
-
-export function select(info: LicenseInfo): LicenseSelectAction {
- return { type: LicenseActionType.Select, info };
-}
-
-export type LicenseAction =
- | LicenseFetchListAction
- | LicenseDidFetchListAction
- | LicenseDidFailToFetchListAction
- | LicenseSelectAction;
+export const select = createAction((info: LicenseInfo) => ({
+ type: 'license.action.select',
+ info,
+}));
diff --git a/src/licenses/reducers.test.ts b/src/licenses/reducers.test.ts
index 1fbdfdae..dbbb9768 100644
--- a/src/licenses/reducers.test.ts
+++ b/src/licenses/reducers.test.ts
@@ -1,14 +1,14 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
-import { Action } from '../actions';
+import { AnyAction } from 'redux';
import { didFetchList, select } from './actions';
import reducers, { LicenseInfo, LicenseList } from './reducers';
type State = ReturnType;
test('initial state', () => {
- expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
+ expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
"list": null,
"selected": null,
diff --git a/src/licenses/reducers.ts b/src/licenses/reducers.ts
index 46501a8d..72489641 100644
--- a/src/licenses/reducers.ts
+++ b/src/licenses/reducers.ts
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
import { Reducer, combineReducers } from 'redux';
-import { Action } from '../actions';
-import { LicenseActionType } from './actions';
+import { didFetchList, select } from './actions';
export interface LicenseInfo {
readonly name: string;
@@ -15,22 +14,20 @@ export interface LicenseInfo {
export type LicenseList = LicenseInfo[];
-const list: Reducer = (state = null, action) => {
- switch (action.type) {
- case LicenseActionType.DidFetchList:
- return action.list;
- default:
- return state;
+const list: Reducer = (state = null, action) => {
+ if (didFetchList.matches(action)) {
+ return action.list;
}
+
+ return state;
};
-const selected: Reducer = (state = null, action) => {
- switch (action.type) {
- case LicenseActionType.Select:
- return action.info;
- default:
- return state;
+const selected: Reducer = (state = null, action) => {
+ if (select.matches(action)) {
+ return action.info;
}
+
+ return state;
};
export default combineReducers({ list, selected });
diff --git a/src/licenses/sagas.ts b/src/licenses/sagas.ts
index aaef64aa..7dff3cbc 100644
--- a/src/licenses/sagas.ts
+++ b/src/licenses/sagas.ts
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
import { call, put, select, takeEvery } from 'typed-redux-saga/macro';
import { RootState } from '../reducers';
-import { LicenseActionType, didFailToFetchList, didFetchList } from './actions';
+import { didFailToFetchList, didFetchList, fetchList } from './actions';
function* fetchLicenses(): Generator {
const licenses = yield* select((s: RootState) => s.licenses.list);
@@ -24,5 +24,5 @@ function* fetchLicenses(): Generator {
}
export default function* (): Generator {
- yield* takeEvery(LicenseActionType.FetchList, fetchLicenses);
+ yield* takeEvery(fetchList, fetchLicenses);
}
diff --git a/src/lwp3-bootloader/actions.ts b/src/lwp3-bootloader/actions.ts
index 9e1bbb01..b99d787f 100644
--- a/src/lwp3-bootloader/actions.ts
+++ b/src/lwp3-bootloader/actions.ts
@@ -1,79 +1,23 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { Action } from 'redux';
+import { createAction } from '../actions';
import { Command, HubType, ProtectionLevel, Result } from './protocol';
/**
- * Bootloader BLE connection actions.
+ * Initiate a connection.
*/
-export enum BootloaderConnectionActionType {
- /**
- * Initiate a connection.
- */
- Connect = 'bootloader.action.connection.connect',
- /**
- * The connection has been made.
- */
- DidConnect = 'bootloader.action.connection.did.connect',
- /**
- * The connection was not successful.
- */
- DidFailToConnect = 'bootloader.action.connection.did.connect.fail',
- /**
- * There was a connection error.
- */
- DidError = 'bootloader.action.connection.did.error',
- /**
- * Send a message using the connection.
- */
- Send = 'bootloader.action.connection.send',
- /**
- * Finished sending a message.
- */
- DidSend = 'bootloader.action.connection.did.send',
- /**
- * Sending a message failed with error.
- */
- DidFailToSend = 'bootloader.action.connection.did.failToSend',
- /**
- * The connection received a message.
- */
- DidReceive = 'bootloader.action.connection.did.receive',
- /**
- * Initiate disconnection/
- */
- Disconnect = 'bootloader.action.connection.disconnect',
- /**
- * The connection has been closed.
- */
- DidDisconnect = 'bootloader.action.connection.did.disconnect',
- /**
- * Disconnecting failed.
- */
- DidFailToDisconnect = 'bootloader.action.connection.did.failToDisconnect',
-}
+export const connect = createAction(() => ({
+ type: 'bootloader.action.connection.connect',
+}));
-export type BootloaderConnectionConnectAction =
- Action;
-
-export function connect(): BootloaderConnectionConnectAction {
- return { type: BootloaderConnectionActionType.Connect };
-}
-
-export type BootloaderConnectionDidConnectAction =
- Action;
-
-export function didConnect(): BootloaderConnectionDidConnectAction {
- return { type: BootloaderConnectionActionType.DidConnect };
-}
-
-export type BootloaderConnectionDisconnectAction =
- Action;
-
-export function disconnect(): BootloaderConnectionDisconnectAction {
- return { type: BootloaderConnectionActionType.Disconnect };
-}
+/**
+ * The connection has been made.
+ */
+export const didConnect = createAction(() => ({
+ type: 'bootloader.action.connection.didConnect',
+}));
/**
* Possible reasons a device could fail to connect.
@@ -91,476 +35,267 @@ export enum BootloaderConnectionFailureReason {
Unknown = 'unknown',
}
-type Reason = {
- reason: T;
+const didFailToConnectType = 'bootloader.action.connection.didFailToConnect';
+
+function didFailToConnectCreator(
+ reason: BootloaderConnectionFailureReason.NoWebBluetooth,
+): Action & {
+ reason: BootloaderConnectionFailureReason.NoWebBluetooth;
};
-export type BootloaderConnectionFailToConnectNoWebBluetoothReason =
- Reason;
+function didFailToConnectCreator(
+ reason: BootloaderConnectionFailureReason.NoBluetooth,
+): Action & {
+ reason: BootloaderConnectionFailureReason.NoBluetooth;
+};
-export type BootloaderConnectionFailToConnectNoBluetoothReason =
- Reason;
+function didFailToConnectCreator(
+ reason: BootloaderConnectionFailureReason.GattServiceNotFound,
+): Action & {
+ reason: BootloaderConnectionFailureReason.GattServiceNotFound;
+};
-export type BootloaderConnectionFailToConnectGattServiceNotFoundReason =
- Reason;
+function didFailToConnectCreator(
+ reason: BootloaderConnectionFailureReason.Canceled,
+): Action & {
+ reason: BootloaderConnectionFailureReason.Canceled;
+};
-export type BootloaderConnectionFailToConnectCanceledReason =
- Reason;
-
-export type BootloaderConnectionFailToConnectUnknownReason =
- Reason & {
- err: Error;
- };
-
-export type BootloaderConnectionDidFailToConnectReason =
- | BootloaderConnectionFailToConnectNoWebBluetoothReason
- | BootloaderConnectionFailToConnectNoBluetoothReason
- | BootloaderConnectionFailToConnectGattServiceNotFoundReason
- | BootloaderConnectionFailToConnectCanceledReason
- | BootloaderConnectionFailToConnectUnknownReason;
-
-export type BootloaderConnectionDidFailToConnectAction =
- Action &
- BootloaderConnectionDidFailToConnectReason;
-
-export function didFailToConnect(
- reason: Exclude<
- BootloaderConnectionFailureReason,
- BootloaderConnectionFailureReason.Unknown
- >,
-): BootloaderConnectionDidFailToConnectAction;
-
-export function didFailToConnect(
+function didFailToConnectCreator(
reason: BootloaderConnectionFailureReason.Unknown,
err: Error,
-): BootloaderConnectionDidFailToConnectAction;
+): Action & {
+ reason: BootloaderConnectionFailureReason.Unknown;
+ err: Error;
+};
-export function didFailToConnect(
+function didFailToConnectCreator(
+ reason: T,
+ arg1?: Error,
+): Action & {
+ reason: T;
+ err: T extends BootloaderConnectionFailureReason.Unknown ? Error : never;
+};
+
+function didFailToConnectCreator(
reason: BootloaderConnectionFailureReason,
arg1?: Error,
-): BootloaderConnectionDidFailToConnectAction {
+): Action & {
+ reason: BootloaderConnectionFailureReason;
+ err?: Error;
+} {
if (reason === BootloaderConnectionFailureReason.Unknown) {
- return {
- type: BootloaderConnectionActionType.DidFailToConnect,
+ return {
+ type: didFailToConnectType,
reason,
err: arg1,
};
}
- return { type: BootloaderConnectionActionType.DidFailToConnect, reason };
-}
-export type BootloaderConnectionDidErrorAction =
- Action & {
- err: Error;
- };
-
-export function didError(err: Error): BootloaderConnectionDidErrorAction {
- return { type: BootloaderConnectionActionType.DidError, err };
-}
-
-export type BootloaderConnectionSendAction =
- Action & {
- readonly data: ArrayBuffer;
- readonly withResponse: boolean;
- };
-
-export function send(
- data: ArrayBuffer,
- withResponse = true,
-): BootloaderConnectionSendAction {
- return { type: BootloaderConnectionActionType.Send, data, withResponse };
-}
-
-export type BootloaderConnectionDidSendAction =
- Action;
-
-export function didSend(): BootloaderConnectionDidSendAction {
- return { type: BootloaderConnectionActionType.DidSend };
-}
-
-export type BootloaderConnectionDidFailToSendAction =
- Action & {
- err: Error;
- };
-
-export function didFailToSend(err: Error): BootloaderConnectionDidFailToSendAction {
- return { type: BootloaderConnectionActionType.DidFailToSend, err };
-}
-
-export type BootloaderConnectionDidReceiveAction =
- Action & {
- data: DataView;
- };
-
-export function didReceive(data: DataView): BootloaderConnectionDidReceiveAction {
- return { type: BootloaderConnectionActionType.DidReceive, data };
-}
-
-export type BootloaderConnectionDidDisconnectAction =
- Action;
-
-export function didDisconnect(): BootloaderConnectionDidDisconnectAction {
- return { type: BootloaderConnectionActionType.DidDisconnect };
-}
-
-export type BootloaderConnectionDidFailToDisconnectAction =
- Action;
-
-export function didFailToDisconnect(): BootloaderConnectionDidFailToDisconnectAction {
- return { type: BootloaderConnectionActionType.DidFailToDisconnect };
+ return { type: didFailToConnectType, reason };
}
/**
- * Common type for all bootloader connection actions.
+ * The connection was not successful.
*/
-export type BootloaderConnectionAction =
- | BootloaderConnectionConnectAction
- | BootloaderConnectionDidConnectAction
- | BootloaderConnectionDidFailToConnectAction
- | BootloaderConnectionDidErrorAction
- | BootloaderConnectionSendAction
- | BootloaderConnectionDidSendAction
- | BootloaderConnectionDidFailToSendAction
- | BootloaderConnectionDidReceiveAction
- | BootloaderConnectionDisconnectAction
- | BootloaderConnectionDidDisconnectAction
- | BootloaderConnectionDidFailToDisconnectAction;
+export const didFailToConnect = createAction(didFailToConnectCreator);
/**
- * Bootloader request actions for sending commands over the connection.
+ * There was a connection error.
*/
-export enum BootloaderRequestActionType {
- Erase = 'bootloader.action.request.erase',
- Program = 'bootloader.action.request.program',
- Reboot = 'bootloader.action.request.reboot',
- Init = 'bootloader.action.request.init',
- Info = 'bootloader.action.request.info',
- Checksum = 'bootloader.action.request.checksum',
- State = 'bootloader.action.request.state',
- Disconnect = 'bootloader.action.request.disconnect',
-}
-
-type BaseBootloaderRequestAction = Action & {
- /**
- * Unique identifier for this action.
- */
- id: number;
-};
+export const didError = createAction((err: Error) => ({
+ type: 'bootloader.action.connection.didError',
+ err,
+}));
/**
- * Action that requests to erase the flash memory.
+ * Send a message using the connection.
*/
-export type BootloaderEraseRequestAction =
- BaseBootloaderRequestAction & {
- /* City hub requires special handling due to buggy bootloader */
- isCityHub: boolean;
- };
+export const send = createAction((data: ArrayBuffer, withResponse = true) => ({
+ type: 'bootloader.action.connection.send',
+ data,
+ withResponse,
+}));
+
+/**
+ * Finished sending a message.
+ */
+export const didSend = createAction(() => ({
+ type: 'bootloader.action.connection.didSend',
+}));
+
+/**
+ * Sending a message failed with error.
+ */
+export const didFailToSend = createAction((err: Error) => ({
+ type: 'bootloader.action.connection.didFailToSend',
+ err,
+}));
+
+/**
+ * The connection received a message.
+ */
+export const didReceive = createAction((data: DataView) => ({
+ type: 'bootloader.action.connection.didReceive',
+ data,
+}));
+
+/**
+ * Initiate disconnection.
+ */
+export const disconnect = createAction(() => ({
+ type: 'bootloader.action.connection.disconnect',
+}));
+
+/**
+ * The connection has been closed.
+ */
+export const didDisconnect = createAction(() => ({
+ type: 'bootloader.action.connection.didDisconnect',
+}));
+
+/**
+ * Disconnecting failed.
+ */
+export const didFailToDisconnect = createAction(() => ({
+ type: 'bootloader.action.connection.didFailToDisconnect',
+}));
+
+// LWP3 bootloader request message actions
/**
* Creates a request to erase the flash memory.
*/
-export function eraseRequest(
- id: number,
- isCityHub: boolean,
-): BootloaderEraseRequestAction {
- return { type: BootloaderRequestActionType.Erase, id, isCityHub };
-}
-
-/**
- * Action that requests to program the flash memory.
- */
-export type BootloaderProgramRequestAction =
- BaseBootloaderRequestAction & {
- address: number;
- payload: ArrayBuffer;
- };
+export const eraseRequest = createAction((id: number, isCityHub: boolean) => ({
+ type: 'bootloader.action.request.eraseRequest',
+ id,
+ isCityHub,
+}));
/**
* Creates a request to program the flash memory.
* @param address The starting address in the flash memory.
* @param payload The bytes to write (max 14 bytes!)
*/
-export function programRequest(
- id: number,
- address: number,
- payload: ArrayBuffer,
-): BootloaderProgramRequestAction {
- return {
- type: BootloaderRequestActionType.Program,
+export const programRequest = createAction(
+ (id: number, address: number, payload: ArrayBuffer) => ({
+ type: 'bootloader.action.request.programRequest',
id,
address,
payload,
- };
-}
-
-/**
- * Action that requests to reboot the hub.
- */
-export type BootloaderRebootRequestAction =
- BaseBootloaderRequestAction;
+ }),
+);
/**
* Creates a request to reboot the hub.
*/
-export function rebootRequest(id: number): BootloaderRebootRequestAction {
- return { type: BootloaderRequestActionType.Reboot, id };
-}
-
-/**
- * Action that requests to initialize the firmware flashing process.
- */
-export type BootloaderInitRequestAction =
- BaseBootloaderRequestAction & {
- firmwareSize: number;
- };
+export const rebootRequest = createAction((id: number) => ({
+ type: 'bootloader.action.request.rebootRequest',
+ id,
+}));
/**
* Creates a request to initialize the firmware flashing process.
* @param firmwareSize The size of the firmware to written to flash memory.
*/
-export function initRequest(
- id: number,
- firmwareSize: number,
-): BootloaderInitRequestAction {
- return {
- type: BootloaderRequestActionType.Init,
- id,
- firmwareSize,
- };
-}
-
-/**
- * Action that requests information about the hub.
- */
-export type BootloaderInfoRequestAction =
- BaseBootloaderRequestAction;
+export const initRequest = createAction((id: number, firmwareSize: number) => ({
+ type: 'bootloader.action.request.initRequest',
+ id,
+ firmwareSize,
+}));
/**
* Creates a request to get information about the hub.
*/
-export function infoRequest(id: number): BootloaderInfoRequestAction {
- return { type: BootloaderRequestActionType.Info, id };
-}
-
-/**
- * Action to get the checksum of the bytes that have been written to flash
- * so far.
- */
-export type BootloaderChecksumRequestAction =
- BaseBootloaderRequestAction;
+export const infoRequest = createAction((id: number) => ({
+ type: 'bootloader.action.request.infoRequest',
+ id,
+}));
/**
* Creates a request to get the checksum of the bytes that have been written
* to flash so far.
*/
-export function checksumRequest(id: number): BootloaderChecksumRequestAction {
- return { type: BootloaderRequestActionType.Checksum, id };
-}
-
-/**
- * Action that requests the bootloader flash memory protection state.
- */
-export type BootloaderStateRequestAction =
- BaseBootloaderRequestAction;
+export const checksumRequest = createAction((id: number) => ({
+ type: 'bootloader.action.request.checksumRequest',
+ id,
+}));
/**
* Creates a request to get the bootloader flash memory protection state.
*/
-export function stateRequest(id: number): BootloaderStateRequestAction {
- return { type: BootloaderRequestActionType.State, id };
-}
-
-/**
- * Action that requests to disconnect the hub.
- */
-export type BootloaderDisconnectRequestAction =
- BaseBootloaderRequestAction;
+export const stateRequest = createAction((id: number) => ({
+ type: 'bootloader.action.request.stateRequest',
+ id,
+}));
/**
* Creates a request to disconnect the hub.
*/
-export function disconnectRequest(id: number): BootloaderDisconnectRequestAction {
- return { type: BootloaderRequestActionType.Disconnect, id };
-}
-
-/**
- * Common type for all bootloader requests.
- */
-export type BootloaderRequestAction =
- | BootloaderEraseRequestAction
- | BootloaderProgramRequestAction
- | BootloaderRebootRequestAction
- | BootloaderInitRequestAction
- | BootloaderInfoRequestAction
- | BootloaderChecksumRequestAction
- | BootloaderStateRequestAction
- | BootloaderDisconnectRequestAction;
-
-/**
- * Action type for bootloader did request action.
- */
-export type BootloaderDidRequestType = 'bootloader.action.did.request';
-
-/**
- * Action type for bootloader did request action.
- */
-export const BootloaderDidRequestType = 'bootloader.action.did.request';
-
-/**
- * Action that indicates a request was sent.
- */
-export type BootloaderDidRequestAction = Action & {
- /**
- * The unique identifier of the action.
- */
- id: number;
-};
+export const disconnectRequest = createAction((id: number) => ({
+ type: 'bootloader.action.request.disconnectRequest',
+ id,
+}));
/**
* Creates an action that indicates a request was sent.
* @param id The unique identifier of the action.
*/
-export function didRequest(id: number): BootloaderDidRequestAction {
- return { type: BootloaderDidRequestType, id };
-}
-
-/**
- * Action type for bootloader did fail to request action.
- */
-export type BootloaderDidFailToRequestType = 'bootloader.action.did.failToRequest';
-
-/**
- * Action type for bootloader did fail to request action.
- */
-export const BootloaderDidFailToRequestType = 'bootloader.action.did.failToRequest';
-
-/**
- * Action that indicates a request failed to send.
- */
-export type BootloaderDidFailToRequestAction =
- Action & {
- /**
- * The unique identifier of the action.
- */
- id: number;
- /**
- * The error.
- */
- err: Error;
- };
+export const didRequest = createAction((id: number) => ({
+ type: 'bootloader.action.didRequest',
+ id,
+}));
/**
* Creates an action that indicates a request failed to send.
* @param id The unique identifier of the action.
* @param err The error message.
*/
-export function didFailToRequest(
- id: number,
- err: Error,
-): BootloaderDidFailToRequestAction {
- return { type: BootloaderDidFailToRequestType, id, err };
-}
+export const didFailToRequest = createAction((id: number, err: Error) => ({
+ type: 'bootloader.action.didFailToRequest',
+ id,
+ err,
+}));
-/**
- * Bootloader response actions for receiving responses from the connection.
- */
-export enum BootloaderResponseActionType {
- Erase = 'bootloader.action.response.erase',
- Program = 'bootloader.action.response.program',
- Init = 'bootloader.action.response.init',
- Info = 'bootloader.action.response.info',
- Checksum = 'bootloader.action.response.checksum',
- State = 'bootloader.action.response.state',
- Error = 'bootloader.action.response.error',
-}
+// Bootloader response actions for receiving responses from the connection.
-export type BootloaderEraseResponseAction =
- Action & {
- result: Result;
- };
+export const eraseResponse = createAction((result: Result) => ({
+ type: 'bootloader.action.response.eraseResponse',
+ result,
+}));
-export function eraseResponse(result: Result): BootloaderEraseResponseAction {
- return { type: BootloaderResponseActionType.Erase, result };
-}
+export const programResponse = createAction((checksum: number, count: number) => ({
+ type: 'bootloader.action.response.programResponse',
+ checksum,
+ count,
+}));
-export type BootloaderProgramResponseAction =
- Action & {
- checksum: number;
- count: number;
- };
+export const initResponse = createAction((result: Result) => ({
+ type: 'bootloader.action.response.initResponse',
+ result,
+}));
-export function programResponse(
- checksum: number,
- count: number,
-): BootloaderProgramResponseAction {
- return { type: BootloaderResponseActionType.Program, checksum, count };
-}
-
-export type BootloaderInitResponseAction = Action & {
- result: Result;
-};
-
-export function initResponse(result: Result): BootloaderInitResponseAction {
- return { type: BootloaderResponseActionType.Init, result };
-}
-
-export type BootloaderInfoResponseAction = Action & {
- version: number;
- startAddress: number;
- endAddress: number;
- hubType: HubType;
-};
-
-export function infoResponse(
- version: number,
- startAddress: number,
- endAddress: number,
- hubType: HubType,
-): BootloaderInfoResponseAction {
- return {
- type: BootloaderResponseActionType.Info,
+export const infoResponse = createAction(
+ (version: number, startAddress: number, endAddress: number, hubType: HubType) => ({
+ type: 'bootloader.action.response.infoResponse',
version,
startAddress,
endAddress,
hubType,
- };
-}
+ }),
+);
-export type BootloaderChecksumResponseAction =
- Action & {
- checksum: number;
- };
+export const checksumResponse = createAction((checksum: number) => ({
+ type: 'bootloader.action.response.checksumResponse',
+ checksum,
+}));
-export function checksumResponse(checksum: number): BootloaderChecksumResponseAction {
- return { type: BootloaderResponseActionType.Checksum, checksum };
-}
+export const stateResponse = createAction((level: ProtectionLevel) => ({
+ type: 'bootloader.action.response.stateResponse',
+ level,
+}));
-export type BootloaderStateResponseAction =
- Action & {
- level: ProtectionLevel;
- };
-
-export function stateResponse(level: ProtectionLevel): BootloaderStateResponseAction {
- return { type: BootloaderResponseActionType.State, level };
-}
-
-export type BootloaderErrorResponseAction =
- Action & {
- command: Command;
- };
-
-export function errorResponse(command: Command): BootloaderErrorResponseAction {
- return { type: BootloaderResponseActionType.Error, command };
-}
-
-/**
- * Common type for all bootloader response actions.
- */
-export type BootloaderResponseAction =
- | BootloaderEraseResponseAction
- | BootloaderProgramResponseAction
- | BootloaderInitResponseAction
- | BootloaderInfoResponseAction
- | BootloaderChecksumResponseAction
- | BootloaderStateResponseAction
- | BootloaderErrorResponseAction;
+export const errorResponse = createAction((command: Command) => ({
+ type: 'bootloader.action.response.errorResponse',
+ command,
+}));
diff --git a/src/lwp3-bootloader/reducers.test.ts b/src/lwp3-bootloader/reducers.test.ts
index 55801959..a04dcde0 100644
--- a/src/lwp3-bootloader/reducers.test.ts
+++ b/src/lwp3-bootloader/reducers.test.ts
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
-import { Action } from '../actions';
+import { AnyAction } from 'redux';
import {
BootloaderConnectionFailureReason,
connect,
@@ -18,7 +18,7 @@ import reducers, { BootloaderConnectionState } from './reducers';
type State = ReturnType;
test('initial state', () => {
- expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
+ expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
"connection": "bootloader.connection.disconnected",
}
diff --git a/src/lwp3-bootloader/reducers.ts b/src/lwp3-bootloader/reducers.ts
index a7aee859..770be010 100644
--- a/src/lwp3-bootloader/reducers.ts
+++ b/src/lwp3-bootloader/reducers.ts
@@ -1,9 +1,17 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020 The Pybricks Authors
+// Copyright (c) 2020,2022 The Pybricks Authors
import { Reducer, combineReducers } from 'redux';
-import { Action } from '../actions';
-import { BootloaderConnectionActionType, BootloaderRequestActionType } from './actions';
+import {
+ connect,
+ didConnect,
+ didDisconnect,
+ didFailToConnect,
+ didFailToDisconnect,
+ disconnect,
+ disconnectRequest,
+ rebootRequest,
+} from './actions';
/**
* Describes the state of the bootloader connection.
@@ -27,26 +35,31 @@ export enum BootloaderConnectionState {
Disconnecting = 'bootloader.connection.disconnecting',
}
-const connection: Reducer = (
+const connection: Reducer = (
state = BootloaderConnectionState.Disconnected,
action,
) => {
- switch (action.type) {
- case BootloaderConnectionActionType.Connect:
- return BootloaderConnectionState.Connecting;
- case BootloaderConnectionActionType.DidConnect:
- case BootloaderConnectionActionType.DidFailToDisconnect:
- return BootloaderConnectionState.Connected;
- case BootloaderConnectionActionType.Disconnect:
- case BootloaderRequestActionType.Reboot:
- case BootloaderRequestActionType.Disconnect:
- return BootloaderConnectionState.Disconnecting;
- case BootloaderConnectionActionType.DidDisconnect:
- case BootloaderConnectionActionType.DidFailToConnect:
- return BootloaderConnectionState.Disconnected;
- default:
- return state;
+ if (connect.matches(action)) {
+ return BootloaderConnectionState.Connecting;
}
+
+ if (didConnect.matches(action) || didFailToDisconnect.matches(action)) {
+ return BootloaderConnectionState.Connected;
+ }
+
+ if (
+ disconnect.matches(action) ||
+ rebootRequest.matches(action) ||
+ disconnectRequest.matches(action)
+ ) {
+ return BootloaderConnectionState.Disconnecting;
+ }
+
+ if (didDisconnect.matches(action) || didFailToConnect.matches(action)) {
+ return BootloaderConnectionState.Disconnected;
+ }
+
+ return state;
};
export default combineReducers({ connection });
diff --git a/src/lwp3-bootloader/sagas-ble.ts b/src/lwp3-bootloader/sagas-ble.ts
index 9b278f93..8b384736 100644
--- a/src/lwp3-bootloader/sagas-ble.ts
+++ b/src/lwp3-bootloader/sagas-ble.ts
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
//
// Handles Bluetooth Low Energy connection to LEGO Wireless Protocol v3 Bootloader service.
@@ -7,16 +7,16 @@ import { END, eventChannel } from 'redux-saga';
import { call, cancel, put, spawn, takeEvery, takeMaybe } from 'typed-redux-saga/macro';
import { ensureError } from '../utils';
import {
- BootloaderConnectionAction,
- BootloaderConnectionActionType,
- BootloaderConnectionSendAction,
BootloaderConnectionFailureReason as Reason,
+ connect,
didConnect,
didDisconnect,
didFailToConnect,
didFailToSend,
didReceive,
didSend,
+ disconnect,
+ send,
} from './actions';
import { CharacteristicUUID, ServiceUUID } from './protocol';
@@ -26,7 +26,7 @@ function* handleNotify(data: DataView): Generator {
function* write(
characteristic: BluetoothRemoteGATTCharacteristic,
- action: BootloaderConnectionSendAction,
+ action: ReturnType,
): Generator {
try {
if (action.withResponse) {
@@ -40,7 +40,7 @@ function* write(
}
}
-function* connect(_action: BootloaderConnectionAction): Generator {
+function* handleConnect(): Generator {
if (navigator.bluetooth === undefined) {
yield* put(didFailToConnect(Reason.NoWebBluetooth));
return;
@@ -155,16 +155,13 @@ function* connect(_action: BootloaderConnectionAction): Generator {
// Spawning write so that it can't be canceled. This is important because
// other sagas always expect it to complete with success action or error
// action.
- function* spawnWrite(action: BootloaderConnectionSendAction): Generator {
+ function* spawnWrite(action: ReturnType): Generator {
yield* spawn(write, characteristic, action);
}
yield* takeEvery(notificationChannel, handleNotify);
- yield* takeEvery(BootloaderConnectionActionType.Send, spawnWrite);
- yield* takeEvery(
- BootloaderConnectionActionType.Disconnect,
- server.disconnect.bind(server),
- );
+ yield* takeEvery(send, spawnWrite);
+ yield* takeEvery(disconnect, server.disconnect.bind(server));
yield* put(didConnect());
@@ -178,5 +175,5 @@ function* connect(_action: BootloaderConnectionAction): Generator {
}
export default function* (): Generator {
- yield* takeEvery(BootloaderConnectionActionType.Connect, connect);
+ yield* takeEvery(connect, handleConnect);
}
diff --git a/src/lwp3-bootloader/sagas.test.ts b/src/lwp3-bootloader/sagas.test.ts
index 4468888d..ef04807e 100644
--- a/src/lwp3-bootloader/sagas.test.ts
+++ b/src/lwp3-bootloader/sagas.test.ts
@@ -1,10 +1,9 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { AsyncSaga } from '../../test';
import { createCountFunc } from '../utils/iter';
import {
- BootloaderRequestActionType,
checksumRequest,
checksumResponse,
didError,
@@ -116,10 +115,10 @@ describe('message encoder', () => {
],
])('encode %s request', async (_n, request, expected) => {
const messageTypesThatShouldBeCalledWithoutResponse = [
- BootloaderRequestActionType.Erase,
- BootloaderRequestActionType.Program,
- BootloaderRequestActionType.Reboot,
- BootloaderRequestActionType.Disconnect,
+ eraseRequest.toString(),
+ programRequest.toString(),
+ rebootRequest.toString(),
+ disconnectRequest.toString(),
];
const saga = new AsyncSaga(bootloader);
saga.put(request);
diff --git a/src/lwp3-bootloader/sagas.ts b/src/lwp3-bootloader/sagas.ts
index 4e7267de..161b33c3 100644
--- a/src/lwp3-bootloader/sagas.ts
+++ b/src/lwp3-bootloader/sagas.ts
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020 The Pybricks Authors
+// Copyright (c) 2020,2022 The Pybricks Authors
//
// Handles LEGO Wireless Protocol v3 Bootloader protocol.
+import { AnyAction } from 'redux';
import {
actionChannel,
fork,
@@ -11,26 +12,30 @@ import {
take,
takeEvery,
} from 'typed-redux-saga/macro';
-import { Action } from '../actions';
import { ensureError, hex } from '../utils';
import { isWindows } from '../utils/os';
import {
- BootloaderConnectionActionType,
- BootloaderConnectionDidFailToSendAction,
- BootloaderConnectionDidReceiveAction,
- BootloaderConnectionDidSendAction,
- BootloaderRequestAction,
- BootloaderRequestActionType,
+ checksumRequest,
checksumResponse,
didError,
didFailToRequest,
+ didFailToSend,
+ didReceive,
didRequest,
+ didSend,
+ disconnectRequest,
+ eraseRequest,
eraseResponse,
errorResponse,
+ infoRequest,
infoResponse,
+ initRequest,
initResponse,
+ programRequest,
programResponse,
+ rebootRequest,
send,
+ stateRequest,
stateResponse,
} from './actions';
import {
@@ -62,10 +67,10 @@ import {
function* encodeRequest(): Generator {
// Using a while loop to serialize sending data to avoid "busy" errors.
- const chan = yield* actionChannel