diff --git a/src/actions/bootloader.ts b/src/actions/bootloader.ts index 463136fb..206aa134 100644 --- a/src/actions/bootloader.ts +++ b/src/actions/bootloader.ts @@ -1,5 +1,6 @@ import { Action } from 'redux'; import { Command, HubType, ProtectionLevel, Result } from '../protocols/bootloader'; +import { createCountFunc } from '../utils/iter'; /** * Bootloader BLE connection actions. @@ -128,68 +129,151 @@ export enum BootloaderRequestActionType { Disconnect = 'bootloader.action.request.disconnect', } -export type BootloaderEraseRequestAction = Action; +const nextRequestId = createCountFunc(); -export function eraseRequest(): BootloaderEraseRequestAction { - return { type: BootloaderRequestActionType.Erase }; +interface BaseBootloaderRequestAction + extends Action { + /** + * Unique identifier for this action. + */ + id: number; } +/** + * Action that requests to erase the flash memory. + */ +export type BootloaderEraseRequestAction = BaseBootloaderRequestAction< + BootloaderRequestActionType.Erase +>; + +/** + * Creates a request to erase the flash memory. + */ +export function eraseRequest(): BootloaderEraseRequestAction { + return { type: BootloaderRequestActionType.Erase, id: nextRequestId() }; +} + +/** + * Action that requests to program the flash memory. + */ export interface BootloaderProgramRequestAction - extends Action { + extends BaseBootloaderRequestAction { address: number; payload: ArrayBuffer; } +/** + * 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( address: number, payload: ArrayBuffer, ): BootloaderProgramRequestAction { - return { type: BootloaderRequestActionType.Program, address, payload }; + return { + type: BootloaderRequestActionType.Program, + id: nextRequestId(), + address, + payload, + }; } -export type BootloaderRebootRequestAction = Action; +/** + * Action that requests to reboot the hub. + */ +export type BootloaderRebootRequestAction = BaseBootloaderRequestAction< + BootloaderRequestActionType.Reboot +>; +/** + * Creates a request to reboot the hub. + */ export function rebootRequest(): BootloaderRebootRequestAction { - return { type: BootloaderRequestActionType.Reboot }; + return { type: BootloaderRequestActionType.Reboot, id: nextRequestId() }; } +/** + * Action that requests to initialize the firmware flashing process. + */ export interface BootloaderInitRequestAction - extends Action { + extends BaseBootloaderRequestAction { firmwareSize: number; } +/** + * Creates a request to initialize the firmware flashing process. + * @param firmwareSize The size of the firmware to written to flash memory. + */ export function initRequest(firmwareSize: number): BootloaderInitRequestAction { - return { type: BootloaderRequestActionType.Init, firmwareSize }; + return { + type: BootloaderRequestActionType.Init, + id: nextRequestId(), + firmwareSize, + }; } -export type BootloaderInfoRequestAction = Action; +/** + * Action that requests information about the hub. + */ +export type BootloaderInfoRequestAction = BaseBootloaderRequestAction< + BootloaderRequestActionType.Info +>; +/** + * Creates a request to get information about the hub. + */ export function infoRequest(): BootloaderInfoRequestAction { - return { type: BootloaderRequestActionType.Info }; + return { type: BootloaderRequestActionType.Info, id: nextRequestId() }; } -export type BootloaderChecksumRequestAction = Action< +/** + * Action to get the checksum of the bytes that have been written to flash + * so far. + */ +export type BootloaderChecksumRequestAction = BaseBootloaderRequestAction< BootloaderRequestActionType.Checksum >; +/** + * Creates a request to get the checksum of the bytes that have been written + * to flash so far. + */ export function checksumRequest(): BootloaderChecksumRequestAction { - return { type: BootloaderRequestActionType.Checksum }; + return { type: BootloaderRequestActionType.Checksum, id: nextRequestId() }; } -export type BootloaderStateRequestAction = Action; +/** + * Action that requests the bootloader flash memory protection state. + */ +export type BootloaderStateRequestAction = BaseBootloaderRequestAction< + BootloaderRequestActionType.State +>; +/** + * Creates a request to get the bootloader flash memory protection state. + */ export function stateRequest(): BootloaderStateRequestAction { - return { type: BootloaderRequestActionType.State }; + return { type: BootloaderRequestActionType.State, id: nextRequestId() }; } -export type BootloaderDisconnectRequestAction = Action< +/** + * Action that requests to disconnect the hub. + */ +export type BootloaderDisconnectRequestAction = BaseBootloaderRequestAction< BootloaderRequestActionType.Disconnect >; +/** + * Creates a request to disconnect the hub. + */ export function disconnectRequest(): BootloaderDisconnectRequestAction { - return { type: BootloaderRequestActionType.Disconnect }; + return { type: BootloaderRequestActionType.Disconnect, id: nextRequestId() }; } +/** + * Common type for all bootloader requests. + */ export type BootloaderRequestAction = | BootloaderEraseRequestAction | BootloaderProgramRequestAction @@ -200,6 +284,39 @@ export type BootloaderRequestAction = | 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 or failed to send. + */ +export interface BootloaderDidRequestAction extends Action { + /** + * The unique identifier of the action. + */ + id: number; + /** + * The error on failure or undefined on success. + */ + err?: Error; +} + +/** + * Creates an action that indicates a request was sent or failed to send. + * @param id The unique identifier of the action. + * @param err The error message on failure or undefined on success. + */ +export function didRequest(id: number, err?: Error): BootloaderDidRequestAction { + return { type: BootloaderDidRequestType, id, err }; +} + /** * Bootloader response actions for receiving responses from the connection. */ diff --git a/src/actions/notification.ts b/src/actions/notification.ts index e581acc5..5369f3af 100644 --- a/src/actions/notification.ts +++ b/src/actions/notification.ts @@ -1,4 +1,5 @@ import { Action } from 'redux'; +import { createCountFunc } from '../utils/iter'; export enum NotificationActionType { /** @@ -42,7 +43,7 @@ export interface NotificationRemoveAction export type NotificationAction = NotificationAddAction | NotificationRemoveAction; -let nextId = 0; +const nextId = createCountFunc(); /** * Action to add a notification to the list. @@ -55,7 +56,7 @@ export function add( message: string, helpUrl?: string, ): NotificationAddAction { - return { type: NotificationActionType.Add, id: nextId++, level, message, helpUrl }; + return { type: NotificationActionType.Add, id: nextId(), level, message, helpUrl }; } /** diff --git a/src/sagas/bootloader.test.ts b/src/sagas/bootloader.test.ts index a0d775d1..e9ee5f01 100644 --- a/src/sagas/bootloader.test.ts +++ b/src/sagas/bootloader.test.ts @@ -4,6 +4,7 @@ import { BootloaderChecksumRequestAction, BootloaderChecksumResponseAction, BootloaderConnectionActionType, + BootloaderDidRequestType, BootloaderDisconnectRequestAction, BootloaderEraseRequestAction, BootloaderEraseResponseAction, @@ -24,6 +25,7 @@ import { eraseRequest, } from '../actions/bootloader'; import { Command, HubType, ProtectionLevel, Result } from '../protocols/bootloader'; +import { createCountFunc } from '../utils/iter'; import bootloader from './bootloader'; describe('message encoder', () => { @@ -165,23 +167,37 @@ describe('message encoder', () => { channel.put(eraseRequest()); channel.put(eraseRequest()); - // but only one didSend action meaning only the first one completed + // but only two didSend action meaning only the first two completed + channel.put(didSend()); channel.put(didSend()); task.cancel(); await task.toPromise(); - // so only 2 messages were actually sent and 2 are still waiting for - // the second one to complete - expect(dispatched.length).toEqual(2); + // So only 3 requests were actually sent and two didRequests were + // dispatched (making 5 total dispatches). The last request is still + // buffered and has not been dispatched. + expect(dispatched.length).toEqual(5); + + // every other action is the "send" action const message = new Uint8Array([Command.EraseFlash]); - for (const d of dispatched) { - expect(d).toEqual({ + for (let i = 0; i < dispatched.length; i += 2) { + expect(dispatched[i]).toEqual({ type: BootloaderConnectionActionType.Send, data: message, withResponse: false, }); } + + // and the interleaving actions are "did request" actions + const nextId = createCountFunc(); + for (let i = 1; i < dispatched.length; i += 2) { + expect(dispatched[i]).toEqual({ + type: BootloaderDidRequestType, + id: nextId(), + err: undefined, + }); + } }); }); diff --git a/src/sagas/bootloader.ts b/src/sagas/bootloader.ts index 3d6f3c7c..def68f12 100644 --- a/src/sagas/bootloader.ts +++ b/src/sagas/bootloader.ts @@ -21,6 +21,7 @@ import { BootloaderConnectionDidConnectAction, BootloaderConnectionDidErrorAction, BootloaderConnectionDidReceiveAction, + BootloaderConnectionDidSendAction, BootloaderEraseResponseAction, BootloaderErrorResponseAction, BootloaderFlashFirmwareAction, @@ -33,6 +34,7 @@ import { checksumRequest, checksumResponse, connect, + didRequest, eraseRequest, eraseResponse, errorResponse, @@ -120,7 +122,10 @@ function* encodeRequest(): Generator { continue; } - yield take(BootloaderConnectionActionType.DidSend); + const sent = (yield take( + BootloaderConnectionActionType.DidSend, + )) as BootloaderConnectionDidSendAction; + yield put(didRequest(action.id, sent.err)); } } diff --git a/src/utils/iter.test.ts b/src/utils/iter.test.ts new file mode 100644 index 00000000..47430c72 --- /dev/null +++ b/src/utils/iter.test.ts @@ -0,0 +1,26 @@ +import { count, createCountFunc } from './iter'; + +test('count', () => { + let expected = 0; + for (const i of count()) { + expect(i).toBe(expected); + expected++; + if (expected > 10) { + break; + } + } +}); + +test('createCountFunc', () => { + const func = createCountFunc(); + expect(func()).toBe(0); + expect(func()).toBe(1); + expect(func()).toBe(2); + expect(func()).toBe(3); + expect(func()).toBe(4); + expect(func()).toBe(5); + expect(func()).toBe(6); + expect(func()).toBe(7); + expect(func()).toBe(8); + expect(func()).toBe(9); +}); diff --git a/src/utils/iter.ts b/src/utils/iter.ts new file mode 100644 index 00000000..e188af0c --- /dev/null +++ b/src/utils/iter.ts @@ -0,0 +1,19 @@ +/** + * Creates an iterator that counts infinitely (to Number.MAX_SAFE_INTEGER really) + * starting from 0. + */ +export function* count(): Generator { + let n = 0; + while (true) { + yield n++; + } +} + +/** + * Creates a new function that counts infinitely (to Number.MAX_SAFE_INTEGER really) + * starting from 0. + */ +export function createCountFunc(): () => number { + const gen = count(); + return (): number => gen.next().value; +}