mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
@@ -64,29 +64,6 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.`;
|
||||
}
|
||||
|
||||
const pybricksLicense = `MIT License
|
||||
|
||||
Copyright (c) 2018-2021 The Pybricks Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
`;
|
||||
|
||||
const shopifyLicense = `MIT License
|
||||
|
||||
Copyright (c) 2021 Shopify
|
||||
@@ -110,8 +87,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.`;
|
||||
|
||||
const licenseTextOverrides = {
|
||||
'@pybricks/firmware': pybricksLicense,
|
||||
'@pybricks/mpy-cross-v5': pybricksLicense,
|
||||
'@shopify/dates': shopifyLicense,
|
||||
'@shopify/decorators': shopifyLicense,
|
||||
'@shopify/function-enhancers': shopifyLicense,
|
||||
|
||||
+9
-2
@@ -10,8 +10,8 @@
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.36.0",
|
||||
"@craco/craco": "^6.0.0",
|
||||
"@pybricks/firmware": "4.4.0",
|
||||
"@pybricks/mpy-cross-v5": "^1.2.0",
|
||||
"@pybricks/firmware": "4.5.0",
|
||||
"@pybricks/mpy-cross-v5": "^2.0.0",
|
||||
"@shopify/react-i18n": "^5.2.0",
|
||||
"@testing-library/dom": "^7.29.2",
|
||||
"@testing-library/jest-dom": "^5.11.8",
|
||||
@@ -19,6 +19,7 @@
|
||||
"@testing-library/user-event": "^12.6.0",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/jszip": "^3.4.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
@@ -28,7 +29,9 @@
|
||||
"@types/web-bluetooth": "^0.0.9",
|
||||
"@types/zen-push": "^0.1.1",
|
||||
"ace-builds": "^1.4.12",
|
||||
"babel-plugin-macros": "^3.0.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"jszip": "^3.5.0",
|
||||
"license-webpack-plugin": "^2.3.11",
|
||||
"node-sass": "^4.14.1",
|
||||
"prop-types": "^15.7.2",
|
||||
@@ -43,6 +46,7 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-saga": "^1.1.3",
|
||||
"spdx-satisfies": "^5.0.0",
|
||||
"typed-redux-saga": "^1.3.1",
|
||||
"typescript": "~4.1.3",
|
||||
"web-vitals": "^1.0.1",
|
||||
"xterm": "^4.9.0",
|
||||
@@ -78,9 +82,12 @@
|
||||
"@typescript-eslint/parser": "^4.13.0",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-config-prettier": "^7.1.0",
|
||||
"eslint-config-typed-fp": "^1.3.0",
|
||||
"eslint-plugin-functional": "^3.2.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-total-functions": "^4.7.2",
|
||||
"jest-mock-extended": "^1.0.9",
|
||||
"prettier": "^2.2.1"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
// actions/ble-uart.ts: Actions for Bluetooth Low Energy nRF UART service
|
||||
|
||||
import { Action } from 'redux';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
|
||||
/**
|
||||
* BLE nRF UART service actions types.
|
||||
@@ -27,15 +26,13 @@ export enum BleUartActionType {
|
||||
Notify = 'ble.data.action.receive',
|
||||
}
|
||||
|
||||
const nextId = createCountFunc();
|
||||
|
||||
export type BleUartWriteAction = Action<BleUartActionType.Write> & {
|
||||
id: number;
|
||||
value: Uint8Array;
|
||||
};
|
||||
|
||||
export function write(value: Uint8Array): BleUartWriteAction {
|
||||
return { type: BleUartActionType.Write, id: nextId(), value };
|
||||
export function write(id: number, value: Uint8Array): BleUartWriteAction {
|
||||
return { type: BleUartActionType.Write, id, value };
|
||||
}
|
||||
|
||||
export type BleUartDidWriteAction = Action<BleUartActionType.DidWrite> & {
|
||||
|
||||
+299
-20
@@ -1,22 +1,138 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { FirmwareMetadata, FirmwareReaderError } from '@pybricks/firmware';
|
||||
import { Action } from 'redux';
|
||||
import { assert } from '../utils';
|
||||
|
||||
/**
|
||||
* High-level bootloader actions.
|
||||
*/
|
||||
export enum FlashFirmwareActionType {
|
||||
/**
|
||||
* Flash new firmware to the device.
|
||||
*/
|
||||
/** Request to flash new firmware to the device. */
|
||||
FlashFirmware = 'flashFirmware.action.flashFirmware',
|
||||
/**
|
||||
* Firmware flash progress.
|
||||
*/
|
||||
Progress = 'flashFirmware.action.progress',
|
||||
/** Flashing started. */
|
||||
DidStart = 'flashFirmware.action.didStart',
|
||||
/** Flashing was not able to start. */
|
||||
DidFailToStart = 'flashFirmware.action.didFailStart',
|
||||
/** Firmware flash progress. */
|
||||
DidProgress = 'flashFirmware.action.didProgress',
|
||||
/** Flashing finished successfully. */
|
||||
DidFinish = 'flashFirmware.action.didFinish',
|
||||
/** Flashing firmware failed. */
|
||||
DidFailToFinish = 'flashFirmware.action.didFailToFinish',
|
||||
}
|
||||
|
||||
export enum MetadataProblem {
|
||||
Missing = 'metadata.missing',
|
||||
NotSupported = 'metadata.notSupported',
|
||||
}
|
||||
|
||||
export enum HubError {
|
||||
UnknownCommand = 'hubError.unknownCommand',
|
||||
EraseFailed = 'hubError.eraseFailed',
|
||||
InitFailed = 'hubError.initFailed',
|
||||
CountMismatch = 'hubError.countMismatch',
|
||||
ChecksumMismatch = 'hubError.checksumMismatch',
|
||||
}
|
||||
|
||||
function isHubError(arg: unknown): arg is HubError {
|
||||
if (typeof arg !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return Object.keys(HubError).includes(arg);
|
||||
}
|
||||
|
||||
type Reason<T> = {
|
||||
reason: T;
|
||||
};
|
||||
|
||||
export enum FailToStartReasonType {
|
||||
/** Connecting to the hub failed. */
|
||||
FailedToConnect = 'flashFirmware.failToStart.reason.failedToConnect',
|
||||
/** The is no firmware available that matches the connected hub. */
|
||||
NoFirmware = 'flashFirmware.failToStart.reason.noFirmware',
|
||||
/** The provided firmware.zip does not match the connected hub. */
|
||||
DeviceMismatch = 'flashFirmware.failToStart.reason.deviceMismatch',
|
||||
/** There was a problem with the zip file. */
|
||||
ZipError = 'flashFirmware.failToStart.reason.zipError',
|
||||
/** Metadata property is missing or invalid. */
|
||||
BadMetadata = 'flashFirmware.failToStart.reason.badMetadata',
|
||||
/** The main.py file failed to compile. */
|
||||
FailedToCompile = 'flashFirmware.failToStart.reason.failedToCompile',
|
||||
/** The combined firmware-base.bin and main.mpy are too big. */
|
||||
FirmwareSize = 'flashFirmware.failToStart.reason.firmwareSize',
|
||||
/** An unexpected error occurred. */
|
||||
Unknown = 'flashFirmware.failToStart.reason.unknown',
|
||||
}
|
||||
|
||||
export type FailToStartReasonFailedToConnect = Reason<FailToStartReasonType.FailedToConnect>;
|
||||
|
||||
export type FailToStartReasonNoFirmware = Reason<FailToStartReasonType.NoFirmware>;
|
||||
|
||||
export type FailToStartReasonDeviceMismatch = Reason<FailToStartReasonType.DeviceMismatch>;
|
||||
|
||||
export type FailToStartReasonZipError = Reason<FailToStartReasonType.ZipError> & {
|
||||
err: FirmwareReaderError;
|
||||
};
|
||||
|
||||
export type FailToStartReasonBadMetadata = Reason<FailToStartReasonType.BadMetadata> & {
|
||||
property: keyof FirmwareMetadata;
|
||||
problem: MetadataProblem;
|
||||
};
|
||||
|
||||
export type FailToStartReasonFirmwareSize = Reason<FailToStartReasonType.FirmwareSize>;
|
||||
|
||||
export type FailToStartReasonFailedToCompile = Reason<FailToStartReasonType.FailedToCompile>;
|
||||
|
||||
export type FailToStartReasonUnknown = Reason<FailToStartReasonType.Unknown> & {
|
||||
err: Error;
|
||||
};
|
||||
|
||||
export type FailToStartReason =
|
||||
| FailToStartReasonFailedToConnect
|
||||
| FailToStartReasonNoFirmware
|
||||
| FailToStartReasonDeviceMismatch
|
||||
| FailToStartReasonZipError
|
||||
| FailToStartReasonBadMetadata
|
||||
| FailToStartReasonFirmwareSize
|
||||
| FailToStartReasonFailedToCompile
|
||||
| FailToStartReasonUnknown;
|
||||
|
||||
export enum FailToFinishReasonType {
|
||||
/** Waiting for a response from the hub took too long. */
|
||||
TimedOut = 'flashFirmware.failToFinish.reason.timedOut',
|
||||
/** Something went wrong with the BLE connection. */
|
||||
BleError = 'flashFirmware.failToFinish.reason.bleError',
|
||||
/** The BLE connection was lost before flashing completed. */
|
||||
Disconnected = 'flashFirmware.failToFinish.reason.disconnected',
|
||||
/** The hub sent a response indicating a problem. */
|
||||
HubError = 'flashFirmware.failToFinish.reason.hubError',
|
||||
/** An unexpected error occurred. */
|
||||
Unknown = 'flashFirmware.failToFinish.reason.unknown',
|
||||
}
|
||||
|
||||
export type FailToFinishReasonTimedOut = Reason<FailToFinishReasonType.TimedOut>;
|
||||
|
||||
export type FailToFinishReasonBleError = Reason<FailToFinishReasonType.BleError>;
|
||||
|
||||
export type FailToFinishReasonDisconnected = Reason<FailToFinishReasonType.Disconnected>;
|
||||
|
||||
export type FailToFinishReasonHubError = Reason<FailToFinishReasonType.HubError> & {
|
||||
hubError: HubError;
|
||||
};
|
||||
|
||||
export type FailToFinishReasonUnknown = Reason<FailToFinishReasonType.Unknown> & {
|
||||
err: Error;
|
||||
};
|
||||
|
||||
export type FailToFinishReason =
|
||||
| FailToFinishReasonTimedOut
|
||||
| FailToFinishReasonBleError
|
||||
| FailToFinishReasonDisconnected
|
||||
| FailToFinishReasonHubError
|
||||
| FailToFinishReasonUnknown;
|
||||
|
||||
/**
|
||||
* Action that flashes firmware to a hub.
|
||||
*/
|
||||
@@ -33,19 +149,178 @@ export function flashFirmware(data?: ArrayBuffer): FlashFirmwareFlashAction {
|
||||
return { type: FlashFirmwareActionType.FlashFirmware, data };
|
||||
}
|
||||
|
||||
export type FlashFirmwareProgressAction = Action<FlashFirmwareActionType.Progress> & {
|
||||
/**
|
||||
* The number of bytes that have been flashed so far.
|
||||
*/
|
||||
complete: number;
|
||||
/**
|
||||
* The total number of bytes to be flashed.
|
||||
*/
|
||||
total: number;
|
||||
/** Action that indicates flashing firmware started. */
|
||||
export type FlashFirmwareDidStartAction = Action<FlashFirmwareActionType.DidStart>;
|
||||
|
||||
/**
|
||||
* 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 flashing did not start because of an error. */
|
||||
export type FlashFirmwareDidFailToStartAction = Action<FlashFirmwareActionType.DidFailToStart> & {
|
||||
reason: FailToStartReason;
|
||||
};
|
||||
|
||||
export function progress(complete: number, total: number): FlashFirmwareProgressAction {
|
||||
return { type: FlashFirmwareActionType.Progress, complete, total };
|
||||
export function didFailToStart(
|
||||
reason: FailToStartReasonType.ZipError,
|
||||
err: FirmwareReaderError,
|
||||
): FlashFirmwareDidFailToStartAction;
|
||||
|
||||
export function didFailToStart(
|
||||
reason: FailToStartReasonType.BadMetadata,
|
||||
property: keyof FirmwareMetadata,
|
||||
problem: MetadataProblem,
|
||||
): FlashFirmwareDidFailToStartAction;
|
||||
|
||||
export function didFailToStart(
|
||||
reason: FailToStartReasonType.Unknown,
|
||||
err: Error,
|
||||
): FlashFirmwareDidFailToStartAction;
|
||||
|
||||
export function didFailToStart(
|
||||
reason: Exclude<
|
||||
FailToStartReasonType,
|
||||
| FailToStartReasonType.ZipError
|
||||
| FailToStartReasonType.BadMetadata
|
||||
| FailToStartReasonType.Unknown
|
||||
>,
|
||||
): FlashFirmwareDidFailToStartAction;
|
||||
|
||||
/**
|
||||
* Action that indicates flashing did not start because of an error.
|
||||
* @param total The total number of bytes to be flashed.
|
||||
*/
|
||||
export function didFailToStart(
|
||||
reason: FailToStartReasonType,
|
||||
arg1?: string | Error,
|
||||
arg2?: MetadataProblem,
|
||||
): FlashFirmwareDidFailToStartAction {
|
||||
if (reason === FailToStartReasonType.ZipError) {
|
||||
// istanbul ignore if: programmer error give wrong arg
|
||||
if (!(arg1 instanceof FirmwareReaderError)) {
|
||||
throw new Error('missing or invalid err');
|
||||
}
|
||||
return {
|
||||
type: FlashFirmwareActionType.DidFailToStart,
|
||||
reason: { reason, err: arg1 },
|
||||
};
|
||||
}
|
||||
|
||||
if (reason === FailToStartReasonType.BadMetadata) {
|
||||
// istanbul ignore if: programmer error give wrong arg
|
||||
if (
|
||||
arg1 !== 'metadata-version' &&
|
||||
arg1 !== 'firmware-version' &&
|
||||
arg1 !== 'device-id' &&
|
||||
arg1 !== 'checksum-type' &&
|
||||
arg1 !== 'mpy-abi-version' &&
|
||||
arg1 !== 'mpy-cross-options' &&
|
||||
arg1 !== 'user-mpy-offset' &&
|
||||
arg1 !== 'max-firmware-size'
|
||||
) {
|
||||
throw new Error('missing or invalid property');
|
||||
}
|
||||
// istanbul ignore if: programmer error give wrong arg
|
||||
if (arg2 === undefined) {
|
||||
throw new Error('missing or invalid problem');
|
||||
}
|
||||
return {
|
||||
type: FlashFirmwareActionType.DidFailToStart,
|
||||
reason: { reason, property: arg1, problem: arg2 },
|
||||
};
|
||||
}
|
||||
|
||||
if (reason === FailToStartReasonType.Unknown) {
|
||||
// istanbul ignore if: programmer error give wrong arg
|
||||
if (!(arg1 instanceof Error)) {
|
||||
throw new Error('missing or invalid err');
|
||||
}
|
||||
return {
|
||||
type: FlashFirmwareActionType.DidFailToStart,
|
||||
reason: { reason, err: arg1 },
|
||||
};
|
||||
}
|
||||
|
||||
return { type: FlashFirmwareActionType.DidFailToStart, reason: { reason } };
|
||||
}
|
||||
|
||||
/** Action that indicates current firmware flashing progress. */
|
||||
export type FlashFirmwareDidProgressAction = Action<FlashFirmwareActionType.DidProgress> & {
|
||||
/** The current progress (0 to 1). */
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
||||
/** Action that indicates that flashing firmware completed successfully. */
|
||||
export type FlashFirmwareDidFinishAction = Action<FlashFirmwareActionType.DidFinish>;
|
||||
|
||||
/** Action that indicates that flashing firmware completed successfully. */
|
||||
export function didFinish(): FlashFirmwareDidFinishAction {
|
||||
return { type: FlashFirmwareActionType.DidFinish };
|
||||
}
|
||||
|
||||
/** Action that indicates that flashing failed. */
|
||||
export type FlashFirmwareDidFailToFinishAction = Action<FlashFirmwareActionType.DidFailToFinish> & {
|
||||
reason: FailToFinishReason;
|
||||
};
|
||||
|
||||
export function didFailToFinish(
|
||||
reason: FailToFinishReasonType.HubError,
|
||||
hubError: HubError,
|
||||
): FlashFirmwareDidFailToFinishAction;
|
||||
|
||||
export function didFailToFinish(
|
||||
reason: FailToFinishReasonType.Unknown,
|
||||
err: Error,
|
||||
): FlashFirmwareDidFailToFinishAction;
|
||||
|
||||
export function didFailToFinish(
|
||||
reason: Exclude<
|
||||
FailToFinishReasonType,
|
||||
FailToFinishReasonType.HubError | FailToFinishReasonType.Unknown
|
||||
>,
|
||||
): FlashFirmwareDidFailToFinishAction;
|
||||
|
||||
/** Action that indicates that flashing failed. */
|
||||
export function didFailToFinish(
|
||||
reason: FailToFinishReasonType,
|
||||
arg1?: HubError | Error,
|
||||
): FlashFirmwareDidFailToFinishAction {
|
||||
if (reason === FailToFinishReasonType.HubError) {
|
||||
// istanbul ignore if: programmer error give wrong arg
|
||||
if (!isHubError(arg1)) {
|
||||
throw new Error('missing or invalid err');
|
||||
}
|
||||
return {
|
||||
type: FlashFirmwareActionType.DidFailToFinish,
|
||||
reason: { reason, hubError: arg1 },
|
||||
};
|
||||
}
|
||||
|
||||
if (reason === FailToFinishReasonType.Unknown) {
|
||||
// istanbul ignore if: programmer error give wrong arg
|
||||
if (!(arg1 instanceof Error)) {
|
||||
throw new Error('missing or invalid err');
|
||||
}
|
||||
return {
|
||||
type: FlashFirmwareActionType.DidFailToFinish,
|
||||
reason: { reason, err: arg1 },
|
||||
};
|
||||
}
|
||||
|
||||
return { type: FlashFirmwareActionType.DidFailToFinish, reason: { reason } };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,4 +328,8 @@ export function progress(complete: number, total: number): FlashFirmwareProgress
|
||||
*/
|
||||
export type FlashFirmwareAction =
|
||||
| FlashFirmwareFlashAction
|
||||
| FlashFirmwareProgressAction;
|
||||
| FlashFirmwareDidStartAction
|
||||
| FlashFirmwareDidFailToStartAction
|
||||
| FlashFirmwareDidProgressAction
|
||||
| FlashFirmwareDidFinishAction
|
||||
| FlashFirmwareDidFailToFinishAction;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Action } from 'redux';
|
||||
import {
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ProtectionLevel,
|
||||
Result,
|
||||
} from '../protocols/lwp3-bootloader';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
|
||||
/**
|
||||
* Bootloader BLE connection actions.
|
||||
@@ -64,26 +63,63 @@ export function didConnect(): BootloaderConnectionDidConnectAction {
|
||||
* Possible reasons a device could fail to connect.
|
||||
*/
|
||||
export enum BootloaderConnectionFailureReason {
|
||||
/** The reason is not known */
|
||||
Unknown = 'unknown',
|
||||
/** The connection was canceled */
|
||||
Canceled = 'canceled',
|
||||
/** Web Bluetooth is not available */
|
||||
NoWebBluetooth = 'no-web-bluetooth',
|
||||
/** Connected but failed to find the bootloader GATT service */
|
||||
GattServiceNotFound = 'gatt-service-not-found',
|
||||
/** The connection was canceled */
|
||||
Canceled = 'canceled',
|
||||
/** The reason is not known */
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export type BootloaderConnectionDidFailToConnectAction = Action<BootloaderConnectionActionType.DidFailToConnect> & {
|
||||
reason: BootloaderConnectionFailureReason;
|
||||
err?: Error;
|
||||
type Reason<T extends BootloaderConnectionFailureReason> = {
|
||||
reason: T;
|
||||
};
|
||||
|
||||
export type BootloaderConnectionFailToConnectNoWebBluetoothReason = Reason<BootloaderConnectionFailureReason.NoWebBluetooth>;
|
||||
|
||||
export type BootloaderConnectionFailToConnectGattServiceNotFoundReason = Reason<BootloaderConnectionFailureReason.GattServiceNotFound>;
|
||||
|
||||
export type BootloaderConnectionFailToConnectCanceledReason = Reason<BootloaderConnectionFailureReason.Canceled>;
|
||||
|
||||
export type BootloaderConnectionFailToConnectUnknownReason = Reason<BootloaderConnectionFailureReason.Unknown> & {
|
||||
err: Error;
|
||||
};
|
||||
|
||||
export type BootloaderConnectionDidFailToConnectReason =
|
||||
| BootloaderConnectionFailToConnectNoWebBluetoothReason
|
||||
| BootloaderConnectionFailToConnectGattServiceNotFoundReason
|
||||
| BootloaderConnectionFailToConnectCanceledReason
|
||||
| BootloaderConnectionFailToConnectUnknownReason;
|
||||
|
||||
export type BootloaderConnectionDidFailToConnectAction = Action<BootloaderConnectionActionType.DidFailToConnect> &
|
||||
BootloaderConnectionDidFailToConnectReason;
|
||||
|
||||
export function didFailToConnect(
|
||||
reason: Exclude<
|
||||
BootloaderConnectionFailureReason,
|
||||
BootloaderConnectionFailureReason.Unknown
|
||||
>,
|
||||
): BootloaderConnectionDidFailToConnectAction;
|
||||
|
||||
export function didFailToConnect(
|
||||
reason: BootloaderConnectionFailureReason.Unknown,
|
||||
err: Error,
|
||||
): BootloaderConnectionDidFailToConnectAction;
|
||||
|
||||
export function didFailToConnect(
|
||||
reason: BootloaderConnectionFailureReason,
|
||||
err?: Error,
|
||||
): BootloaderConnectionDidFailToConnectAction {
|
||||
return { type: BootloaderConnectionActionType.DidFailToConnect, reason, err };
|
||||
if (reason === BootloaderConnectionFailureReason.Unknown) {
|
||||
return <BootloaderConnectionDidFailToConnectAction>{
|
||||
type: BootloaderConnectionActionType.DidFailToConnect,
|
||||
reason,
|
||||
err,
|
||||
};
|
||||
}
|
||||
return { type: BootloaderConnectionActionType.DidFailToConnect, reason };
|
||||
}
|
||||
|
||||
export type BootloaderConnectionDidErrorAction = Action<BootloaderConnectionActionType.DidError> & {
|
||||
@@ -156,8 +192,6 @@ export enum BootloaderRequestActionType {
|
||||
Disconnect = 'bootloader.action.request.disconnect',
|
||||
}
|
||||
|
||||
const nextRequestId = createCountFunc();
|
||||
|
||||
type BaseBootloaderRequestAction<T extends BootloaderRequestActionType> = Action<T> & {
|
||||
/**
|
||||
* Unique identifier for this action.
|
||||
@@ -173,8 +207,8 @@ export type BootloaderEraseRequestAction = BaseBootloaderRequestAction<Bootloade
|
||||
/**
|
||||
* Creates a request to erase the flash memory.
|
||||
*/
|
||||
export function eraseRequest(): BootloaderEraseRequestAction {
|
||||
return { type: BootloaderRequestActionType.Erase, id: nextRequestId() };
|
||||
export function eraseRequest(id: number): BootloaderEraseRequestAction {
|
||||
return { type: BootloaderRequestActionType.Erase, id };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,12 +225,13 @@ export type BootloaderProgramRequestAction = BaseBootloaderRequestAction<Bootloa
|
||||
* @param payload The bytes to write (max 14 bytes!)
|
||||
*/
|
||||
export function programRequest(
|
||||
id: number,
|
||||
address: number,
|
||||
payload: ArrayBuffer,
|
||||
): BootloaderProgramRequestAction {
|
||||
return {
|
||||
type: BootloaderRequestActionType.Program,
|
||||
id: nextRequestId(),
|
||||
id,
|
||||
address,
|
||||
payload,
|
||||
};
|
||||
@@ -210,8 +245,8 @@ export type BootloaderRebootRequestAction = BaseBootloaderRequestAction<Bootload
|
||||
/**
|
||||
* Creates a request to reboot the hub.
|
||||
*/
|
||||
export function rebootRequest(): BootloaderRebootRequestAction {
|
||||
return { type: BootloaderRequestActionType.Reboot, id: nextRequestId() };
|
||||
export function rebootRequest(id: number): BootloaderRebootRequestAction {
|
||||
return { type: BootloaderRequestActionType.Reboot, id };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,10 +260,13 @@ export type BootloaderInitRequestAction = BaseBootloaderRequestAction<Bootloader
|
||||
* 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 {
|
||||
export function initRequest(
|
||||
id: number,
|
||||
firmwareSize: number,
|
||||
): BootloaderInitRequestAction {
|
||||
return {
|
||||
type: BootloaderRequestActionType.Init,
|
||||
id: nextRequestId(),
|
||||
id,
|
||||
firmwareSize,
|
||||
};
|
||||
}
|
||||
@@ -241,8 +279,8 @@ export type BootloaderInfoRequestAction = BaseBootloaderRequestAction<Bootloader
|
||||
/**
|
||||
* Creates a request to get information about the hub.
|
||||
*/
|
||||
export function infoRequest(): BootloaderInfoRequestAction {
|
||||
return { type: BootloaderRequestActionType.Info, id: nextRequestId() };
|
||||
export function infoRequest(id: number): BootloaderInfoRequestAction {
|
||||
return { type: BootloaderRequestActionType.Info, id };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,8 +293,8 @@ export type BootloaderChecksumRequestAction = BaseBootloaderRequestAction<Bootlo
|
||||
* 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, id: nextRequestId() };
|
||||
export function checksumRequest(id: number): BootloaderChecksumRequestAction {
|
||||
return { type: BootloaderRequestActionType.Checksum, id };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,8 +305,8 @@ export type BootloaderStateRequestAction = BaseBootloaderRequestAction<Bootloade
|
||||
/**
|
||||
* Creates a request to get the bootloader flash memory protection state.
|
||||
*/
|
||||
export function stateRequest(): BootloaderStateRequestAction {
|
||||
return { type: BootloaderRequestActionType.State, id: nextRequestId() };
|
||||
export function stateRequest(id: number): BootloaderStateRequestAction {
|
||||
return { type: BootloaderRequestActionType.State, id };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,8 +317,8 @@ export type BootloaderDisconnectRequestAction = BaseBootloaderRequestAction<Boot
|
||||
/**
|
||||
* Creates a request to disconnect the hub.
|
||||
*/
|
||||
export function disconnectRequest(): BootloaderDisconnectRequestAction {
|
||||
return { type: BootloaderRequestActionType.Disconnect, id: nextRequestId() };
|
||||
export function disconnectRequest(id: number): BootloaderDisconnectRequestAction {
|
||||
return { type: BootloaderRequestActionType.Disconnect, id };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -32,10 +32,10 @@ export function didCompile(data: Uint8Array): MpyDidCompileAction {
|
||||
|
||||
export type MpyDidFailToCompileAction = Action<MpyActionType.DidFailToCompile> & {
|
||||
/** Error output. */
|
||||
readonly err: string;
|
||||
readonly err: string[];
|
||||
};
|
||||
|
||||
export function didFailToCompile(err: string): MpyDidFailToCompileAction {
|
||||
export function didFailToCompile(err: string[]): MpyDidFailToCompileAction {
|
||||
return { type: MpyActionType.DidFailToCompile, err };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from '../actions';
|
||||
@@ -11,12 +11,18 @@ import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton';
|
||||
import { TooltipId } from './button-i18n';
|
||||
import firmwareIcon from './images/firmware.svg';
|
||||
|
||||
type StateProps = Pick<OpenFileButtonProps, 'enabled'>;
|
||||
type StateProps = Pick<
|
||||
OpenFileButtonProps,
|
||||
'tooltip' | 'enabled' | 'showProgress' | 'progress'
|
||||
>;
|
||||
type DispatchProps = Pick<OpenFileButtonProps, 'onFile' | 'onReject' | 'onClick'>;
|
||||
type OwnProps = Pick<OpenFileButtonProps, 'id'>;
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
tooltip: state.firmware.flashing ? TooltipId.FlashProgress : TooltipId.Flash,
|
||||
enabled: state.bootloader.connection === BootloaderConnectionState.Disconnected,
|
||||
showProgress: state.firmware.flashing,
|
||||
progress: state.firmware.progress === null ? undefined : state.firmware.progress,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
@@ -39,7 +45,6 @@ const mergeProps = (
|
||||
ownProps: OwnProps,
|
||||
): OpenFileButtonProps => ({
|
||||
fileExtension: '.zip',
|
||||
tooltip: TooltipId.Flash,
|
||||
icon: firmwareIcon,
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// provides translation for notification text
|
||||
|
||||
import { Replacements, useI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { MessageId } from './notification-i18n';
|
||||
import en from './notification-i18n.en.json';
|
||||
|
||||
// provides translation for notification text
|
||||
|
||||
type OwnProps = {
|
||||
messageId: MessageId;
|
||||
replacements?: Replacements;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
|
||||
import { Button, Intent, Position, Spinner, Tooltip } from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import Dropzone, { FileRejection } from 'react-dropzone';
|
||||
@@ -20,6 +20,10 @@ export interface OpenFileButtonProps {
|
||||
readonly icon: string;
|
||||
/** When true or undefined, the button is enabled. */
|
||||
readonly enabled?: boolean;
|
||||
/** Show progress spinner instead of icon. */
|
||||
readonly showProgress?: boolean;
|
||||
/** The progress value (0 to 1) for the progress spinner. */
|
||||
readonly progress?: number;
|
||||
/** Callback that is called when a file has been selected and opened for reading. */
|
||||
readonly onFile: (data: ArrayBuffer) => void;
|
||||
/** Callback that is called when a file has been rejected (e.g. bad file extension). */
|
||||
@@ -80,7 +84,19 @@ class OpenFileButton extends React.Component<Props> {
|
||||
>
|
||||
{({ getRootProps, getInputProps }): JSX.Element => (
|
||||
<Tooltip
|
||||
content={this.props.i18n.translate(this.props.tooltip)}
|
||||
content={this.props.i18n.translate(
|
||||
this.props.tooltip,
|
||||
this.props.tooltip === TooltipId.FlashProgress
|
||||
? {
|
||||
percent:
|
||||
this.props.progress === undefined
|
||||
? ''
|
||||
: this.props.i18n.formatPercentage(
|
||||
this.props.progress,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
position={Position.BOTTOM}
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
@@ -102,7 +118,14 @@ class OpenFileButton extends React.Component<Props> {
|
||||
: {})}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<img src={this.props.icon} alt={this.props.id} />
|
||||
{this.props.showProgress ? (
|
||||
<Spinner
|
||||
value={this.props.progress}
|
||||
intent={Intent.PRIMARY}
|
||||
/>
|
||||
) : (
|
||||
<img src={this.props.icon} alt={this.props.id} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Action, Dispatch } from '../actions';
|
||||
import { closeSettings, openAboutDialog } from '../actions/app';
|
||||
import { setBoolean } from '../actions/settings';
|
||||
import { RootState } from '../reducers';
|
||||
import { pseudolocalize } from '../settings/i18n';
|
||||
import {
|
||||
pybricksBugReportsUrl,
|
||||
pybricksGitterUrl,
|
||||
@@ -206,6 +207,15 @@ class SettingsDrawer extends React.PureComponent<SettingsProps> {
|
||||
<AboutDialog />
|
||||
</ButtonGroup>
|
||||
</FormGroup>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<FormGroup label="Developer">
|
||||
<Switch
|
||||
checked={i18n.pseudolocalize !== false}
|
||||
onClick={() => pseudolocalize(!i18n.pseudolocalize)}
|
||||
label="Pseudolocalize"
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { ProgressBar } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { RootState } from '../reducers';
|
||||
|
||||
import './status-bar.scss';
|
||||
|
||||
type StateProps = { progress: number };
|
||||
|
||||
type StatusProps = StateProps;
|
||||
|
||||
class StatusBar extends React.Component<StatusProps> {
|
||||
class StatusBar extends React.Component {
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div className="status-bar" onContextMenu={(e): void => e.preventDefault()}>
|
||||
<ProgressBar
|
||||
className="status-bar-item"
|
||||
value={this.props.progress}
|
||||
animate={false}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="status-bar"
|
||||
onContextMenu={(e): void => e.preventDefault()}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
progress: state.status.progress,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(StatusBar);
|
||||
export default connect()(StatusBar);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// Provides special notification contents for unexpected errors.
|
||||
|
||||
import { AnchorButton, Button, ButtonGroup, Intent } from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { MessageId } from './notification-i18n';
|
||||
import en from './notification-i18n.en.json';
|
||||
|
||||
type OwnProps = {
|
||||
messageId: MessageId;
|
||||
err: Error;
|
||||
};
|
||||
|
||||
export default function UnexpectedErrorNotification(props: OwnProps): JSX.Element {
|
||||
const [i18n] = useI18n({
|
||||
id: 'notification',
|
||||
translations: { en },
|
||||
fallback: en,
|
||||
});
|
||||
const { messageId, err } = props;
|
||||
return (
|
||||
<>
|
||||
<p>{i18n.translate(messageId, { errorMessage: err.message })}</p>
|
||||
<div>
|
||||
<ButtonGroup minimal={true} fill={true}>
|
||||
<Button
|
||||
intent={Intent.DANGER}
|
||||
icon="duplicate"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`\`\`\`\n${err.stack || err.message}\n\`\`\``,
|
||||
)
|
||||
}
|
||||
>
|
||||
{i18n.translate(MessageId.CopyErrorMessage)}
|
||||
</Button>
|
||||
<AnchorButton
|
||||
intent={Intent.DANGER}
|
||||
icon="virus"
|
||||
href={`https://github.com/pybricks/support/issues?q=${encodeURIComponent(
|
||||
'is:issue',
|
||||
)}+${encodeURIComponent(err.message)}`}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(MessageId.ReportBug)}
|
||||
</AnchorButton>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"connect": { "tooltip": "Connect using Bluetooth" },
|
||||
"disconnect": { "tooltip": "Disconnect Bluetooth" }
|
||||
},
|
||||
"flash": { "tooltip": "Install Pybricks firmware" },
|
||||
"flash": {
|
||||
"action": { "tooltip": "Install Pybricks firmware" },
|
||||
"progress": { "tooltip": "Flashing… {percent}" }
|
||||
},
|
||||
"settings": { "tooltip": "Settings" }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
// File: components/button-i18n.ts
|
||||
// Button translation keys.
|
||||
|
||||
@@ -9,7 +9,8 @@ export enum TooltipId {
|
||||
Run = 'run.tooltip',
|
||||
Stop = 'stop.tooltip',
|
||||
Repl = 'repl.tooltip',
|
||||
Flash = 'flash.tooltip',
|
||||
Flash = 'flash.action.tooltip',
|
||||
FlashProgress = 'flash.progress.tooltip',
|
||||
BluetoothConnect = 'bluetooth.connect.tooltip',
|
||||
BluetoothDisconnect = 'bluetooth.disconnect.tooltip',
|
||||
Settings = 'settings.tooltip',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"copyErrorMessage": "Copy Error Message",
|
||||
"reportBug": "Report Bug",
|
||||
"ble": {
|
||||
"gattPermission": "The web browser did not give permission to use Bluetooth Low Energy",
|
||||
"gattServiceNotFound": "Connected to hub but failed to get {serviceName} service. Try removing the \"{hubName}\" device in your OS Bluetooth settings, then try again.",
|
||||
"noWebBluetooth": "This web browser does not support Web Bluetooth or it is not enabled.",
|
||||
"connectFailed": "Unexpected error while trying to connect. Check console log and report the error."
|
||||
"unexpectedError": "Unexpected error while trying to connect: {errorMessage}"
|
||||
},
|
||||
"editor": {
|
||||
"programChanged": {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
// Notification translation keys.
|
||||
|
||||
export enum MessageId {
|
||||
BleConnectFailed = 'ble.connectFailed',
|
||||
CopyErrorMessage = 'copyErrorMessage',
|
||||
ReportBug = 'reportBug',
|
||||
BleUnexpectedError = 'ble.unexpectedError',
|
||||
BleGattPermission = 'ble.gattPermission',
|
||||
BleGattServiceNotFound = 'ble.gattServiceNotFound',
|
||||
BleNoWebBluetooth = 'ble.noWebBluetooth',
|
||||
|
||||
@@ -14,13 +14,3 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-bar-item {
|
||||
width: 25%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.#{$ns}-progress-bar.status-bar-item {
|
||||
// override progress bar default gray1 backgound
|
||||
background-color: $pt-app-background-color;
|
||||
}
|
||||
|
||||
+11
-8
@@ -2,7 +2,7 @@
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Classes, ResizeSensor } from '@blueprintjs/core';
|
||||
import { I18nContext, I18nManager } from '@shopify/react-i18n';
|
||||
import { I18nContext } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
@@ -17,15 +17,18 @@ import rootReducer from './reducers';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import rootSaga from './sagas';
|
||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||
import { i18nManager } from './settings/i18n';
|
||||
import { createCountFunc } from './utils/iter';
|
||||
|
||||
const i18n = new I18nManager({
|
||||
locale: 'en',
|
||||
onError: (err): void => console.error(err),
|
||||
const toaster = I18nToaster.create(i18nManager);
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware({
|
||||
context: {
|
||||
nextMessageId: createCountFunc(),
|
||||
notification: { toaster },
|
||||
},
|
||||
});
|
||||
|
||||
const toaster = I18nToaster.create(i18n);
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware({ context: { notification: { toaster } } });
|
||||
// TODO: add runtime option or filter - logger affects firmware flash performance
|
||||
const loggerMiddleware = createLogger({ predicate: () => false });
|
||||
|
||||
@@ -58,7 +61,7 @@ sagaMiddleware.run(rootSaga);
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<I18nContext.Provider value={i18n}>
|
||||
<I18nContext.Provider value={i18nManager}>
|
||||
{/* This is a hack for correctly sizing to view height on mobile when not running in fullscreen mode. */}
|
||||
{/* https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ */}
|
||||
<ResizeSensor
|
||||
|
||||
@@ -6,9 +6,7 @@ import { Reducer, combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { EditorActionType } from '../actions/editor';
|
||||
|
||||
type CurrentEditSession = Ace.EditSession | null;
|
||||
|
||||
const current: Reducer<CurrentEditSession, Action> = (state = null, action) => {
|
||||
const current: Reducer<Ace.EditSession | null, Action> = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case EditorActionType.Current:
|
||||
return action.editSession || null;
|
||||
@@ -18,6 +16,6 @@ const current: Reducer<CurrentEditSession, Action> = (state = null, action) => {
|
||||
};
|
||||
|
||||
export interface EditorState {
|
||||
current: CurrentEditSession;
|
||||
current: Ace.EditSession | null;
|
||||
}
|
||||
export default combineReducers({ current });
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { FlashFirmwareActionType } from '../actions/flash-firmware';
|
||||
|
||||
export interface FirmwareState {
|
||||
/** The firmware is being erased/flashed right now. */
|
||||
flashing: boolean;
|
||||
/** The current progress (0 to 1) or null for unknown (e.g erasing) */
|
||||
progress: number | null;
|
||||
}
|
||||
|
||||
const flashing: Reducer<boolean, Action> = (state = false, action) => {
|
||||
switch (action.type) {
|
||||
case FlashFirmwareActionType.DidStart:
|
||||
return true;
|
||||
case FlashFirmwareActionType.DidFinish:
|
||||
case FlashFirmwareActionType.DidFailToFinish:
|
||||
return false;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const progress: Reducer<number | null, Action> = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case FlashFirmwareActionType.DidStart:
|
||||
return null;
|
||||
case FlashFirmwareActionType.DidProgress:
|
||||
return action.value;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({ flashing, progress });
|
||||
@@ -1,15 +1,15 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { combineReducers } from 'redux';
|
||||
import app, { AppState } from './app';
|
||||
import ble, { BleState } from './ble';
|
||||
import bootloader, { BootloaderState } from './bootloader';
|
||||
import editor, { EditorState } from './editor';
|
||||
import firmware, { FirmwareState } from './firmware';
|
||||
import hub, { HubState } from './hub';
|
||||
import license, { LicenseState } from './license';
|
||||
import settings, { SettingsState } from './settings';
|
||||
import status, { StatusState } from './status';
|
||||
import terminal, { TerminalState } from './terminal';
|
||||
|
||||
/**
|
||||
@@ -20,10 +20,10 @@ export interface RootState {
|
||||
readonly bootloader: BootloaderState;
|
||||
readonly ble: BleState;
|
||||
readonly editor: EditorState;
|
||||
readonly firmware: FirmwareState;
|
||||
readonly hub: HubState;
|
||||
readonly license: LicenseState;
|
||||
readonly settings: SettingsState;
|
||||
readonly status: StatusState;
|
||||
readonly terminal: TerminalState;
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ export default combineReducers({
|
||||
bootloader,
|
||||
ble,
|
||||
editor,
|
||||
firmware,
|
||||
hub,
|
||||
license,
|
||||
settings,
|
||||
status,
|
||||
terminal,
|
||||
});
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Reducer } from 'react';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { FlashFirmwareActionType } from '../actions/flash-firmware';
|
||||
|
||||
const progress: Reducer<number, Action> = (state = -1, action) => {
|
||||
switch (action.type) {
|
||||
case FlashFirmwareActionType.Progress:
|
||||
return action.complete / action.total;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export interface StatusState {
|
||||
readonly progress: number;
|
||||
}
|
||||
|
||||
export default combineReducers({ progress });
|
||||
@@ -0,0 +1,31 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`flashFirmware normal flow 1`] = `
|
||||
Object {
|
||||
"options": Array [
|
||||
"-mno-unicode",
|
||||
],
|
||||
"script": "print(\\"test\\")",
|
||||
"type": "mpy.action.compile",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`flashFirmware user supplied firmware.zip success 1`] = `
|
||||
Object {
|
||||
"options": Array [
|
||||
"-mno-unicode",
|
||||
],
|
||||
"script": "print(\\"test\\")",
|
||||
"type": "mpy.action.compile",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`flashFirmware user supplied main.py 1`] = `
|
||||
Object {
|
||||
"options": Array [
|
||||
"-mno-unicode",
|
||||
],
|
||||
"script": "print(\\"test\\")",
|
||||
"type": "mpy.action.compile",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`compiler error works 1`] = `
|
||||
Array [
|
||||
"Traceback (most recent call last):",
|
||||
" File \\"main.py\\", line 1",
|
||||
"SyntaxError: invalid syntax",
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { AsyncSaga, delay } from '../../test';
|
||||
import { reload } from '../actions/app';
|
||||
import app from './app';
|
||||
|
||||
test('reload', async () => {
|
||||
const saga = new AsyncSaga(app);
|
||||
|
||||
// mock registration as if service worker was register on app startup
|
||||
const registration: Partial<ServiceWorkerRegistration> = {
|
||||
unregister: jest.fn(),
|
||||
};
|
||||
|
||||
// @ts-expect-error: navigator.serviceWorker is not implemented in JSDOM
|
||||
navigator.serviceWorker = {
|
||||
getRegistrations: jest.fn().mockResolvedValue([registration]),
|
||||
};
|
||||
|
||||
// @ts-expect-error: JSDOM implementation of location.reload() causes error
|
||||
delete window.location;
|
||||
// @ts-expect-error: JSDOM implementation of location.reload() causes error
|
||||
window.location = {
|
||||
reload: jest.fn(),
|
||||
};
|
||||
|
||||
saga.put(reload());
|
||||
|
||||
// yield to allow generators to complete
|
||||
await delay(0);
|
||||
|
||||
expect(registration.unregister).toHaveBeenCalled();
|
||||
expect(location.reload).toHaveBeenCalled();
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
@@ -5,8 +5,6 @@ import { call, takeEvery } from 'redux-saga/effects';
|
||||
import { AppActionType } from '../actions/app';
|
||||
|
||||
function* reload(): Generator {
|
||||
console.log('reload');
|
||||
|
||||
// unregister the service worker so that when the page reloads, it uses
|
||||
// the new version
|
||||
const registrations = (yield call(() =>
|
||||
|
||||
@@ -49,12 +49,8 @@ test('bleDataDidFailToWrite', async () => {
|
||||
test('bootloaderDidFailToConnect', async () => {
|
||||
const saga = new AsyncSaga(errorLog);
|
||||
|
||||
console.debug = jest.fn();
|
||||
saga.put(didFailToConnect(BootloaderConnectionFailureReason.Canceled));
|
||||
expect(console.debug).toHaveBeenCalledTimes(1);
|
||||
|
||||
console.error = jest.fn();
|
||||
saga.put(didFailToConnect(BootloaderConnectionFailureReason.Unknown));
|
||||
saga.put(didFailToConnect(BootloaderConnectionFailureReason.Unknown, <Error>{}));
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
|
||||
await saga.end();
|
||||
|
||||
@@ -31,8 +31,6 @@ function bootloaderDidFailToConnect(
|
||||
): void {
|
||||
if (action.reason === BootloaderConnectionFailureReason.Unknown) {
|
||||
console.error(action.err);
|
||||
} else {
|
||||
console.debug(action.err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import {
|
||||
FirmwareMetadata,
|
||||
FirmwareReaderError,
|
||||
FirmwareReaderErrorCode,
|
||||
} from '@pybricks/firmware';
|
||||
import JSZip from 'jszip';
|
||||
import { AsyncSaga } from '../../test';
|
||||
import {
|
||||
FailToStartReasonType,
|
||||
didFailToStart,
|
||||
didFinish,
|
||||
didProgress,
|
||||
didStart,
|
||||
flashFirmware as flashFirmwareAction,
|
||||
} from '../actions/flash-firmware';
|
||||
import {
|
||||
BootloaderProgramRequestAction,
|
||||
checksumRequest,
|
||||
checksumResponse,
|
||||
connect,
|
||||
didConnect,
|
||||
didRequest,
|
||||
eraseRequest,
|
||||
eraseResponse,
|
||||
infoRequest,
|
||||
infoResponse,
|
||||
initRequest,
|
||||
initResponse,
|
||||
programRequest,
|
||||
programResponse,
|
||||
rebootRequest,
|
||||
} from '../actions/lwp3-bootloader';
|
||||
import { didCompile } from '../actions/mpy';
|
||||
import { HubType, Result } from '../protocols/lwp3-bootloader';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
import flashFirmware from './flash-firmware';
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('flashFirmware', () => {
|
||||
test('normal flow', async () => {
|
||||
const metadata: FirmwareMetadata = {
|
||||
'metadata-version': '1.0.0',
|
||||
'device-id': HubType.MoveHub,
|
||||
'checksum-type': 'sum',
|
||||
'firmware-version': '1.2.3',
|
||||
'max-firmware-size': 1024,
|
||||
'mpy-abi-version': 5,
|
||||
'mpy-cross-options': ['-mno-unicode'],
|
||||
'user-mpy-offset': 100,
|
||||
};
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('firmware-base.bin', new Uint8Array(64));
|
||||
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
||||
zip.file('main.py', 'print("test")');
|
||||
zip.file('ReadMe_OSS.txt', 'test');
|
||||
|
||||
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
||||
new Response(await zip.generateAsync({ type: 'blob' })),
|
||||
);
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.setState({ settings: { flashCurrentProgram: false } });
|
||||
|
||||
// saga is triggered by this action
|
||||
|
||||
saga.put(flashFirmwareAction());
|
||||
|
||||
// first step is to connect to the hub bootloader
|
||||
|
||||
let action = await saga.take();
|
||||
expect(action).toEqual(connect());
|
||||
|
||||
saga.put(didConnect());
|
||||
|
||||
// then find out what kind of hub it is
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(infoRequest(0));
|
||||
|
||||
saga.put(didRequest(0));
|
||||
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
||||
|
||||
// then compile main.py to .mpy
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toMatchSnapshot();
|
||||
|
||||
const mpySize = 20;
|
||||
const mpyBinaryData = new Uint8Array(mpySize);
|
||||
saga.put(didCompile(mpyBinaryData));
|
||||
|
||||
// then start flashing the firmware
|
||||
|
||||
// should get didStart action just before starting to erase
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didStart());
|
||||
|
||||
// erase first
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(eraseRequest(1));
|
||||
|
||||
saga.put(didRequest(1));
|
||||
saga.put(eraseResponse(Result.OK));
|
||||
|
||||
// then write the new firmware
|
||||
|
||||
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
||||
|
||||
saga.put(didRequest(2));
|
||||
saga.put(initResponse(Result.OK));
|
||||
|
||||
const dummyPayload = new ArrayBuffer(0);
|
||||
let id = 2;
|
||||
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(
|
||||
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
||||
);
|
||||
expect((action as BootloaderProgramRequestAction).payload.byteLength).toBe(
|
||||
Math.min(14, totalFirmwareSize - offset),
|
||||
);
|
||||
|
||||
saga.put(didRequest(id));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
||||
|
||||
// Have to be careful that a checksum request is not sent after
|
||||
// last payload is sent, otherwise the hub gets confused.
|
||||
|
||||
if (offset + 14 >= totalFirmwareSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (count % 10 === 0) {
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(checksumRequest(++id));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
saga.put(checksumResponse(0));
|
||||
}
|
||||
}
|
||||
|
||||
// hub indicates success
|
||||
|
||||
saga.put(programResponse(0, totalFirmwareSize));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didProgress(1));
|
||||
|
||||
// and finally reboot the hub
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(rebootRequest(++id));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
|
||||
// then we are done
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didFinish());
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
describe('user supplied firmware.zip', () => {
|
||||
test('success', async () => {
|
||||
const metadata: FirmwareMetadata = {
|
||||
'metadata-version': '1.0.0',
|
||||
'device-id': HubType.MoveHub,
|
||||
'checksum-type': 'sum',
|
||||
'firmware-version': '1.2.3',
|
||||
'max-firmware-size': 1024,
|
||||
'mpy-abi-version': 5,
|
||||
'mpy-cross-options': ['-mno-unicode'],
|
||||
'user-mpy-offset': 100,
|
||||
};
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('firmware-base.bin', new Uint8Array(64));
|
||||
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
||||
zip.file('main.py', 'print("test")');
|
||||
zip.file('ReadMe_OSS.txt', 'test');
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
});
|
||||
|
||||
saga.setState({ settings: { flashCurrentProgram: false } });
|
||||
|
||||
// saga is triggered by this action
|
||||
|
||||
saga.put(
|
||||
flashFirmwareAction(await zip.generateAsync({ type: 'arraybuffer' })),
|
||||
);
|
||||
|
||||
// the first step is to compile main.py to .mpy
|
||||
|
||||
let action = await saga.take();
|
||||
expect(action).toMatchSnapshot();
|
||||
|
||||
const mpySize = 20;
|
||||
const mpyBinaryData = new Uint8Array(mpySize);
|
||||
saga.put(didCompile(mpyBinaryData));
|
||||
|
||||
// then connect to the hub bootloader
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(connect());
|
||||
|
||||
saga.put(didConnect());
|
||||
|
||||
// then find out what kind of hub it is
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(infoRequest(0));
|
||||
|
||||
saga.put(didRequest(0));
|
||||
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
||||
|
||||
// then start flashing the firmware
|
||||
|
||||
// should get didStart action just before starting to erase
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didStart());
|
||||
|
||||
// erase first
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(eraseRequest(1));
|
||||
|
||||
saga.put(didRequest(1));
|
||||
saga.put(eraseResponse(Result.OK));
|
||||
|
||||
// then write the new firmware
|
||||
|
||||
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
||||
|
||||
saga.put(didRequest(2));
|
||||
saga.put(initResponse(Result.OK));
|
||||
|
||||
const dummyPayload = new ArrayBuffer(0);
|
||||
let id = 2;
|
||||
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(
|
||||
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
||||
);
|
||||
expect(
|
||||
(action as BootloaderProgramRequestAction).payload.byteLength,
|
||||
).toBe(Math.min(14, totalFirmwareSize - offset));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
||||
|
||||
// Have to be careful that a checksum request is not sent after
|
||||
// last payload is sent, otherwise the hub gets confused.
|
||||
|
||||
if (offset + 14 >= totalFirmwareSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (count % 10 === 0) {
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(checksumRequest(++id));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
saga.put(checksumResponse(0));
|
||||
}
|
||||
}
|
||||
|
||||
// hub indicates success
|
||||
|
||||
saga.put(programResponse(0, totalFirmwareSize));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didProgress(1));
|
||||
|
||||
// and finally reboot the hub
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(rebootRequest(++id));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
|
||||
// then we are done
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didFinish());
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('zip error', async () => {
|
||||
const metadata: FirmwareMetadata = {
|
||||
'metadata-version': '1.0.0',
|
||||
'device-id': HubType.MoveHub,
|
||||
'checksum-type': 'sum',
|
||||
'firmware-version': '1.2.3',
|
||||
'max-firmware-size': 1024,
|
||||
'mpy-abi-version': 5,
|
||||
'mpy-cross-options': ['-mno-unicode'],
|
||||
'user-mpy-offset': 100,
|
||||
};
|
||||
|
||||
const zip = new JSZip();
|
||||
// no firmware-base.bin - triggers zip error
|
||||
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
||||
zip.file('main.py', 'print("test")');
|
||||
zip.file('ReadMe_OSS.txt', 'test');
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
});
|
||||
|
||||
saga.setState({ settings: { flashCurrentProgram: false } });
|
||||
|
||||
// saga is triggered by this action
|
||||
|
||||
saga.put(
|
||||
flashFirmwareAction(await zip.generateAsync({ type: 'arraybuffer' })),
|
||||
);
|
||||
|
||||
// should get failure due to missing file
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toStrictEqual(
|
||||
didFailToStart(
|
||||
FailToStartReasonType.ZipError,
|
||||
new FirmwareReaderError(
|
||||
FirmwareReaderErrorCode.MissingFirmwareBaseBin,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('user supplied main.py', async () => {
|
||||
const metadata: FirmwareMetadata = {
|
||||
'metadata-version': '1.0.0',
|
||||
'device-id': HubType.MoveHub,
|
||||
'checksum-type': 'sum',
|
||||
'firmware-version': '1.2.3',
|
||||
'max-firmware-size': 1024,
|
||||
'mpy-abi-version': 5,
|
||||
'mpy-cross-options': ['-mno-unicode'],
|
||||
'user-mpy-offset': 100,
|
||||
};
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('firmware-base.bin', new Uint8Array(64));
|
||||
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
||||
zip.file('main.py', 'print("test")');
|
||||
zip.file('ReadMe_OSS.txt', 'test');
|
||||
|
||||
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
||||
new Response(await zip.generateAsync({ type: 'blob' })),
|
||||
);
|
||||
|
||||
const editor = {
|
||||
getValue: () => 'print("test")',
|
||||
};
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.setState({
|
||||
editor: { current: editor },
|
||||
settings: { flashCurrentProgram: true },
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
|
||||
saga.put(flashFirmwareAction());
|
||||
|
||||
// first step is to connect to the hub bootloader
|
||||
|
||||
let action = await saga.take();
|
||||
expect(action).toEqual(connect());
|
||||
|
||||
saga.put(didConnect());
|
||||
|
||||
// then find out what kind of hub it is
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(infoRequest(0));
|
||||
|
||||
saga.put(didRequest(0));
|
||||
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
||||
|
||||
// then compile main.py to .mpy
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toMatchSnapshot();
|
||||
|
||||
const mpySize = 20;
|
||||
const mpyBinaryData = new Uint8Array(mpySize);
|
||||
saga.put(didCompile(mpyBinaryData));
|
||||
|
||||
// then start flashing the firmware
|
||||
|
||||
// should get didStart action just before starting to erase
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didStart());
|
||||
|
||||
// erase first
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(eraseRequest(1));
|
||||
|
||||
saga.put(didRequest(1));
|
||||
saga.put(eraseResponse(Result.OK));
|
||||
|
||||
// then write the new firmware
|
||||
|
||||
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
||||
|
||||
saga.put(didRequest(2));
|
||||
saga.put(initResponse(Result.OK));
|
||||
|
||||
const dummyPayload = new ArrayBuffer(0);
|
||||
let id = 2;
|
||||
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(
|
||||
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
||||
);
|
||||
expect((action as BootloaderProgramRequestAction).payload.byteLength).toBe(
|
||||
Math.min(14, totalFirmwareSize - offset),
|
||||
);
|
||||
|
||||
saga.put(didRequest(id));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
||||
|
||||
// Have to be careful that a checksum request is not sent after
|
||||
// last payload is sent, otherwise the hub gets confused.
|
||||
|
||||
if (offset + 14 >= totalFirmwareSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (count % 10 === 0) {
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(checksumRequest(++id));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
saga.put(checksumResponse(0));
|
||||
}
|
||||
}
|
||||
|
||||
// hub indicates success
|
||||
|
||||
saga.put(programResponse(0, totalFirmwareSize));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didProgress(1));
|
||||
|
||||
// and finally reboot the hub
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(rebootRequest(++id));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
|
||||
// then we are done
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didFinish());
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
+138
-121
@@ -1,47 +1,45 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { FirmwareMetadata, FirmwareReader, HubType } from '@pybricks/firmware';
|
||||
import { FirmwareReader, FirmwareReaderError, HubType } from '@pybricks/firmware';
|
||||
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 { Ace } from 'ace-builds';
|
||||
import {
|
||||
Effect,
|
||||
SagaGenerator,
|
||||
all,
|
||||
call,
|
||||
cancel,
|
||||
delay,
|
||||
getContext,
|
||||
put,
|
||||
race,
|
||||
select,
|
||||
take,
|
||||
takeEvery,
|
||||
} from 'redux-saga/effects';
|
||||
} from 'typed-redux-saga/macro';
|
||||
import { Action } from '../actions';
|
||||
import {
|
||||
FailToStartReasonType,
|
||||
FlashFirmwareActionType,
|
||||
FlashFirmwareFlashAction,
|
||||
progress,
|
||||
didFailToStart,
|
||||
didFinish,
|
||||
didProgress,
|
||||
didStart,
|
||||
} from '../actions/flash-firmware';
|
||||
import {
|
||||
BootloaderChecksumRequestAction,
|
||||
BootloaderChecksumResponseAction,
|
||||
BootloaderConnectionActionType,
|
||||
BootloaderConnectionDidConnectAction,
|
||||
BootloaderConnectionDidFailToConnectAction,
|
||||
BootloaderDidRequestAction,
|
||||
BootloaderDidRequestType,
|
||||
BootloaderDisconnectRequestAction,
|
||||
BootloaderEraseRequestAction,
|
||||
BootloaderEraseResponseAction,
|
||||
BootloaderErrorResponseAction,
|
||||
BootloaderInfoRequestAction,
|
||||
BootloaderInfoResponseAction,
|
||||
BootloaderInitRequestAction,
|
||||
BootloaderInitResponseAction,
|
||||
BootloaderProgramRequestAction,
|
||||
BootloaderProgramResponseAction,
|
||||
BootloaderRebootRequestAction,
|
||||
BootloaderResponseAction,
|
||||
BootloaderResponseActionType,
|
||||
checksumRequest,
|
||||
@@ -62,6 +60,7 @@ import {
|
||||
import * as notification from '../actions/notification';
|
||||
import { MaxProgramFlashSize } from '../protocols/lwp3-bootloader';
|
||||
import { RootState } from '../reducers';
|
||||
import { defined, maybe } from '../utils';
|
||||
import { fmod, sumComplement32 } from '../utils/math';
|
||||
|
||||
const firmwareZipMap = new Map<HubType, string>([
|
||||
@@ -70,25 +69,10 @@ const firmwareZipMap = new Map<HubType, string>([
|
||||
[HubType.MoveHub, moveHubZip],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Helper type for return value of wait() function.
|
||||
*/
|
||||
type WaitResponse<T extends BootloaderResponseAction> = [
|
||||
T,
|
||||
BootloaderErrorResponseAction,
|
||||
boolean,
|
||||
];
|
||||
|
||||
function* waitForDidSend(id: number): Generator {
|
||||
const didRequest = (yield take(
|
||||
(a: Action) =>
|
||||
a.type === BootloaderDidRequestType &&
|
||||
(a as BootloaderDidRequestAction).id === id,
|
||||
)) as BootloaderDidRequestAction;
|
||||
if (didRequest.err) {
|
||||
console.error(didRequest.err);
|
||||
}
|
||||
return didRequest;
|
||||
function* waitForDidRequest(id: number): SagaGenerator<BootloaderDidRequestAction> {
|
||||
return yield* take<BootloaderDidRequestAction>(
|
||||
(a: Action) => a.type === BootloaderDidRequestType && a.id === id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,8 +81,19 @@ function* waitForDidSend(id: number): Generator {
|
||||
* @param type The action type to wait for.
|
||||
* @param timeout The timeout in milliseconds.
|
||||
*/
|
||||
function waitForResponse(type: BootloaderResponseActionType, timeout = 500): Effect {
|
||||
return race([take(type), take(BootloaderResponseActionType.Error), delay(timeout)]);
|
||||
function* waitForResponse<T extends BootloaderResponseAction>(
|
||||
type: BootloaderResponseActionType,
|
||||
timeout = 500,
|
||||
): SagaGenerator<{
|
||||
response?: T;
|
||||
error?: BootloaderErrorResponseAction;
|
||||
timeout?: boolean;
|
||||
}> {
|
||||
return yield* race({
|
||||
response: take<T>(type),
|
||||
error: take<BootloaderErrorResponseAction>(BootloaderResponseActionType.Error),
|
||||
timeout: delay(timeout),
|
||||
});
|
||||
}
|
||||
|
||||
function* firmwareIterator(data: DataView, maxSize: number): Generator<number> {
|
||||
@@ -120,15 +115,27 @@ function* firmwareIterator(data: DataView, maxSize: number): Generator<number> {
|
||||
function* loadFirmware(
|
||||
data: ArrayBuffer,
|
||||
program: string | undefined,
|
||||
): Generator<unknown, { firmware: Uint8Array; deviceId: HubType }> {
|
||||
const reader = (yield call(() => FirmwareReader.load(data))) as FirmwareReader;
|
||||
): SagaGenerator<{ firmware: Uint8Array; deviceId: HubType }> {
|
||||
const [reader, readerErr] = yield* call(() => maybe(FirmwareReader.load(data)));
|
||||
|
||||
const firmwareBase = (yield call(() => reader.readFirmwareBase())) as Uint8Array;
|
||||
const metadata = (yield call(() => reader.readMetadata())) as FirmwareMetadata;
|
||||
if (readerErr) {
|
||||
// istanbul ignore else: unexpected error
|
||||
if (readerErr instanceof FirmwareReaderError) {
|
||||
yield* put(didFailToStart(FailToStartReasonType.ZipError, readerErr));
|
||||
} else {
|
||||
yield* put(didFailToStart(FailToStartReasonType.Unknown, readerErr));
|
||||
}
|
||||
yield* cancel();
|
||||
}
|
||||
|
||||
defined(reader);
|
||||
|
||||
const firmwareBase = yield* call(() => reader.readFirmwareBase());
|
||||
const metadata = yield* call(() => reader.readMetadata());
|
||||
|
||||
// if a user program was not given, then use main.py from the frimware.zip
|
||||
if (program === undefined) {
|
||||
program = (yield call(() => reader.readMainPy())) as string;
|
||||
program = yield* call(() => reader.readMainPy());
|
||||
}
|
||||
|
||||
if (metadata['mpy-abi-version'] !== 5) {
|
||||
@@ -137,16 +144,18 @@ function* loadFirmware(
|
||||
);
|
||||
}
|
||||
|
||||
yield put(compile(program, metadata['mpy-cross-options']));
|
||||
const [mpy, mpyFail] = (yield race([
|
||||
take(MpyActionType.DidCompile),
|
||||
take(MpyActionType.DidFailToCompile),
|
||||
])) as [MpyDidCompileAction, MpyDidFailToCompileAction];
|
||||
yield* put(compile(program, metadata['mpy-cross-options']));
|
||||
const { mpy, mpyFail } = yield* race({
|
||||
mpy: take<MpyDidCompileAction>(MpyActionType.DidCompile),
|
||||
mpyFail: take<MpyDidFailToCompileAction>(MpyActionType.DidFailToCompile),
|
||||
});
|
||||
|
||||
if (mpyFail) {
|
||||
throw Error(mpyFail.err);
|
||||
throw Error(mpyFail.err.join('\n'));
|
||||
}
|
||||
|
||||
defined(mpy);
|
||||
|
||||
// compute offset for checksum - must be aligned to 4-byte boundary
|
||||
const checksumOffset =
|
||||
metadata['user-mpy-offset'] + 4 + mpy.data.length + fmod(-mpy.data.length, 4);
|
||||
@@ -185,14 +194,12 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
|
||||
|
||||
let program: string | undefined = undefined;
|
||||
|
||||
const flashCurrentProgram = (yield select(
|
||||
const flashCurrentProgram = yield* select(
|
||||
(s: RootState) => s.settings.flashCurrentProgram,
|
||||
)) as boolean;
|
||||
);
|
||||
|
||||
if (flashCurrentProgram) {
|
||||
const editor = (yield select(
|
||||
(s: RootState) => s.editor.current,
|
||||
)) as Ace.EditSession | null;
|
||||
const editor = yield* select((s: RootState) => s.editor.current);
|
||||
|
||||
// istanbul ignore if: it is a bug to dispatch this action with no current editor
|
||||
if (editor === null) {
|
||||
@@ -207,101 +214,111 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
|
||||
({ firmware, deviceId } = yield* loadFirmware(action.data, program));
|
||||
}
|
||||
|
||||
yield put(connect());
|
||||
const connectResult = (yield take([
|
||||
yield* put(connect());
|
||||
const connectResult = yield* take<
|
||||
| BootloaderConnectionDidConnectAction
|
||||
| BootloaderConnectionDidFailToConnectAction
|
||||
>([
|
||||
BootloaderConnectionActionType.DidConnect,
|
||||
BootloaderConnectionActionType.DidFailToConnect,
|
||||
])) as
|
||||
| BootloaderConnectionDidConnectAction
|
||||
| BootloaderConnectionDidFailToConnectAction;
|
||||
]);
|
||||
|
||||
if (connectResult.type === BootloaderConnectionActionType.DidFailToConnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const infoAction = (yield put(infoRequest())) as BootloaderInfoRequestAction;
|
||||
const [, info] = (yield all([
|
||||
waitForDidSend(infoAction.id),
|
||||
waitForResponse(BootloaderResponseActionType.Info),
|
||||
])) as [BootloaderDidRequestAction, WaitResponse<BootloaderInfoResponseAction>];
|
||||
if (!info[0]) {
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
|
||||
const infoAction = yield* put(infoRequest(nextMessageId()));
|
||||
const { info } = yield* all({
|
||||
sent: waitForDidRequest(infoAction.id),
|
||||
info: waitForResponse<BootloaderInfoResponseAction>(
|
||||
BootloaderResponseActionType.Info,
|
||||
),
|
||||
});
|
||||
if (!info.response) {
|
||||
throw Error(`failed to get info: ${info}`);
|
||||
}
|
||||
|
||||
if (deviceId !== undefined && info[0].hubType !== deviceId) {
|
||||
throw Error(`Connected to ${info[0].hubType} but firmware is for ${deviceId}`);
|
||||
if (deviceId !== undefined && info.response.hubType !== deviceId) {
|
||||
throw Error(
|
||||
`Connected to ${info.response.hubType} but firmware is for ${deviceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (firmware === undefined) {
|
||||
const firmwarePath = firmwareZipMap.get(info[0].hubType);
|
||||
const firmwarePath = firmwareZipMap.get(info.response.hubType);
|
||||
if (firmwarePath === undefined) {
|
||||
yield put(
|
||||
yield* put(
|
||||
notification.add(
|
||||
'error',
|
||||
"Sorry, we don't have firmware for this hub yet.",
|
||||
),
|
||||
);
|
||||
yield put(disconnectRequest());
|
||||
yield* put(disconnectRequest(nextMessageId()));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = (yield call(() => fetch(firmwarePath))) as Response;
|
||||
const response = yield* call(() => fetch(firmwarePath));
|
||||
if (!response.ok) {
|
||||
yield put(notification.add('error', 'Failed to fetch firmware.'));
|
||||
const disconnectAction = (yield put(
|
||||
disconnectRequest(),
|
||||
)) as BootloaderDisconnectRequestAction;
|
||||
yield waitForDidSend(disconnectAction.id);
|
||||
yield* put(notification.add('error', 'Failed to fetch firmware.'));
|
||||
const disconnectAction = yield* put(disconnectRequest(nextMessageId()));
|
||||
yield* waitForDidRequest(disconnectAction.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = (yield call(() => response.arrayBuffer())) as ArrayBuffer;
|
||||
const data = yield* call(() => response.arrayBuffer());
|
||||
({ firmware, deviceId } = yield* loadFirmware(data, program));
|
||||
|
||||
if (deviceId !== undefined && info[0].hubType !== deviceId) {
|
||||
if (deviceId !== undefined && info.response.hubType !== deviceId) {
|
||||
throw Error(
|
||||
`Connected to ${info[0].hubType} but firmware is for ${deviceId}`,
|
||||
`Connected to ${info.response.hubType} but firmware is for ${deviceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const eraseAction = (yield put(eraseRequest())) as BootloaderEraseRequestAction;
|
||||
const [, erase] = (yield all([
|
||||
waitForDidSend(eraseAction.id),
|
||||
waitForResponse(BootloaderResponseActionType.Erase, 5000),
|
||||
])) as [BootloaderDidRequestAction, WaitResponse<BootloaderEraseResponseAction>];
|
||||
if (!erase[0] || erase[0].result) {
|
||||
yield* put(didStart());
|
||||
|
||||
const eraseAction = yield* put(eraseRequest(nextMessageId()));
|
||||
const { erase } = yield* all({
|
||||
sent: waitForDidRequest(eraseAction.id),
|
||||
erase: waitForResponse<BootloaderEraseResponseAction>(
|
||||
BootloaderResponseActionType.Erase,
|
||||
5000,
|
||||
),
|
||||
});
|
||||
if (!erase.response || erase.response.result) {
|
||||
// TODO: proper error handling
|
||||
throw Error(`Failed to erase: ${erase}`);
|
||||
}
|
||||
|
||||
const initAction = (yield put(
|
||||
initRequest(firmware.length),
|
||||
)) as BootloaderInitRequestAction;
|
||||
const [, init] = (yield all([
|
||||
waitForDidSend(initAction.id),
|
||||
waitForResponse(BootloaderResponseActionType.Init),
|
||||
])) as [BootloaderDidRequestAction, WaitResponse<BootloaderInitResponseAction>];
|
||||
if (!init[0] || init[0].result) {
|
||||
const initAction = yield* put(initRequest(nextMessageId(), firmware.length));
|
||||
const { init } = yield* all({
|
||||
sent: waitForDidRequest(initAction.id),
|
||||
init: waitForResponse<BootloaderInitResponseAction>(
|
||||
BootloaderResponseActionType.Init,
|
||||
),
|
||||
});
|
||||
if (!init.response || init.response.result) {
|
||||
// TODO: proper error handling
|
||||
throw Error(`Failed to init: ${init}`);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
const maxDataSize = MaxProgramFlashSize.get(info[0].hubType);
|
||||
if (maxDataSize === undefined) {
|
||||
// istanbul ignore next: indicates programmer error if reached
|
||||
throw Error('Missing hub type in MaxProgramFlashSize');
|
||||
}
|
||||
// 14 is "safe" size for all hubs
|
||||
const maxDataSize = MaxProgramFlashSize.get(info.response.hubType) || 14;
|
||||
|
||||
for (let offset = 0; ; ) {
|
||||
for (let count = 1, offset = 0; ; count++) {
|
||||
const payload = firmware.slice(offset, offset + maxDataSize);
|
||||
const programAction = (yield put(
|
||||
programRequest(info[0].startAddress + offset, payload.buffer),
|
||||
)) as BootloaderProgramRequestAction;
|
||||
yield waitForDidSend(programAction.id);
|
||||
const programAction = yield* put(
|
||||
programRequest(
|
||||
nextMessageId(),
|
||||
info.response.startAddress + offset,
|
||||
payload.buffer,
|
||||
),
|
||||
);
|
||||
yield* waitForDidRequest(programAction.id);
|
||||
|
||||
yield put(progress(offset, firmware.length));
|
||||
yield* put(didProgress(offset / firmware.length));
|
||||
|
||||
// we don't want to request checksum if this is the last packet since
|
||||
// the bootloader will send a response to the program request already.
|
||||
@@ -314,43 +331,43 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
|
||||
// the hub because of sending too much data at once. The actual
|
||||
// number of packets that can be queued in the Bluetooth chip on
|
||||
// the hub is not known and could vary by device.
|
||||
if (++count % 10 === 0) {
|
||||
const checksumAction = (yield put(
|
||||
checksumRequest(),
|
||||
)) as BootloaderChecksumRequestAction;
|
||||
const [, checksum] = (yield all([
|
||||
waitForDidSend(checksumAction.id),
|
||||
waitForResponse(BootloaderResponseActionType.Checksum, 5000),
|
||||
])) as [
|
||||
BootloaderDidRequestAction,
|
||||
WaitResponse<BootloaderChecksumResponseAction>,
|
||||
];
|
||||
if (!checksum[0]) {
|
||||
if (count % 10 === 0) {
|
||||
const checksumAction = yield* put(checksumRequest(nextMessageId()));
|
||||
const { checksum } = yield* all({
|
||||
sent: waitForDidRequest(checksumAction.id),
|
||||
checksum: waitForResponse<BootloaderChecksumResponseAction>(
|
||||
BootloaderResponseActionType.Checksum,
|
||||
5000,
|
||||
),
|
||||
});
|
||||
if (!checksum.response) {
|
||||
// TODO: proper error handling
|
||||
throw Error(`Failed to get checksum: ${checksum}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flash = (yield waitForResponse(
|
||||
const flash = yield* waitForResponse<BootloaderProgramResponseAction>(
|
||||
BootloaderResponseActionType.Program,
|
||||
5000,
|
||||
)) as WaitResponse<BootloaderProgramResponseAction>;
|
||||
if (!flash[0]) {
|
||||
);
|
||||
if (!flash.response) {
|
||||
throw Error(`failed to get final response: ${flash}`);
|
||||
}
|
||||
if (flash[0].count !== firmware.length) {
|
||||
if (flash.response.count !== firmware.length) {
|
||||
// TODO: proper error handling
|
||||
throw Error("Didn't flash all bytes");
|
||||
}
|
||||
|
||||
yield put(progress(firmware.length, firmware.length));
|
||||
yield* put(didProgress(1));
|
||||
|
||||
// this will cause the remote device to disconnect and reboot
|
||||
const rebootAction = (yield put(rebootRequest())) as BootloaderRebootRequestAction;
|
||||
yield waitForDidSend(rebootAction.id);
|
||||
const rebootAction = yield* put(rebootRequest(nextMessageId()));
|
||||
yield* waitForDidRequest(rebootAction.id);
|
||||
|
||||
yield* put(didFinish());
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(FlashFirmwareActionType.FlashFirmware, flashFirmware);
|
||||
yield* takeEvery(FlashFirmwareActionType.FlashFirmware, flashFirmware);
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
stop,
|
||||
} from '../actions/hub';
|
||||
import { MpyActionType, didCompile } from '../actions/mpy';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
import hub from './hub';
|
||||
|
||||
jest.mock('ace-builds');
|
||||
|
||||
describe('downloadAndRun', () => {
|
||||
test('no errors', async () => {
|
||||
const saga = new AsyncSaga(hub);
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
const mockEditor = mock<Ace.EditSession>();
|
||||
saga.setState({ editor: { current: mockEditor } });
|
||||
@@ -75,7 +76,7 @@ describe('downloadAndRun', () => {
|
||||
});
|
||||
|
||||
test('repl', async () => {
|
||||
const saga = new AsyncSaga(hub);
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(repl());
|
||||
|
||||
@@ -86,7 +87,7 @@ test('repl', async () => {
|
||||
});
|
||||
|
||||
test('stop', async () => {
|
||||
const saga = new AsyncSaga(hub);
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(stop());
|
||||
|
||||
|
||||
+12
-5
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Ace } from 'ace-builds';
|
||||
import { Channel } from 'redux-saga';
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
RaceEffect,
|
||||
TakeEffect,
|
||||
actionChannel,
|
||||
getContext,
|
||||
put,
|
||||
race,
|
||||
select,
|
||||
@@ -80,11 +81,15 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
|
||||
HubMessageActionType.Checksum,
|
||||
)) as Channel<HubChecksumMessageAction>;
|
||||
|
||||
const nextMessageId = (yield getContext('nextMessageId')) as () => number;
|
||||
|
||||
// first send payload size as big-endian 32-bit integer
|
||||
const sizeBuf = new Uint8Array(4);
|
||||
const sizeView = new DataView(sizeBuf.buffer);
|
||||
sizeView.setUint32(0, mpy.data.byteLength, true);
|
||||
const writeAction = (yield put(write(sizeBuf))) as BleUartWriteAction;
|
||||
const writeAction = (yield put(
|
||||
write(nextMessageId(), sizeBuf),
|
||||
)) as BleUartWriteAction;
|
||||
const [, didFailToWrite] = (yield waitForWrite(writeAction.id)) as [
|
||||
BleUartDidWriteAction,
|
||||
BleUartDidFailToWriteAction,
|
||||
@@ -113,7 +118,7 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
|
||||
// we can actually only write 20 bytes at a time
|
||||
for (let j = 0; j < chunk.length; j += SafeTxCharLength) {
|
||||
const writeAction = (yield put(
|
||||
write(chunk.slice(j, j + SafeTxCharLength)),
|
||||
write(nextMessageId(), chunk.slice(j, j + SafeTxCharLength)),
|
||||
)) as BleUartWriteAction;
|
||||
const [, didFailToWrite] = (yield waitForWrite(writeAction.id)) as [
|
||||
BleUartDidWriteAction,
|
||||
@@ -146,14 +151,16 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
|
||||
const startReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
|
||||
|
||||
function* startRepl(_action: HubReplAction): Generator {
|
||||
yield put(write(startReplCommand));
|
||||
const nextMessageId = (yield getContext('nextMessageId')) as () => number;
|
||||
yield put(write(nextMessageId(), startReplCommand));
|
||||
}
|
||||
|
||||
// CTRL+C, CTRL+C, CTRL+D
|
||||
const stopCommand = new Uint8Array([0x03, 0x03, 0x04]);
|
||||
|
||||
function* stop(_action: HubStopAction): Generator {
|
||||
yield put(write(stopCommand));
|
||||
const nextMessageId = (yield getContext('nextMessageId')) as () => number;
|
||||
yield put(write(nextMessageId(), stopCommand));
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { AsyncSaga, delay } from '../../test';
|
||||
import { openLicenseDialog } from '../actions/app';
|
||||
import { didFailToFetchList, didFetchList } from '../actions/license';
|
||||
import { LicenseList, LicenseState } from '../reducers/license';
|
||||
import { LicenseList } from '../reducers/license';
|
||||
import license from './license';
|
||||
|
||||
afterAll(() => {
|
||||
@@ -24,7 +24,7 @@ describe('fetchLicenses', () => {
|
||||
|
||||
// initially, license list starts as null, so fetch is called to get
|
||||
// the list
|
||||
saga.setState({ license: { list: null } as LicenseState });
|
||||
saga.setState({ license: { list: null } });
|
||||
saga.put(openLicenseDialog());
|
||||
|
||||
const action = await saga.take();
|
||||
@@ -42,7 +42,7 @@ describe('fetchLicenses', () => {
|
||||
|
||||
// after we have the list, we don't fetch it again since it will
|
||||
// always be the same list
|
||||
saga.setState({ license: { list: testLicenseList } as LicenseState });
|
||||
saga.setState({ license: { list: testLicenseList } });
|
||||
saga.put(openLicenseDialog());
|
||||
|
||||
// have to yield to be sure fetch call would have taken place on error
|
||||
@@ -56,7 +56,7 @@ describe('fetchLicenses', () => {
|
||||
|
||||
jest.spyOn(globalThis, 'fetch').mockResolvedValue(failResponse);
|
||||
|
||||
saga.setState({ license: { list: null } as LicenseState });
|
||||
saga.setState({ license: { list: null } });
|
||||
saga.put(openLicenseDialog());
|
||||
|
||||
const action = await saga.take();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
// File: sagas/lwp3-bootloader-protocol.test.ts
|
||||
|
||||
import { AsyncSaga } from '../../test';
|
||||
@@ -40,7 +40,7 @@ describe('message encoder', () => {
|
||||
test.each([
|
||||
[
|
||||
'erase',
|
||||
eraseRequest(),
|
||||
eraseRequest(0),
|
||||
[
|
||||
0x11, // erase command
|
||||
],
|
||||
@@ -48,6 +48,7 @@ describe('message encoder', () => {
|
||||
[
|
||||
'program',
|
||||
programRequest(
|
||||
1,
|
||||
0x08005000,
|
||||
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]).buffer,
|
||||
),
|
||||
@@ -76,14 +77,14 @@ describe('message encoder', () => {
|
||||
],
|
||||
[
|
||||
'reboot',
|
||||
rebootRequest(),
|
||||
rebootRequest(2),
|
||||
[
|
||||
0x33, // reboot command
|
||||
],
|
||||
],
|
||||
[
|
||||
'init',
|
||||
initRequest(100000),
|
||||
initRequest(3, 100000),
|
||||
[
|
||||
0x44, // init command
|
||||
0xa0, // size LSB
|
||||
@@ -94,33 +95,38 @@ describe('message encoder', () => {
|
||||
],
|
||||
[
|
||||
'info',
|
||||
infoRequest(),
|
||||
infoRequest(4),
|
||||
[
|
||||
0x55, // info command
|
||||
],
|
||||
],
|
||||
[
|
||||
'checksum',
|
||||
checksumRequest(),
|
||||
checksumRequest(5),
|
||||
[
|
||||
0x66, // checksum command
|
||||
],
|
||||
],
|
||||
[
|
||||
'state',
|
||||
stateRequest(),
|
||||
stateRequest(6),
|
||||
[
|
||||
0x77, // state command
|
||||
],
|
||||
],
|
||||
[
|
||||
'disconnect',
|
||||
disconnectRequest(),
|
||||
disconnectRequest(7),
|
||||
[
|
||||
0x88, // disconnect command
|
||||
],
|
||||
],
|
||||
])('encode %s request', async (_n, request, expected) => {
|
||||
const messageTypesThatShouldBeCalledWithoutResponse = [
|
||||
BootloaderRequestActionType.Program,
|
||||
BootloaderRequestActionType.Reboot,
|
||||
BootloaderRequestActionType.Disconnect,
|
||||
];
|
||||
const saga = new AsyncSaga(bootloader);
|
||||
saga.put(request);
|
||||
const message = new Uint8Array(expected);
|
||||
@@ -128,7 +134,7 @@ describe('message encoder', () => {
|
||||
expect(action).toEqual(
|
||||
send(
|
||||
message,
|
||||
/* withResponse */ request.type !== BootloaderRequestActionType.Program,
|
||||
!messageTypesThatShouldBeCalledWithoutResponse.includes(request.type),
|
||||
),
|
||||
);
|
||||
await saga.end();
|
||||
@@ -138,10 +144,10 @@ describe('message encoder', () => {
|
||||
const saga = new AsyncSaga(bootloader);
|
||||
|
||||
// we send 4 requests
|
||||
saga.put({ ...eraseRequest(), id: 0 });
|
||||
saga.put({ ...eraseRequest(), id: 1 });
|
||||
saga.put({ ...eraseRequest(), id: 2 });
|
||||
saga.put({ ...eraseRequest(), id: 3 });
|
||||
saga.put(eraseRequest(0));
|
||||
saga.put(eraseRequest(1));
|
||||
saga.put(eraseRequest(2));
|
||||
saga.put(eraseRequest(3));
|
||||
|
||||
// but only two didSend action meaning only the first two completed
|
||||
saga.put(didSend());
|
||||
|
||||
@@ -81,7 +81,7 @@ function* encodeRequest(): Generator {
|
||||
);
|
||||
break;
|
||||
case BootloaderRequestActionType.Reboot:
|
||||
yield put(send(createStartAppRequest()));
|
||||
yield put(send(createStartAppRequest(), /* withResponse */ false));
|
||||
break;
|
||||
case BootloaderRequestActionType.Init:
|
||||
yield put(send(createInitLoaderRequest(action.firmwareSize)));
|
||||
@@ -96,7 +96,7 @@ function* encodeRequest(): Generator {
|
||||
yield put(send(createGetFlashStateRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.Disconnect:
|
||||
yield put(send(createDisconnectRequest()));
|
||||
yield put(send(createDisconnectRequest(), /* withResponse */ false));
|
||||
break;
|
||||
/* istanbul ignore next: should not be possible to reach */
|
||||
default:
|
||||
|
||||
@@ -37,7 +37,7 @@ test('compiler error works', async () => {
|
||||
const action = await saga.take();
|
||||
expect(action.type).toBe(MpyActionType.DidFailToCompile);
|
||||
const { err } = action as MpyDidFailToCompileAction;
|
||||
expect(err).toContain('SyntaxError');
|
||||
expect(err).toMatchSnapshot();
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
BootloaderConnectionFailureReason,
|
||||
didFailToConnect as bootloaderDidFailToConnect,
|
||||
} from '../actions/lwp3-bootloader';
|
||||
import { didFailToCompile } from '../actions/mpy';
|
||||
import { didCompile, didFailToCompile } from '../actions/mpy';
|
||||
import { add } from '../actions/notification';
|
||||
import { didSucceed, didUpdate } from '../actions/service-worker';
|
||||
import { MessageId } from '../components/notification-i18n';
|
||||
import notification from './notification';
|
||||
|
||||
test.each([
|
||||
@@ -26,11 +27,13 @@ test.each([
|
||||
reason: BleDeviceFailToConnectReasonType.Unknown,
|
||||
err: { name: 'test', message: 'unknown' },
|
||||
}),
|
||||
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.Unknown),
|
||||
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.Unknown, <Error>{
|
||||
message: 'test',
|
||||
}),
|
||||
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.NoWebBluetooth),
|
||||
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.GattServiceNotFound),
|
||||
storageChanged('test'),
|
||||
didFailToCompile('reason'),
|
||||
didFailToCompile(['reason']),
|
||||
add('warning', 'message'),
|
||||
add('error', 'message', 'url'),
|
||||
didUpdate({} as ServiceWorkerRegistration),
|
||||
@@ -86,3 +89,30 @@ test.each([
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test.each([[didCompile(new Uint8Array()), MessageId.MpyError]])(
|
||||
'actions that should close a notification: %o',
|
||||
async (action: Action, key: string) => {
|
||||
const getToasts = jest.fn().mockReturnValue([]);
|
||||
const show = jest.fn();
|
||||
const dismiss = jest.fn();
|
||||
const clear = jest.fn();
|
||||
|
||||
const toaster: IToaster = {
|
||||
getToasts,
|
||||
show,
|
||||
dismiss,
|
||||
clear,
|
||||
};
|
||||
|
||||
const saga = new AsyncSaga(notification, { notification: { toaster } });
|
||||
|
||||
saga.put(action);
|
||||
|
||||
expect(show).not.toBeCalled();
|
||||
expect(dismiss).toBeCalledWith(key);
|
||||
expect(clear).not.toBeCalled();
|
||||
|
||||
await saga.end();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ import { MpyActionType, MpyDidFailToCompileAction } from '../actions/mpy';
|
||||
import { NotificationActionType, NotificationAddAction } from '../actions/notification';
|
||||
import { ServiceWorkerActionType } from '../actions/service-worker';
|
||||
import Notification from '../components/Notification';
|
||||
import UnexpectedErrorNotification from '../components/UnexpectedErrorNotification';
|
||||
import { MessageId } from '../components/notification-i18n';
|
||||
import { appName } from '../settings/ui';
|
||||
|
||||
@@ -140,6 +141,17 @@ function* showSingleton(
|
||||
);
|
||||
}
|
||||
|
||||
/** Shows a special notification for unexpected errors. */
|
||||
function* showUnexpectedError(messageId: MessageId, err: Error): Generator {
|
||||
const { toaster } = (yield getContext('notification')) as NotificationContext;
|
||||
toaster.show({
|
||||
intent: mapIntent(Level.Error),
|
||||
icon: mapIcon(Level.Error),
|
||||
message: React.createElement(UnexpectedErrorNotification, { messageId, err }),
|
||||
timeout: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function* showBleDeviceDidFailToConnectError(
|
||||
action: BleDeviceDidFailToConnectAction,
|
||||
): Generator {
|
||||
@@ -165,7 +177,7 @@ function* showBleDeviceDidFailToConnectError(
|
||||
);
|
||||
break;
|
||||
case BleDeviceFailToConnectReasonType.Unknown:
|
||||
yield* showSingleton(Level.Error, MessageId.BleConnectFailed);
|
||||
yield* showUnexpectedError(MessageId.BleUnexpectedError, action.err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -191,7 +203,7 @@ function* showBootloaderDidFailToConnectError(
|
||||
);
|
||||
break;
|
||||
case BootloaderConnectionFailureReason.Unknown:
|
||||
yield* showSingleton(Level.Error, MessageId.BleConnectFailed);
|
||||
yield* showUnexpectedError(MessageId.BleUnexpectedError, action.err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -214,8 +226,15 @@ function* showEditorStorageChanged(): Generator {
|
||||
yield put(reloadProgram());
|
||||
}
|
||||
|
||||
function* dismissCompilerError(): Generator {
|
||||
const { toaster } = (yield getContext('notification')) as NotificationContext;
|
||||
toaster.dismiss(MessageId.MpyError);
|
||||
}
|
||||
|
||||
function* showCompilerError(action: MpyDidFailToCompileAction): Generator {
|
||||
yield* showSingleton(Level.Error, MessageId.MpyError, { errorMessage: action.err });
|
||||
yield* showSingleton(Level.Error, MessageId.MpyError, {
|
||||
errorMessage: React.createElement('pre', undefined, action.err.join('\n')),
|
||||
});
|
||||
}
|
||||
|
||||
function* addNotification(action: NotificationAddAction): Generator {
|
||||
@@ -263,6 +282,7 @@ export default function* (): Generator {
|
||||
showBootloaderDidFailToConnectError,
|
||||
);
|
||||
yield takeEvery(EditorActionType.StorageChanged, showEditorStorageChanged);
|
||||
yield takeEvery(MpyActionType.DidCompile, dismissCompilerError);
|
||||
yield takeEvery(MpyActionType.DidFailToCompile, showCompilerError);
|
||||
yield takeEvery(NotificationActionType.Add, addNotification);
|
||||
yield takeEvery(ServiceWorkerActionType.DidUpdate, showServiceWorkerUpdate);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { AsyncSaga } from '../../test';
|
||||
import { didStart } from '../actions/app';
|
||||
import { didBooleanChange, didFailToSetBoolean, setBoolean } from '../actions/settings';
|
||||
import { SettingsState } from '../reducers/settings';
|
||||
import { SettingId } from '../settings/user';
|
||||
import settings from './settings';
|
||||
|
||||
@@ -221,7 +220,7 @@ describe('store settings to local storage', () => {
|
||||
throw testError;
|
||||
});
|
||||
|
||||
saga.setState({ settings: { showDocs: false } as SettingsState });
|
||||
saga.setState({ settings: { showDocs: false } });
|
||||
saga.put(setBoolean(SettingId.ShowDocs, true));
|
||||
expect(mockSetItem).toHaveBeenCalled();
|
||||
|
||||
@@ -246,7 +245,7 @@ describe('store settings to local storage', () => {
|
||||
expect(value).toBe('true');
|
||||
});
|
||||
|
||||
saga.setState({ settings: { showDocs: false } as SettingsState });
|
||||
saga.setState({ settings: { showDocs: false } });
|
||||
saga.put(setBoolean(SettingId.ShowDocs, true));
|
||||
expect(mockSetItem).toHaveBeenCalled();
|
||||
|
||||
@@ -266,7 +265,7 @@ describe('store settings to local storage', () => {
|
||||
expect(value).toBe('false');
|
||||
});
|
||||
|
||||
saga.setState({ settings: { darkMode: true } as SettingsState });
|
||||
saga.setState({ settings: { darkMode: true } });
|
||||
saga.put(setBoolean(SettingId.DarkMode, false));
|
||||
expect(mockSetItem).toHaveBeenCalled();
|
||||
|
||||
@@ -286,7 +285,7 @@ describe('store settings to local storage', () => {
|
||||
expect(value).toBe('false');
|
||||
});
|
||||
|
||||
saga.setState({ settings: { flashCurrentProgram: true } as SettingsState });
|
||||
saga.setState({ settings: { flashCurrentProgram: true } });
|
||||
saga.put(setBoolean(SettingId.FlashCurrentProgram, false));
|
||||
expect(mockSetItem).toHaveBeenCalled();
|
||||
|
||||
|
||||
+16
-15
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { AsyncSaga, delay } from '../../test';
|
||||
|
||||
@@ -25,11 +25,12 @@ import {
|
||||
sendData,
|
||||
} from '../actions/terminal';
|
||||
import { HubRuntimeState } from '../reducers/hub';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
import terminal from './terminal';
|
||||
|
||||
describe('Data receiver filters out hub status', () => {
|
||||
test('normal message - no status', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
// sending ASCII space character
|
||||
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
|
||||
@@ -43,7 +44,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
});
|
||||
|
||||
test('checksum message', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.setState({ hub: { runtime: HubRuntimeState.Loading } });
|
||||
saga.put(notify(new DataView(new Uint8Array([0xaa]).buffer)));
|
||||
@@ -56,7 +57,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
});
|
||||
|
||||
test('idle message', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
// '>>>> IDLE'
|
||||
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
|
||||
@@ -88,7 +89,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
});
|
||||
|
||||
test('idle message with extra text', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
// '0>>>> IDLE1'
|
||||
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
|
||||
@@ -132,7 +133,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
});
|
||||
|
||||
test('error message', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
// '>>>> ERROR'
|
||||
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
|
||||
@@ -165,7 +166,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
});
|
||||
|
||||
test('error message with extra text', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
// '0>>>> ERROR1'
|
||||
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
|
||||
@@ -210,7 +211,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
});
|
||||
|
||||
test('running message', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
// '>>>> ERROR'
|
||||
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
|
||||
@@ -245,7 +246,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
});
|
||||
|
||||
test('running message with extra text', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
// '0>>>> RUNNING1'
|
||||
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
|
||||
@@ -293,7 +294,7 @@ describe('Data receiver filters out hub status', () => {
|
||||
});
|
||||
|
||||
test('Terminal data source responds to send data actions', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(didStart());
|
||||
const dataSourceAction = await saga.take();
|
||||
@@ -320,7 +321,7 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
const expected = new Uint8Array([0x74, 0x65, 0x73, 0x74, 0x31, 0x32, 0x33, 0x34]);
|
||||
|
||||
test('basic function works', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
|
||||
@@ -332,7 +333,7 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
});
|
||||
|
||||
test('messages are queued until previous has completed', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
await delay(50); // without delay, messages are combined
|
||||
@@ -360,7 +361,7 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
});
|
||||
|
||||
test('messages are queued until previous has failed', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
await delay(50); // without delay, messages are combined
|
||||
@@ -390,7 +391,7 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
});
|
||||
|
||||
test('small messages are combined', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(receiveData('test1234'));
|
||||
saga.put(receiveData('test1234'));
|
||||
@@ -405,7 +406,7 @@ describe('Terminal data source responds to receive data actions', () => {
|
||||
});
|
||||
|
||||
test('long messages are split', async () => {
|
||||
const saga = new AsyncSaga(terminal);
|
||||
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(receiveData('012345678901234567890123456789'));
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Channel } from 'redux-saga';
|
||||
import {
|
||||
actionChannel,
|
||||
delay,
|
||||
fork,
|
||||
getContext,
|
||||
put,
|
||||
race,
|
||||
select,
|
||||
@@ -120,11 +121,13 @@ function* receiveTerminalData(): Generator {
|
||||
value += action.value;
|
||||
}
|
||||
|
||||
const nextMessageId = (yield getContext('nextMessageId')) as () => number;
|
||||
|
||||
// stdin gets piped to BLE connection
|
||||
const data = encoder.encode(value);
|
||||
for (let i = 0; i < data.length; i += SafeTxCharLength) {
|
||||
const { id } = (yield put(
|
||||
write(data.slice(i, i + SafeTxCharLength)),
|
||||
write(nextMessageId(), data.slice(i, i + SafeTxCharLength)),
|
||||
)) as BleUartWriteAction;
|
||||
|
||||
yield take(
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { I18nManager } from '@shopify/react-i18n';
|
||||
|
||||
// TODO: add locale setting and use browser preferred language as default
|
||||
|
||||
/** The global i18n manager. */
|
||||
export const i18nManager = new I18nManager({
|
||||
locale: 'en',
|
||||
onError: (err): void => console.error(err),
|
||||
});
|
||||
|
||||
/** Enables or disables pseudolocalization for development. */
|
||||
export function pseudolocalize(pseudolocalize: boolean): void {
|
||||
i18nManager.update({ ...i18nManager.details, pseudolocalize });
|
||||
}
|
||||
+19
-1
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { assert, hex } from '.';
|
||||
import { assert, defined, hex, maybe } from '.';
|
||||
|
||||
test('assert', () => {
|
||||
const assertTrue = jest.fn(() => assert(true, 'should not throw'));
|
||||
@@ -11,6 +11,24 @@ test('assert', () => {
|
||||
expect(() => assert(false, 'should throw')).toThrow();
|
||||
});
|
||||
|
||||
describe('defined', () => {
|
||||
expect(() => defined('test')).not.toThrow();
|
||||
expect(() => defined(undefined)).toThrowError();
|
||||
});
|
||||
|
||||
describe('maybe', () => {
|
||||
test('resolved', async () => {
|
||||
const [result, error] = await maybe(Promise.resolve('test'));
|
||||
expect(result).toBe('test');
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
test('rejected', async () => {
|
||||
const [result, error] = await maybe(Promise.reject(new Error('test')));
|
||||
expect(result).toBeUndefined();
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
test('hex', () => {
|
||||
expect(hex(0, 2)).toBe('0x00');
|
||||
expect(hex(1, 4)).toBe('0x0001');
|
||||
|
||||
+21
-1
@@ -7,12 +7,32 @@
|
||||
* @param condition A condition that is assumed to be true
|
||||
* @param message Informational message for debugging
|
||||
*/
|
||||
export function assert(condition: boolean, message: string): void {
|
||||
export function assert(condition: boolean, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that an object is not undefined. This is used to make the type
|
||||
* checker happy with `maybe()` and saga `race()` and `all()` effects where
|
||||
* we have the condition "if A is undefined, then B is not undefined".
|
||||
*/
|
||||
export function defined<T>(obj: T): asserts obj is NonNullable<T> {
|
||||
assert(obj !== undefined, 'undefined object');
|
||||
}
|
||||
|
||||
export type Maybe<T> = [T?, Error?];
|
||||
|
||||
/** Wraps a promise in try/catch and returns the promise result or error. */
|
||||
export async function maybe<T>(promise: Promise<T>): Promise<Maybe<T>> {
|
||||
try {
|
||||
return [await promise];
|
||||
} catch (err) {
|
||||
return [undefined, err];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number as hex (0x00...)
|
||||
* @param n The number to format
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ $pt-font-size-large: $pt-grid-size * 1.8;
|
||||
$pt-font-size-small: $pt-grid-size * 1.4;
|
||||
|
||||
$pt-navbar-height: 72px;
|
||||
$pb-status-bar-height: 3vh;
|
||||
$pb-status-bar-height: 24px;
|
||||
|
||||
$pb-pybricks-blue: #0088ce;
|
||||
$pt-app-background-color: #e8e8e8;
|
||||
|
||||
+6
-2
@@ -5,11 +5,15 @@ import { END, MulticastChannel, Saga, Task, runSaga, stdChannel } from 'redux-sa
|
||||
import { Action } from '../src/actions';
|
||||
import { RootState } from '../src/reducers';
|
||||
|
||||
type RecursivePartial<T> = {
|
||||
[P in keyof T]?: RecursivePartial<T[P]>;
|
||||
};
|
||||
|
||||
export class AsyncSaga {
|
||||
private channel: MulticastChannel<Action>;
|
||||
private dispatches: (Action | END)[];
|
||||
private takers: { put: (action: Action | END) => void }[];
|
||||
private state: Partial<RootState>;
|
||||
private state: RecursivePartial<RootState>;
|
||||
private task: Task;
|
||||
|
||||
public constructor(saga: Saga, context?: Record<string, unknown>) {
|
||||
@@ -63,7 +67,7 @@ export class AsyncSaga {
|
||||
return Promise.resolve(next);
|
||||
}
|
||||
|
||||
public setState(state: Partial<RootState>): void {
|
||||
public setState(state: RecursivePartial<RootState>): void {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
|
||||
@@ -1455,17 +1455,17 @@
|
||||
schema-utils "^2.6.5"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@pybricks/firmware@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@pybricks/firmware/-/firmware-4.4.0.tgz#bdb7fd5476b914533b09d796fb2d98ec1e911ae1"
|
||||
integrity sha512-le6EgkipT74D5lmi47FJa2B/Jms4XuRbh6ReoUH9bgaW5TolmE4wwN67uwA2c3fVY16ApCMe9dZoj/5y6ai50w==
|
||||
"@pybricks/firmware@4.5.0":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@pybricks/firmware/-/firmware-4.5.0.tgz#e1e46da2000e2d4319d19ac0a4d21e3b8040ad0a"
|
||||
integrity sha512-GsV+mTeUkR3RAK+HlBwJe+gifG5X4UFgjXxsB85ykd83g701b8ORP9M44fNy29fj/ZsOwKTz+MpyibbE6NgZXA==
|
||||
dependencies:
|
||||
jszip "^3.5.0"
|
||||
|
||||
"@pybricks/mpy-cross-v5@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@pybricks/mpy-cross-v5/-/mpy-cross-v5-1.2.0.tgz#29cbd949c579c0551792d2fd1cd777d193aa925c"
|
||||
integrity sha512-A1FXGP0teuZa3tPBTz9niCdEol4Ld6jOABl9FkglmutGWO99eLwuyYibh8wQqHNKwXRHrzCJmo/W+/42HmSjog==
|
||||
"@pybricks/mpy-cross-v5@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@pybricks/mpy-cross-v5/-/mpy-cross-v5-2.0.0.tgz#9d64e1dedda0a7a028117f510ce5365f504df400"
|
||||
integrity sha512-s3B+0tsXRHF3Y+FfOeDkNLoM+dwwT8P05UowVYPQvTV/UjhBGwruUdEoG/cYz2cFrqx+VFF8gpgmpw+5aA7/hQ==
|
||||
|
||||
"@redux-saga/core@^1.1.3":
|
||||
version "1.1.3"
|
||||
@@ -1935,6 +1935,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||
|
||||
"@types/jszip@^3.4.1":
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jszip/-/jszip-3.4.1.tgz#e7a4059486e494c949ef750933d009684227846f"
|
||||
integrity sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==
|
||||
dependencies:
|
||||
jszip "*"
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
@@ -2131,6 +2138,20 @@
|
||||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^4.8.2":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.0.tgz#92db8e7c357ed7d69632d6843ca70b71be3a721d"
|
||||
integrity sha512-IJ5e2W7uFNfg4qh9eHkHRUCbgZ8VKtGwD07kannJvM5t/GU8P8+24NX8gi3Hf5jST5oWPY8kyV1s/WtfiZ4+Ww==
|
||||
dependencies:
|
||||
"@typescript-eslint/experimental-utils" "4.14.0"
|
||||
"@typescript-eslint/scope-manager" "4.14.0"
|
||||
debug "^4.1.1"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
lodash "^4.17.15"
|
||||
regexpp "^3.0.0"
|
||||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/experimental-utils@4.13.0", "@typescript-eslint/experimental-utils@^4.0.1":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.13.0.tgz#9dc9ab375d65603b43d938a0786190a0c72be44e"
|
||||
@@ -2143,6 +2164,18 @@
|
||||
eslint-scope "^5.0.0"
|
||||
eslint-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/experimental-utils@4.14.0", "@typescript-eslint/experimental-utils@^4.8.2", "@typescript-eslint/experimental-utils@^4.9.1":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.0.tgz#5aa7b006736634f588a69ee343ca959cd09988df"
|
||||
integrity sha512-6i6eAoiPlXMKRbXzvoQD5Yn9L7k9ezzGRvzC/x1V3650rUk3c3AOjQyGYyF9BDxQQDK2ElmKOZRD0CbtdkMzQQ==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.3"
|
||||
"@typescript-eslint/scope-manager" "4.14.0"
|
||||
"@typescript-eslint/types" "4.14.0"
|
||||
"@typescript-eslint/typescript-estree" "4.14.0"
|
||||
eslint-scope "^5.0.0"
|
||||
eslint-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/experimental-utils@^3.10.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686"
|
||||
@@ -2164,6 +2197,16 @@
|
||||
"@typescript-eslint/typescript-estree" "4.13.0"
|
||||
debug "^4.1.1"
|
||||
|
||||
"@typescript-eslint/parser@^4.8.2":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.14.0.tgz#62d4cd2079d5c06683e9bfb200c758f292c4dee7"
|
||||
integrity sha512-sUDeuCjBU+ZF3Lzw0hphTyScmDDJ5QVkyE21pRoBo8iDl7WBtVFS+WDN3blY1CH3SBt7EmYCw6wfmJjF0l/uYg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "4.14.0"
|
||||
"@typescript-eslint/types" "4.14.0"
|
||||
"@typescript-eslint/typescript-estree" "4.14.0"
|
||||
debug "^4.1.1"
|
||||
|
||||
"@typescript-eslint/scope-manager@4.13.0":
|
||||
version "4.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz#5b45912a9aa26b29603d8fa28f5e09088b947141"
|
||||
@@ -2172,6 +2215,14 @@
|
||||
"@typescript-eslint/types" "4.13.0"
|
||||
"@typescript-eslint/visitor-keys" "4.13.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@4.14.0":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.0.tgz#55a4743095d684e1f7b7180c4bac2a0a3727f517"
|
||||
integrity sha512-/J+LlRMdbPh4RdL4hfP1eCwHN5bAhFAGOTsvE6SxsrM/47XQiPSgF5MDgLyp/i9kbZV9Lx80DW0OpPkzL+uf8Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.14.0"
|
||||
"@typescript-eslint/visitor-keys" "4.14.0"
|
||||
|
||||
"@typescript-eslint/types@3.10.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
|
||||
@@ -2182,6 +2233,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.13.0.tgz#6a7c6015a59a08fbd70daa8c83dfff86250502f8"
|
||||
integrity sha512-/+aPaq163oX+ObOG00M0t9tKkOgdv9lq0IQv/y4SqGkAXmhFmCfgsELV7kOCTb2vVU5VOmVwXBXJTDr353C1rQ==
|
||||
|
||||
"@typescript-eslint/types@4.14.0":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.0.tgz#d8a8202d9b58831d6fd9cee2ba12f8a5a5dd44b6"
|
||||
integrity sha512-VsQE4VvpldHrTFuVPY1ZnHn/Txw6cZGjL48e+iBxTi2ksa9DmebKjAeFmTVAYoSkTk7gjA7UqJ7pIsyifTsI4A==
|
||||
|
||||
"@typescript-eslint/typescript-estree@3.10.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853"
|
||||
@@ -2210,6 +2266,20 @@
|
||||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/typescript-estree@4.14.0":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.0.tgz#4bcd67486e9acafc3d0c982b23a9ab8ac8911ed7"
|
||||
integrity sha512-wRjZ5qLao+bvS2F7pX4qi2oLcOONIB+ru8RGBieDptq/SudYwshveORwCVU4/yMAd4GK7Fsf8Uq1tjV838erag==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.14.0"
|
||||
"@typescript-eslint/visitor-keys" "4.14.0"
|
||||
debug "^4.1.1"
|
||||
globby "^11.0.1"
|
||||
is-glob "^4.0.1"
|
||||
lodash "^4.17.15"
|
||||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@3.10.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931"
|
||||
@@ -2225,6 +2295,14 @@
|
||||
"@typescript-eslint/types" "4.13.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.14.0":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.0.tgz#b1090d9d2955b044b2ea2904a22496849acbdf54"
|
||||
integrity sha512-MeHHzUyRI50DuiPgV9+LxcM52FCJFYjJiWHtXlbyC27b80mfOwKeiKI+MHOTEpcpfmoPFm/vvQS88bYIx6PZTA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.14.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@webassemblyjs/ast@1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
|
||||
@@ -2674,7 +2752,7 @@ array.prototype.flat@^1.2.3:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.18.0-next.1"
|
||||
|
||||
array.prototype.flatmap@^1.2.3:
|
||||
array.prototype.flatmap@^1.2.3, array.prototype.flatmap@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9"
|
||||
integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==
|
||||
@@ -2886,7 +2964,7 @@ babel-plugin-jest-hoist@^26.6.2:
|
||||
"@types/babel__core" "^7.0.0"
|
||||
"@types/babel__traverse" "^7.0.6"
|
||||
|
||||
babel-plugin-macros@2.8.0:
|
||||
babel-plugin-macros@2.8.0, babel-plugin-macros@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
|
||||
integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
|
||||
@@ -2895,6 +2973,15 @@ babel-plugin-macros@2.8.0:
|
||||
cosmiconfig "^6.0.0"
|
||||
resolve "^1.12.0"
|
||||
|
||||
babel-plugin-macros@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.0.1.tgz#0d412d68f5b3d1b64358f24ab099bd148724e2a9"
|
||||
integrity sha512-CKt4+Oy9k2wiN+hT1uZzOw7d8zb1anbQpf7KLwaaXRCi/4pzKdFKHf7v5mvoPmjkmxshh7eKZQuRop06r5WP4w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
cosmiconfig "^7.0.0"
|
||||
resolve "^1.19.0"
|
||||
|
||||
babel-plugin-named-asset-import@^0.3.7:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz#156cd55d3f1228a5765774340937afc8398067dd"
|
||||
@@ -4854,6 +4941,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
escape-string-regexp@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||
|
||||
escodegen@^1.14.1:
|
||||
version "1.14.3"
|
||||
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
|
||||
@@ -4878,6 +4970,11 @@ eslint-config-react-app@^6.0.0:
|
||||
dependencies:
|
||||
confusing-browser-globals "^1.0.10"
|
||||
|
||||
eslint-config-typed-fp@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-typed-fp/-/eslint-config-typed-fp-1.3.0.tgz#ca62050793a80c0b9af6f370e925797fa4c243f9"
|
||||
integrity sha512-I6+/szKXAbZQ23pCjVAoqaM0AtYXIfo40QrLKHFfZ/Fh+ROnd2vOawCJzgYfIuPZrjbVbnlsLuM8LTK19YihYA==
|
||||
|
||||
eslint-import-resolver-node@^0.3.4:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
|
||||
@@ -4902,6 +4999,17 @@ eslint-plugin-flowtype@^5.2.0:
|
||||
lodash "^4.17.15"
|
||||
string-natural-compare "^3.0.1"
|
||||
|
||||
eslint-plugin-functional@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-functional/-/eslint-plugin-functional-3.2.1.tgz#d5ad668b57646ad24f4ef0476328408681d59061"
|
||||
integrity sha512-uJ8W0FznWsKp4exxO79b0xSc1WNROzDiVNGgSFOwdZCBeUHQf89BqwqlshNW9aSz/kg2gVGs+Ue6AeTpNSFM/g==
|
||||
dependencies:
|
||||
"@typescript-eslint/experimental-utils" "^4.9.1"
|
||||
array.prototype.flatmap "^1.2.4"
|
||||
deepmerge "^4.2.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
object.fromentries "^2.0.3"
|
||||
|
||||
eslint-plugin-import@^2.22.1:
|
||||
version "2.22.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702"
|
||||
@@ -4981,6 +5089,16 @@ eslint-plugin-testing-library@^3.9.2:
|
||||
dependencies:
|
||||
"@typescript-eslint/experimental-utils" "^3.10.1"
|
||||
|
||||
eslint-plugin-total-functions@^4.7.2:
|
||||
version "4.7.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-total-functions/-/eslint-plugin-total-functions-4.7.2.tgz#e60801da31e1f0e30a2d28b42921ab1f72cb66de"
|
||||
integrity sha512-NG0Is/W+l9vGMbo6wABGGw00Wl6VR0JdN99u28bTKfRzisxnRFJGR/i12jFgQqJHxPxGxC+lLxZ/E5NtwOjo+A==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "^4.8.2"
|
||||
"@typescript-eslint/experimental-utils" "^4.8.2"
|
||||
"@typescript-eslint/parser" "^4.8.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
eslint-scope@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
|
||||
@@ -7389,7 +7507,7 @@ jsprim@^1.2.2:
|
||||
array-includes "^3.1.2"
|
||||
object.assign "^4.1.2"
|
||||
|
||||
jszip@^3.5.0:
|
||||
jszip@*, jszip@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6"
|
||||
integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==
|
||||
@@ -8379,7 +8497,7 @@ object.entries@^1.1.0, object.entries@^1.1.2:
|
||||
es-abstract "^1.18.0-next.1"
|
||||
has "^1.0.3"
|
||||
|
||||
object.fromentries@^2.0.2:
|
||||
object.fromentries@^2.0.2, object.fromentries@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072"
|
||||
integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==
|
||||
@@ -10410,7 +10528,7 @@ resolve@1.18.1:
|
||||
is-core-module "^2.0.0"
|
||||
path-parse "^1.0.6"
|
||||
|
||||
resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1:
|
||||
resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.3.2, resolve@^1.8.1:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
|
||||
integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
|
||||
@@ -11848,6 +11966,14 @@ type@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f"
|
||||
integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==
|
||||
|
||||
typed-redux-saga@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/typed-redux-saga/-/typed-redux-saga-1.3.1.tgz#92b01db41e3510102f87eb9ff261ec73d38a2e44"
|
||||
integrity sha512-nUj1/1/SAesEsZrr7o24ID+++CqZ6QfPVDcwhY2rVmm4vEBr/vbDHJ6j/w6SomOcooLwnh3sdaWVhNEIy7VgNA==
|
||||
optionalDependencies:
|
||||
"@babel/helper-module-imports" "^7.12.1"
|
||||
babel-plugin-macros "^2.8.0"
|
||||
|
||||
typed-styles@^0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
|
||||
|
||||
Reference in New Issue
Block a user