mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
+4
-6
@@ -1,23 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { call, takeEvery } from 'redux-saga/effects';
|
||||
import { call, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { AppActionType } from '../actions/app';
|
||||
|
||||
function* reload(): Generator {
|
||||
// unregister the service worker so that when the page reloads, it uses
|
||||
// the new version
|
||||
const registrations = (yield call(() =>
|
||||
navigator.serviceWorker.getRegistrations(),
|
||||
)) as ServiceWorkerRegistration[];
|
||||
const registrations = yield* call(() => navigator.serviceWorker.getRegistrations());
|
||||
|
||||
for (const r of registrations) {
|
||||
yield call(() => r.unregister());
|
||||
yield* call(() => r.unregister());
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
export default function* app(): Generator {
|
||||
yield takeEvery(AppActionType.Reload, reload);
|
||||
yield* takeEvery(AppActionType.Reload, reload);
|
||||
}
|
||||
|
||||
+45
-47
@@ -4,7 +4,14 @@
|
||||
// Manages connection to a Bluetooth Low Energy device with the Nordic (nRF) UART service.
|
||||
|
||||
import { END, eventChannel } from 'redux-saga';
|
||||
import { call, cancel, put, select, takeEvery, takeMaybe } from 'redux-saga/effects';
|
||||
import {
|
||||
call,
|
||||
cancel,
|
||||
put,
|
||||
select,
|
||||
takeEvery,
|
||||
takeMaybe,
|
||||
} from 'typed-redux-saga/macro';
|
||||
import {
|
||||
BLEActionType,
|
||||
BleDeviceActionType as BLEDeviceActionType,
|
||||
@@ -42,7 +49,7 @@ function disconnect(
|
||||
}
|
||||
|
||||
function* handleValueChanged(data: DataView): Generator {
|
||||
yield put(notify(data));
|
||||
yield* put(notify(data));
|
||||
}
|
||||
|
||||
function* write(
|
||||
@@ -50,16 +57,16 @@ function* write(
|
||||
action: BleUartWriteAction,
|
||||
): Generator {
|
||||
try {
|
||||
yield call(() => rxChar.writeValueWithoutResponse(action.value.buffer));
|
||||
yield put(didWrite(action.id));
|
||||
yield* call(() => rxChar.writeValueWithoutResponse(action.value.buffer));
|
||||
yield* put(didWrite(action.id));
|
||||
} catch (err) {
|
||||
yield put(didFailToWrite(action.id, err));
|
||||
yield* put(didFailToWrite(action.id, err));
|
||||
}
|
||||
}
|
||||
|
||||
function* connect(_action: BleDeviceConnectAction): Generator {
|
||||
if (navigator.bluetooth === undefined) {
|
||||
yield put(didFailToConnect({ reason: Reason.NoWebBluetooth }));
|
||||
yield* put(didFailToConnect({ reason: Reason.NoWebBluetooth }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,24 +74,24 @@ function* connect(_action: BleDeviceConnectAction): Generator {
|
||||
|
||||
let device: BluetoothDevice;
|
||||
try {
|
||||
device = (yield call(() =>
|
||||
device = yield* call(() =>
|
||||
navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [pybricksServiceUUID] }],
|
||||
optionalServices: [uartServiceUUID],
|
||||
}),
|
||||
)) as BluetoothDevice;
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) {
|
||||
// this can happen if the use cancels the dialog
|
||||
yield put(didFailToConnect({ reason: Reason.Canceled }));
|
||||
yield* put(didFailToConnect({ reason: Reason.Canceled }));
|
||||
} else {
|
||||
yield put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
yield* put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.gatt === undefined) {
|
||||
yield put(didFailToConnect({ reason: Reason.NoGatt }));
|
||||
yield* put(didFailToConnect({ reason: Reason.NoGatt }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,57 +104,48 @@ function* connect(_action: BleDeviceConnectAction): Generator {
|
||||
|
||||
let server: BluetoothRemoteGATTServer;
|
||||
try {
|
||||
server = (yield call([device.gatt, 'connect'])) as BluetoothRemoteGATTServer;
|
||||
server = yield* call([device.gatt, 'connect']);
|
||||
} catch (err) {
|
||||
disconnectChannel.close();
|
||||
yield put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
yield* put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
return;
|
||||
}
|
||||
|
||||
yield takeEvery(BLEDeviceActionType.Disconnect, disconnect, server);
|
||||
yield* takeEvery(BLEDeviceActionType.Disconnect, disconnect, server);
|
||||
|
||||
let service: BluetoothRemoteGATTService;
|
||||
try {
|
||||
service = (yield call(
|
||||
[server, 'getPrimaryService'],
|
||||
uartServiceUUID,
|
||||
)) as BluetoothRemoteGATTService;
|
||||
service = yield* call([server, 'getPrimaryService'], uartServiceUUID);
|
||||
} catch (err) {
|
||||
server.disconnect();
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) {
|
||||
// Possibly/probably caused by Chrome BlueZ back-end bug
|
||||
// https://chromium-review.googlesource.com/c/chromium/src/+/2214098
|
||||
yield put(didFailToConnect({ reason: Reason.NoService }));
|
||||
yield* put(didFailToConnect({ reason: Reason.NoService }));
|
||||
} else {
|
||||
yield put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
yield* put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let rxChar: BluetoothRemoteGATTCharacteristic;
|
||||
try {
|
||||
rxChar = (yield call(
|
||||
[service, 'getCharacteristic'],
|
||||
urtRxCharUUID,
|
||||
)) as BluetoothRemoteGATTCharacteristic;
|
||||
rxChar = yield* call([service, 'getCharacteristic'], urtRxCharUUID);
|
||||
} catch (err) {
|
||||
server.disconnect();
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
yield* put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
return;
|
||||
}
|
||||
|
||||
let txChar: BluetoothRemoteGATTCharacteristic;
|
||||
try {
|
||||
txChar = (yield call(
|
||||
[service, 'getCharacteristic'],
|
||||
uartTxCharUUID,
|
||||
)) as BluetoothRemoteGATTCharacteristic;
|
||||
txChar = yield* call([service, 'getCharacteristic'], uartTxCharUUID);
|
||||
} catch (err) {
|
||||
server.disconnect();
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
yield* put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,27 +167,27 @@ function* connect(_action: BleDeviceConnectAction): Generator {
|
||||
// and reconnecting unless we stop notifications before we start them
|
||||
// again. Wireshark shows that no enable notification descriptor write
|
||||
// is performed but notifications are received.
|
||||
yield call([txChar, 'stopNotifications']);
|
||||
yield call([txChar, 'startNotifications']);
|
||||
yield* call([txChar, 'stopNotifications']);
|
||||
yield* call([txChar, 'startNotifications']);
|
||||
} catch (err) {
|
||||
txChannel.close();
|
||||
server.disconnect();
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
yield* put(didFailToConnect({ reason: Reason.Unknown, err }));
|
||||
return;
|
||||
}
|
||||
|
||||
yield takeEvery(txChannel, handleValueChanged);
|
||||
yield takeEvery(BleUartActionType.Write, write, rxChar);
|
||||
yield* takeEvery(txChannel, handleValueChanged);
|
||||
yield* takeEvery(BleUartActionType.Write, write, rxChar);
|
||||
|
||||
yield put(didConnect());
|
||||
yield* put(didConnect());
|
||||
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
txChannel.close();
|
||||
try {
|
||||
yield cancel(); // have to cancel to stop forked effects
|
||||
yield* cancel(); // have to cancel to stop forked effects
|
||||
} finally {
|
||||
yield put(didDisconnect());
|
||||
yield* put(didDisconnect());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,15 +198,15 @@ function* toggle(_action: BLEToggleAction): Generator {
|
||||
|
||||
switch (connectionState) {
|
||||
case BleConnectionState.Connected:
|
||||
yield put(disconnectAction());
|
||||
yield* put(disconnectAction());
|
||||
break;
|
||||
case BleConnectionState.Disconnected:
|
||||
yield put(connectAction());
|
||||
yield* put(connectAction());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(BLEDeviceActionType.Connect, connect);
|
||||
yield takeEvery(BLEActionType.Toggle, toggle);
|
||||
yield* takeEvery(BLEDeviceActionType.Connect, connect);
|
||||
yield* takeEvery(BLEActionType.Toggle, toggle);
|
||||
}
|
||||
|
||||
+7
-14
@@ -1,9 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Ace } from 'ace-builds';
|
||||
import FileSaver from 'file-saver';
|
||||
import { select, takeEvery } from 'redux-saga/effects';
|
||||
import { select, takeEvery } from 'typed-redux-saga/macro';
|
||||
import {
|
||||
EditorActionType,
|
||||
EditorOpenAction,
|
||||
@@ -15,9 +14,7 @@ import { RootState } from '../reducers';
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
function* open(action: EditorOpenAction): Generator {
|
||||
const editor = (yield select(
|
||||
(s: RootState) => s.editor.current,
|
||||
)) as Ace.EditSession | null;
|
||||
const editor = yield* select((s: RootState) => s.editor.current);
|
||||
|
||||
// istanbul ignore next: it is a bug to dispatch this action with no current editor
|
||||
if (editor === null) {
|
||||
@@ -30,9 +27,7 @@ function* open(action: EditorOpenAction): Generator {
|
||||
}
|
||||
|
||||
function* saveAs(_action: EditorSaveAsAction): Generator {
|
||||
const editor = (yield select(
|
||||
(s: RootState) => s.editor.current,
|
||||
)) as Ace.EditSession | null;
|
||||
const editor = yield* select((s: RootState) => s.editor.current);
|
||||
|
||||
// istanbul ignore next: it is a bug to dispatch this action with no current editor
|
||||
if (editor === null) {
|
||||
@@ -46,9 +41,7 @@ function* saveAs(_action: EditorSaveAsAction): Generator {
|
||||
}
|
||||
|
||||
function* reloadProgram(_action: EditorReloadProgramAction): Generator {
|
||||
const editor = (yield select(
|
||||
(s: RootState) => s.editor.current,
|
||||
)) as Ace.EditSession | null;
|
||||
const editor = yield* select((s: RootState) => s.editor.current);
|
||||
|
||||
// istanbul ignore next: it is a bug to dispatch this action with no current editor
|
||||
if (editor === null) {
|
||||
@@ -60,7 +53,7 @@ function* reloadProgram(_action: EditorReloadProgramAction): Generator {
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(EditorActionType.Open, open);
|
||||
yield takeEvery(EditorActionType.SaveAs, saveAs);
|
||||
yield takeEvery(EditorActionType.ReloadProgram, reloadProgram);
|
||||
yield* takeEvery(EditorActionType.Open, open);
|
||||
yield* takeEvery(EditorActionType.SaveAs, saveAs);
|
||||
yield* takeEvery(EditorActionType.ReloadProgram, reloadProgram);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { takeEvery } from 'redux-saga/effects';
|
||||
import { takeEvery } from 'typed-redux-saga/macro';
|
||||
import {
|
||||
BleDeviceActionType,
|
||||
BleDeviceDidFailToConnectAction,
|
||||
@@ -43,12 +43,12 @@ function licenseDidFailToFetch(action: LicenseDidFailToFetchListAction): void {
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(BleDeviceActionType.DidFailToConnect, bleDeviceDidFailToConnect);
|
||||
yield takeEvery(BleUartActionType.DidFailToWrite, bleDataDidFailToWrite);
|
||||
yield takeEvery(
|
||||
yield* takeEvery(BleDeviceActionType.DidFailToConnect, bleDeviceDidFailToConnect);
|
||||
yield* takeEvery(BleUartActionType.DidFailToWrite, bleDataDidFailToWrite);
|
||||
yield* takeEvery(
|
||||
BootloaderConnectionActionType.DidFailToConnect,
|
||||
bootloaderDidFailToConnect,
|
||||
);
|
||||
yield takeEvery(BootloaderConnectionActionType.DidError, bootloaderDidError);
|
||||
yield takeEvery(LicenseActionType.DidFailToFetchList, licenseDidFailToFetch);
|
||||
yield* takeEvery(BootloaderConnectionActionType.DidError, bootloaderDidError);
|
||||
yield* takeEvery(LicenseActionType.DidFailToFetchList, licenseDidFailToFetch);
|
||||
}
|
||||
|
||||
@@ -408,8 +408,9 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
|
||||
|
||||
const checksum = firmware.reduce((prev, curr) => prev ^ curr, 0xff);
|
||||
if (flash.checksum !== checksum) {
|
||||
// istanbul ignore next
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.log(
|
||||
console.error(
|
||||
'checksum:',
|
||||
flash.checksum.toString(16).padStart(2, '0').padStart(4, '0x'),
|
||||
checksum.toString(16).padStart(2, '0').padStart(4, '0x'),
|
||||
|
||||
+49
-53
@@ -1,11 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Ace } from 'ace-builds';
|
||||
import { Channel } from 'redux-saga';
|
||||
import {
|
||||
RaceEffect,
|
||||
TakeEffect,
|
||||
SagaGenerator,
|
||||
actionChannel,
|
||||
getContext,
|
||||
put,
|
||||
@@ -13,14 +10,13 @@ import {
|
||||
select,
|
||||
take,
|
||||
takeEvery,
|
||||
} from 'redux-saga/effects';
|
||||
} from 'typed-redux-saga/macro';
|
||||
import { Action } from '../actions';
|
||||
import { BleDeviceActionType } from '../actions/ble';
|
||||
import {
|
||||
BleUartActionType,
|
||||
BleUartDidFailToWriteAction,
|
||||
BleUartDidWriteAction,
|
||||
BleUartWriteAction,
|
||||
write,
|
||||
} from '../actions/ble-uart';
|
||||
import {
|
||||
@@ -41,21 +37,29 @@ import {
|
||||
} from '../actions/mpy';
|
||||
import { SafeTxCharLength } from '../protocols/nrf-uart';
|
||||
import { RootState } from '../reducers';
|
||||
import { defined } from '../utils';
|
||||
import { xor8 } from '../utils/math';
|
||||
|
||||
const downloadChunkSize = 100;
|
||||
|
||||
function waitForWrite(id: number): RaceEffect<TakeEffect> {
|
||||
return race([
|
||||
take((a: Action) => a.type === BleUartActionType.DidWrite && a.id === id),
|
||||
take((a: Action) => a.type === BleUartActionType.DidFailToWrite && a.id === id),
|
||||
]);
|
||||
function* waitForWrite(
|
||||
id: number,
|
||||
): SagaGenerator<{
|
||||
didWrite: BleUartDidWriteAction | undefined;
|
||||
didFailToWrite: BleUartDidFailToWriteAction | undefined;
|
||||
}> {
|
||||
return yield* race({
|
||||
didWrite: take<BleUartDidWriteAction>(
|
||||
(a: Action) => a.type === BleUartActionType.DidWrite && a.id === id,
|
||||
),
|
||||
didFailToWrite: take<BleUartDidFailToWriteAction>(
|
||||
(a: Action) => a.type === BleUartActionType.DidFailToWrite && a.id === id,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
|
||||
const editor = (yield select(
|
||||
(s: RootState) => s.editor.current,
|
||||
)) as Ace.EditSession | null;
|
||||
const editor = yield* select((s: RootState) => s.editor.current);
|
||||
|
||||
// istanbul ignore next: it is a bug to dispatch this action with no current editor
|
||||
if (editor === null) {
|
||||
@@ -64,48 +68,45 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
|
||||
}
|
||||
|
||||
const script = editor.getValue();
|
||||
yield put(compile(script, ['-mno-unicode']));
|
||||
const [mpy, mpyFail] = (yield race([
|
||||
take(MpyActionType.DidCompile),
|
||||
take(MpyActionType.DidFailToCompile),
|
||||
])) as [MpyDidCompileAction, MpyDidFailToCompileAction];
|
||||
yield* put(compile(script, ['-mno-unicode']));
|
||||
const { mpy, mpyFail } = yield* race({
|
||||
mpy: take<MpyDidCompileAction>(MpyActionType.DidCompile),
|
||||
mpyFail: take<MpyDidFailToCompileAction>(MpyActionType.DidFailToCompile),
|
||||
});
|
||||
|
||||
if (mpyFail) {
|
||||
return;
|
||||
}
|
||||
|
||||
defined(mpy);
|
||||
|
||||
// let everyone know the runtime is busy loading the program
|
||||
yield put(updateStatus(HubRuntimeStatusType.Loading));
|
||||
yield* put(updateStatus(HubRuntimeStatusType.Loading));
|
||||
|
||||
const checksumChannel = (yield actionChannel(
|
||||
const checksumChannel = yield* actionChannel<HubChecksumMessageAction>(
|
||||
HubMessageActionType.Checksum,
|
||||
)) as Channel<HubChecksumMessageAction>;
|
||||
);
|
||||
|
||||
const nextMessageId = (yield getContext('nextMessageId')) as () => number;
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
|
||||
// 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(nextMessageId(), sizeBuf),
|
||||
)) as BleUartWriteAction;
|
||||
const [, didFailToWrite] = (yield waitForWrite(writeAction.id)) as [
|
||||
BleUartDidWriteAction,
|
||||
BleUartDidFailToWriteAction,
|
||||
];
|
||||
const writeAction = yield* put(write(nextMessageId(), sizeBuf));
|
||||
const { didFailToWrite } = yield* waitForWrite(writeAction.id);
|
||||
|
||||
if (didFailToWrite) {
|
||||
yield put(updateStatus(HubRuntimeStatusType.Error));
|
||||
yield* put(updateStatus(HubRuntimeStatusType.Error));
|
||||
return;
|
||||
}
|
||||
|
||||
const checksumAction = (yield take(checksumChannel)) as HubChecksumMessageAction;
|
||||
const checksumAction = yield* take(checksumChannel);
|
||||
if (checksumAction.checksum !== (0xff ^ xor8(sizeBuf))) {
|
||||
console.error(
|
||||
`bad checksum ${checksumAction.checksum} vs ${0xff ^ xor8(sizeBuf)}`,
|
||||
);
|
||||
yield put(updateStatus(HubRuntimeStatusType.Error));
|
||||
yield* put(updateStatus(HubRuntimeStatusType.Error));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,56 +118,51 @@ 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(
|
||||
const writeAction = yield* put(
|
||||
write(nextMessageId(), chunk.slice(j, j + SafeTxCharLength)),
|
||||
)) as BleUartWriteAction;
|
||||
const [, didFailToWrite] = (yield waitForWrite(writeAction.id)) as [
|
||||
BleUartDidWriteAction,
|
||||
BleUartDidFailToWriteAction,
|
||||
];
|
||||
);
|
||||
const { didFailToWrite } = yield* waitForWrite(writeAction.id);
|
||||
|
||||
if (didFailToWrite) {
|
||||
yield put(updateStatus(HubRuntimeStatusType.Error));
|
||||
yield* put(updateStatus(HubRuntimeStatusType.Error));
|
||||
return;
|
||||
}
|
||||
// TODO: dispatch progress
|
||||
}
|
||||
const checksumAction = (yield take(
|
||||
checksumChannel,
|
||||
)) as HubChecksumMessageAction;
|
||||
const checksumAction = yield* take(checksumChannel);
|
||||
if (checksumAction.checksum !== (0xff ^ xor8(chunk))) {
|
||||
console.error(
|
||||
`bad checksum ${checksumAction.checksum} vs ${0xff ^ xor8(chunk)}`,
|
||||
);
|
||||
yield put(updateStatus(HubRuntimeStatusType.Error));
|
||||
yield* put(updateStatus(HubRuntimeStatusType.Error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// let everyone know the runtime is done loading the program
|
||||
yield put(updateStatus(HubRuntimeStatusType.Loaded));
|
||||
yield* put(updateStatus(HubRuntimeStatusType.Loaded));
|
||||
}
|
||||
|
||||
// SPACE, SPACE, SPACE, SPACE
|
||||
const startReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
|
||||
|
||||
function* startRepl(_action: HubReplAction): Generator {
|
||||
const nextMessageId = (yield getContext('nextMessageId')) as () => number;
|
||||
yield put(write(nextMessageId(), startReplCommand));
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
yield* put(write(nextMessageId(), startReplCommand));
|
||||
}
|
||||
|
||||
// CTRL+C, CTRL+C, CTRL+D
|
||||
const stopCommand = new Uint8Array([0x03, 0x03, 0x04]);
|
||||
|
||||
function* stop(_action: HubStopAction): Generator {
|
||||
const nextMessageId = (yield getContext('nextMessageId')) as () => number;
|
||||
yield put(write(nextMessageId(), stopCommand));
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
yield* put(write(nextMessageId(), stopCommand));
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(HubActionType.DownloadAndRun, downloadAndRun);
|
||||
yield takeEvery(HubActionType.Repl, startRepl);
|
||||
yield takeEvery(HubActionType.Stop, stop);
|
||||
yield* takeEvery(HubActionType.DownloadAndRun, downloadAndRun);
|
||||
yield* takeEvery(HubActionType.Repl, startRepl);
|
||||
yield* takeEvery(HubActionType.Stop, stop);
|
||||
// calling stop right after connecting should get the hub into a known state
|
||||
yield takeEvery(BleDeviceActionType.DidConnect, stop);
|
||||
yield* takeEvery(BleDeviceActionType.DidConnect, stop);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { all, put } from 'redux-saga/effects';
|
||||
import { all, put } from 'typed-redux-saga/macro';
|
||||
import { didStart } from '../actions/app';
|
||||
import app from './app';
|
||||
import bleUart from './ble-uart';
|
||||
@@ -19,7 +19,7 @@ import terminal from './terminal';
|
||||
|
||||
/* istanbul ignore next */
|
||||
export default function* (): Generator {
|
||||
yield all([
|
||||
yield* all([
|
||||
app(),
|
||||
bleUart(),
|
||||
lwp3BootloaderBle(),
|
||||
|
||||
+7
-10
@@ -1,32 +1,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { call, put, select, takeEvery } from 'redux-saga/effects';
|
||||
import { call, put, select, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { AppActionType } from '../actions/app';
|
||||
import { didFailToFetchList, didFetchList } from '../actions/license';
|
||||
import { RootState } from '../reducers';
|
||||
import { LicenseList } from '../reducers/license';
|
||||
|
||||
function* fetchLicenses(): Generator {
|
||||
const licenses = (yield select(
|
||||
(s: RootState) => s.license.list,
|
||||
)) as LicenseList | null;
|
||||
const licenses = yield* select((s: RootState) => s.license.list);
|
||||
|
||||
// if we already have license list, nothing to do
|
||||
if (licenses !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = (yield call(() => fetch('static/oss-licenses.json'))) as Response;
|
||||
const response = yield* call(() => fetch('static/oss-licenses.json'));
|
||||
if (!response.ok || response.body === null) {
|
||||
yield put(didFailToFetchList(response));
|
||||
yield* put(didFailToFetchList(response));
|
||||
return;
|
||||
}
|
||||
|
||||
const list = (yield call(() => response.json())) as LicenseList;
|
||||
yield put(didFetchList(list));
|
||||
const list = yield* call(() => response.json());
|
||||
yield* put(didFetchList(list));
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(AppActionType.OpenLicenseDialog, fetchLicenses);
|
||||
yield* takeEvery(AppActionType.OpenLicenseDialog, fetchLicenses);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Handles Bluetooth Low Energy connection to LEGO Wireless Protocol v3 Bootloader service.
|
||||
|
||||
import { END, eventChannel } from 'redux-saga';
|
||||
import { call, cancel, put, spawn, takeEvery, takeMaybe } from 'redux-saga/effects';
|
||||
import { call, cancel, put, spawn, takeEvery, takeMaybe } from 'typed-redux-saga/macro';
|
||||
import {
|
||||
BootloaderConnectionAction,
|
||||
BootloaderConnectionActionType,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { CharacteristicUUID, ServiceUUID } from '../protocols/lwp3-bootloader';
|
||||
|
||||
function* handleNotify(data: DataView): Generator {
|
||||
yield put(didReceive(data));
|
||||
yield* put(didReceive(data));
|
||||
}
|
||||
|
||||
function* write(
|
||||
@@ -28,19 +28,19 @@ function* write(
|
||||
): Generator {
|
||||
try {
|
||||
if (action.withResponse) {
|
||||
yield call(() => characteristic.writeValueWithResponse(action.data));
|
||||
yield* call(() => characteristic.writeValueWithResponse(action.data));
|
||||
} else {
|
||||
yield call(() => characteristic.writeValueWithoutResponse(action.data));
|
||||
yield* call(() => characteristic.writeValueWithoutResponse(action.data));
|
||||
}
|
||||
yield put(didSend());
|
||||
yield* put(didSend());
|
||||
} catch (err) {
|
||||
yield put(didSend(err));
|
||||
yield* put(didSend(err));
|
||||
}
|
||||
}
|
||||
|
||||
function* connect(_action: BootloaderConnectionAction): Generator {
|
||||
if (navigator.bluetooth === undefined) {
|
||||
yield put(didFailToConnect(Reason.NoWebBluetooth));
|
||||
yield* put(didFailToConnect(Reason.NoWebBluetooth));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,24 +48,24 @@ function* connect(_action: BootloaderConnectionAction): Generator {
|
||||
|
||||
let device: BluetoothDevice;
|
||||
try {
|
||||
device = (yield call(() =>
|
||||
device = yield* call(() =>
|
||||
navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [ServiceUUID] }],
|
||||
optionalServices: [ServiceUUID],
|
||||
}),
|
||||
)) as BluetoothDevice;
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) {
|
||||
// this can happen if the use cancels the dialog
|
||||
yield put(didFailToConnect(Reason.Canceled));
|
||||
yield* put(didFailToConnect(Reason.Canceled));
|
||||
} else {
|
||||
yield put(didFailToConnect(Reason.Unknown, err));
|
||||
yield* put(didFailToConnect(Reason.Unknown, err));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.gatt === undefined) {
|
||||
yield put(
|
||||
yield* put(
|
||||
didFailToConnect(Reason.Unknown, new Error('Device does not support GATT')),
|
||||
);
|
||||
return;
|
||||
@@ -80,42 +80,39 @@ function* connect(_action: BootloaderConnectionAction): Generator {
|
||||
|
||||
let server: BluetoothRemoteGATTServer;
|
||||
try {
|
||||
server = (yield call([device.gatt, 'connect'])) as BluetoothRemoteGATTServer;
|
||||
server = yield* call([device.gatt, 'connect']);
|
||||
} catch (err) {
|
||||
disconnectChannel.close();
|
||||
yield put(didFailToConnect(Reason.Unknown, err));
|
||||
yield* put(didFailToConnect(Reason.Unknown, err));
|
||||
return;
|
||||
}
|
||||
|
||||
let service: BluetoothRemoteGATTService;
|
||||
try {
|
||||
service = (yield call(
|
||||
[server, 'getPrimaryService'],
|
||||
ServiceUUID,
|
||||
)) as BluetoothRemoteGATTService;
|
||||
service = yield* call([server, 'getPrimaryService'], ServiceUUID);
|
||||
} catch (err) {
|
||||
server.disconnect();
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) {
|
||||
// Possibly/probably caused by Chrome BlueZ back-end bug
|
||||
// https://chromium-review.googlesource.com/c/chromium/src/+/2214098
|
||||
yield put(didFailToConnect(Reason.GattServiceNotFound));
|
||||
yield* put(didFailToConnect(Reason.GattServiceNotFound));
|
||||
} else {
|
||||
yield put(didFailToConnect(Reason.Unknown, err));
|
||||
yield* put(didFailToConnect(Reason.Unknown, err));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let characteristic: BluetoothRemoteGATTCharacteristic;
|
||||
try {
|
||||
characteristic = (yield call(
|
||||
characteristic = yield* call(
|
||||
[service, 'getCharacteristic'],
|
||||
CharacteristicUUID,
|
||||
)) as BluetoothRemoteGATTCharacteristic;
|
||||
);
|
||||
} catch (err) {
|
||||
server.disconnect();
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield put(didFailToConnect(Reason.Unknown, err));
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
yield* put(didFailToConnect(Reason.Unknown, err));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,19 +130,19 @@ function* connect(_action: BootloaderConnectionAction): Generator {
|
||||
|
||||
try {
|
||||
try {
|
||||
yield call([characteristic, 'stopNotifications']);
|
||||
yield* call([characteristic, 'stopNotifications']);
|
||||
} catch {
|
||||
// HACK: Chromium on Linux (BlueZ) will not receive notifications
|
||||
// if a device disconnects while notifications are enabled and then
|
||||
// reconnects. So we have to call stopNotifications() first to get
|
||||
// back to a known state. https://crbug.com/1170085
|
||||
}
|
||||
yield call([characteristic, 'startNotifications']);
|
||||
yield* call([characteristic, 'startNotifications']);
|
||||
} catch (err) {
|
||||
notificationChannel.close();
|
||||
server.disconnect();
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield put(didFailToConnect(Reason.Unknown, err));
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
yield* put(didFailToConnect(Reason.Unknown, err));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,27 +150,27 @@ function* connect(_action: BootloaderConnectionAction): Generator {
|
||||
// other sagas always expect it to complete with success action or error
|
||||
// action.
|
||||
function* spawnWrite(action: BootloaderConnectionSendAction): Generator {
|
||||
yield spawn(write, characteristic, action);
|
||||
yield* spawn(write, characteristic, action);
|
||||
}
|
||||
|
||||
yield takeEvery(notificationChannel, handleNotify);
|
||||
yield takeEvery(BootloaderConnectionActionType.Send, spawnWrite);
|
||||
yield takeEvery(
|
||||
yield* takeEvery(notificationChannel, handleNotify);
|
||||
yield* takeEvery(BootloaderConnectionActionType.Send, spawnWrite);
|
||||
yield* takeEvery(
|
||||
BootloaderConnectionActionType.Disconnect,
|
||||
server.disconnect.bind(server),
|
||||
);
|
||||
|
||||
yield put(didConnect());
|
||||
yield* put(didConnect());
|
||||
|
||||
yield takeMaybe(disconnectChannel);
|
||||
yield* takeMaybe(disconnectChannel);
|
||||
notificationChannel.close();
|
||||
try {
|
||||
yield cancel(); // have to cancel to stop forked effects
|
||||
yield* cancel(); // have to cancel to stop forked effects
|
||||
} finally {
|
||||
yield put(didDisconnect());
|
||||
yield* put(didDisconnect());
|
||||
}
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(BootloaderConnectionActionType.Connect, connect);
|
||||
yield* takeEvery(BootloaderConnectionActionType.Connect, connect);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
// File: sagas/lwp3-bootloader-protocol.ts
|
||||
// Handles LEGO Wireless Protocol v3 Bootloader protocol.
|
||||
|
||||
import { Channel } from 'redux-saga';
|
||||
import { actionChannel, fork, put, take, takeEvery } from 'redux-saga/effects';
|
||||
import { actionChannel, fork, put, take, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { Action } from '../actions';
|
||||
import {
|
||||
BootloaderConnectionActionType,
|
||||
@@ -53,13 +52,14 @@ import { hex } from '../utils';
|
||||
function* encodeRequest(): Generator {
|
||||
// Using a while loop to serialize sending data to avoid "busy" errors.
|
||||
|
||||
const chan = (yield actionChannel((a: Action) =>
|
||||
const chan = yield* actionChannel<BootloaderRequestAction>((a: Action) =>
|
||||
Object.values(BootloaderRequestActionType).includes(
|
||||
a.type as BootloaderRequestActionType,
|
||||
),
|
||||
)) as Channel<BootloaderRequestAction>;
|
||||
);
|
||||
|
||||
while (true) {
|
||||
const action = (yield take(chan)) as BootloaderRequestAction;
|
||||
const action = yield* take(chan);
|
||||
|
||||
// NB: Commands other than program on city hub will cause BlueZ to
|
||||
// disconnect because they will send a response even if we write without
|
||||
@@ -70,10 +70,10 @@ function* encodeRequest(): Generator {
|
||||
|
||||
switch (action.type) {
|
||||
case BootloaderRequestActionType.Erase:
|
||||
yield put(send(createEraseFlashRequest()));
|
||||
yield* put(send(createEraseFlashRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.Program:
|
||||
yield put(
|
||||
yield* put(
|
||||
send(
|
||||
createProgramFlashRequest(action.address, action.payload),
|
||||
/* withResponse */ false,
|
||||
@@ -81,22 +81,22 @@ function* encodeRequest(): Generator {
|
||||
);
|
||||
break;
|
||||
case BootloaderRequestActionType.Reboot:
|
||||
yield put(send(createStartAppRequest(), /* withResponse */ false));
|
||||
yield* put(send(createStartAppRequest(), /* withResponse */ false));
|
||||
break;
|
||||
case BootloaderRequestActionType.Init:
|
||||
yield put(send(createInitLoaderRequest(action.firmwareSize)));
|
||||
yield* put(send(createInitLoaderRequest(action.firmwareSize)));
|
||||
break;
|
||||
case BootloaderRequestActionType.Info:
|
||||
yield put(send(createGetInfoRequest()));
|
||||
yield* put(send(createGetInfoRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.Checksum:
|
||||
yield put(send(createGetChecksumRequest()));
|
||||
yield* put(send(createGetChecksumRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.State:
|
||||
yield put(send(createGetFlashStateRequest()));
|
||||
yield* put(send(createGetFlashStateRequest()));
|
||||
break;
|
||||
case BootloaderRequestActionType.Disconnect:
|
||||
yield put(send(createDisconnectRequest(), /* withResponse */ false));
|
||||
yield* put(send(createDisconnectRequest(), /* withResponse */ false));
|
||||
break;
|
||||
/* istanbul ignore next: should not be possible to reach */
|
||||
default:
|
||||
@@ -104,10 +104,10 @@ function* encodeRequest(): Generator {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sent = (yield take(
|
||||
const sent = yield* take<BootloaderConnectionDidSendAction>(
|
||||
BootloaderConnectionActionType.DidSend,
|
||||
)) as BootloaderConnectionDidSendAction;
|
||||
yield put(didRequest(action.id, sent.err));
|
||||
);
|
||||
yield* put(didRequest(action.id, sent.err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,25 +120,25 @@ function* decodeResponse(action: BootloaderConnectionDidReceiveAction): Generato
|
||||
const responseType = getMessageType(action.data);
|
||||
switch (responseType) {
|
||||
case Command.EraseFlash:
|
||||
yield put(eraseResponse(parseEraseFlashResponse(action.data)));
|
||||
yield* put(eraseResponse(parseEraseFlashResponse(action.data)));
|
||||
break;
|
||||
case Command.ProgramFlash:
|
||||
yield put(programResponse(...parseProgramFlashResponse(action.data)));
|
||||
yield* put(programResponse(...parseProgramFlashResponse(action.data)));
|
||||
break;
|
||||
case Command.InitLoader:
|
||||
yield put(initResponse(parseInitLoaderResponse(action.data)));
|
||||
yield* put(initResponse(parseInitLoaderResponse(action.data)));
|
||||
break;
|
||||
case Command.GetInfo:
|
||||
yield put(infoResponse(...parseGetInfoResponse(action.data)));
|
||||
yield* put(infoResponse(...parseGetInfoResponse(action.data)));
|
||||
break;
|
||||
case Command.GetChecksum:
|
||||
yield put(checksumResponse(parseGetChecksumResponse(action.data)));
|
||||
yield* put(checksumResponse(parseGetChecksumResponse(action.data)));
|
||||
break;
|
||||
case Command.GetFlashState:
|
||||
yield put(stateResponse(parseGetFlashStateResponse(action.data)));
|
||||
yield* put(stateResponse(parseGetFlashStateResponse(action.data)));
|
||||
break;
|
||||
case ErrorBytecode:
|
||||
yield put(errorResponse(parseErrorResponse(action.data)));
|
||||
yield* put(errorResponse(parseErrorResponse(action.data)));
|
||||
break;
|
||||
default:
|
||||
throw new ProtocolError(
|
||||
@@ -147,11 +147,11 @@ function* decodeResponse(action: BootloaderConnectionDidReceiveAction): Generato
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(didError(err));
|
||||
yield* put(didError(err));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield fork(encodeRequest);
|
||||
yield takeEvery(BootloaderConnectionActionType.DidReceive, decodeResponse);
|
||||
yield* fork(encodeRequest);
|
||||
yield* takeEvery(BootloaderConnectionActionType.DidReceive, decodeResponse);
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { CompileResult, compile as mpyCrossCompile } from '@pybricks/mpy-cross-v5';
|
||||
import { compile as mpyCrossCompile } from '@pybricks/mpy-cross-v5';
|
||||
import wasm from '@pybricks/mpy-cross-v5/build/mpy-cross.wasm';
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
import { call, put, takeEvery } from 'typed-redux-saga/macro';
|
||||
import {
|
||||
MpyActionType,
|
||||
MpyCompileAction,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
* @param action A mpy compile action.
|
||||
*/
|
||||
function* compile(action: MpyCompileAction): Generator {
|
||||
const result = (yield call(() =>
|
||||
const result = yield* call(() =>
|
||||
mpyCrossCompile(
|
||||
'main.py',
|
||||
action.script,
|
||||
@@ -25,14 +25,14 @@ function* compile(action: MpyCompileAction): Generator {
|
||||
// HACK: testing user agent for jsdom is needed only for getting unit tests to work
|
||||
navigator.userAgent.includes('jsdom') ? undefined : wasm,
|
||||
),
|
||||
)) as CompileResult;
|
||||
);
|
||||
if (result.status === 0 && result.mpy) {
|
||||
yield put(didCompile(result.mpy));
|
||||
yield* put(didCompile(result.mpy));
|
||||
} else {
|
||||
yield put(didFailToCompile(result.err));
|
||||
yield* put(didFailToCompile(result.err));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(MpyActionType.Compile, compile);
|
||||
yield* takeEvery(MpyActionType.Compile, compile);
|
||||
}
|
||||
|
||||
+21
-18
@@ -13,7 +13,7 @@ import {
|
||||
import { Replacements } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { channel } from 'redux-saga';
|
||||
import { delay, getContext, put, take, takeEvery } from 'redux-saga/effects';
|
||||
import { delay, getContext, put, take, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { reload } from '../actions/app';
|
||||
import {
|
||||
BleDeviceActionType,
|
||||
@@ -119,7 +119,7 @@ function* showSingleton(
|
||||
action?: IActionProps & ILinkProps,
|
||||
onDismiss?: (didTimeoutExpire: boolean) => void,
|
||||
): Generator {
|
||||
const { toaster } = (yield getContext('notification')) as NotificationContext;
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
|
||||
// if the message is already showing, close it and wait some time so that
|
||||
// users can see that something triggered the message again
|
||||
@@ -130,7 +130,7 @@ function* showSingleton(
|
||||
.includes(messageId)
|
||||
) {
|
||||
toaster.dismiss(messageId);
|
||||
yield delay(500);
|
||||
yield* delay(500);
|
||||
}
|
||||
|
||||
toaster.show(
|
||||
@@ -148,7 +148,7 @@ function* showSingleton(
|
||||
|
||||
/** Shows a special notification for unexpected errors. */
|
||||
function* showUnexpectedError(messageId: MessageId, err: Error): Generator {
|
||||
const { toaster } = (yield getContext('notification')) as NotificationContext;
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
toaster.show({
|
||||
intent: mapIntent(Level.Error),
|
||||
icon: mapIcon(Level.Error),
|
||||
@@ -226,9 +226,9 @@ function* showEditorStorageChanged(): Generator {
|
||||
|
||||
// if the notification is dismissed without clicking on the action, the
|
||||
// saga will be cancelled here
|
||||
yield take(ch);
|
||||
yield* take(ch);
|
||||
|
||||
yield put(reloadProgram());
|
||||
yield* put(reloadProgram());
|
||||
}
|
||||
|
||||
function* showFlashFirmwareError(
|
||||
@@ -249,6 +249,7 @@ function* showFlashFirmwareError(
|
||||
break;
|
||||
case FailToFinishReasonType.HubError:
|
||||
yield* showSingleton(Level.Error, MessageId.FlashFirmwareHubError);
|
||||
// istanbul ignore next
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(action.reason.hubError);
|
||||
}
|
||||
@@ -266,12 +267,14 @@ function* showFlashFirmwareError(
|
||||
break;
|
||||
case FailToFinishReasonType.ZipError:
|
||||
yield* showSingleton(Level.Error, MessageId.FlashFirmwareBadZipFile);
|
||||
// istanbul ignore next
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(action.reason.err);
|
||||
}
|
||||
break;
|
||||
case FailToFinishReasonType.BadMetadata:
|
||||
yield* showSingleton(Level.Error, MessageId.FlashFirmwareBadMetadata);
|
||||
// istanbul ignore next
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(action.reason.property, action.reason.problem);
|
||||
}
|
||||
@@ -292,7 +295,7 @@ function* showFlashFirmwareError(
|
||||
}
|
||||
|
||||
function* dismissCompilerError(): Generator {
|
||||
const { toaster } = (yield getContext('notification')) as NotificationContext;
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
toaster.dismiss(MessageId.MpyError);
|
||||
}
|
||||
|
||||
@@ -303,7 +306,7 @@ function* showCompilerError(action: MpyDidFailToCompileAction): Generator {
|
||||
}
|
||||
|
||||
function* addNotification(action: NotificationAddAction): Generator {
|
||||
const { toaster } = (yield getContext('notification')) as NotificationContext;
|
||||
const { toaster } = yield* getContext<NotificationContext>('notification');
|
||||
|
||||
toaster.show({
|
||||
intent: mapIntent(action.level as Level),
|
||||
@@ -332,24 +335,24 @@ function* showServiceWorkerUpdate(): Generator {
|
||||
ch.close,
|
||||
);
|
||||
|
||||
yield take(ch);
|
||||
yield* take(ch);
|
||||
|
||||
yield put(reload());
|
||||
yield* put(reload());
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(
|
||||
yield* takeEvery(
|
||||
BleDeviceActionType.DidFailToConnect,
|
||||
showBleDeviceDidFailToConnectError,
|
||||
);
|
||||
yield takeEvery(
|
||||
yield* takeEvery(
|
||||
BootloaderConnectionActionType.DidFailToConnect,
|
||||
showBootloaderDidFailToConnectError,
|
||||
);
|
||||
yield takeEvery(EditorActionType.StorageChanged, showEditorStorageChanged);
|
||||
yield takeEvery(FlashFirmwareActionType.DidFailToFinish, showFlashFirmwareError);
|
||||
yield takeEvery(MpyActionType.DidCompile, dismissCompilerError);
|
||||
yield takeEvery(MpyActionType.DidFailToCompile, showCompilerError);
|
||||
yield takeEvery(NotificationActionType.Add, addNotification);
|
||||
yield takeEvery(ServiceWorkerActionType.DidUpdate, showServiceWorkerUpdate);
|
||||
yield* takeEvery(EditorActionType.StorageChanged, showEditorStorageChanged);
|
||||
yield* takeEvery(FlashFirmwareActionType.DidFailToFinish, showFlashFirmwareError);
|
||||
yield* takeEvery(MpyActionType.DidCompile, dismissCompilerError);
|
||||
yield* takeEvery(MpyActionType.DidFailToCompile, showCompilerError);
|
||||
yield* takeEvery(NotificationActionType.Add, addNotification);
|
||||
yield* takeEvery(ServiceWorkerActionType.DidUpdate, showServiceWorkerUpdate);
|
||||
}
|
||||
|
||||
+10
-12
@@ -6,7 +6,7 @@
|
||||
// change action that can be used by reducers to compute the new state.
|
||||
|
||||
import { EventChannel, eventChannel } from 'redux-saga';
|
||||
import { call, fork, put, select, take, takeEvery } from 'redux-saga/effects';
|
||||
import { call, fork, put, select, take, takeEvery } from 'typed-redux-saga/macro';
|
||||
import { AppActionType } from '../actions/app';
|
||||
import {
|
||||
SettingsActionType,
|
||||
@@ -36,12 +36,10 @@ function createLocalStorageEventChannel(): EventChannel<StorageEvent> {
|
||||
}
|
||||
|
||||
function* monitorLocalStorage(): Generator {
|
||||
const chan = (yield call(
|
||||
createLocalStorageEventChannel,
|
||||
)) as EventChannel<StorageEvent>;
|
||||
const chan = yield* call(createLocalStorageEventChannel);
|
||||
|
||||
while (true) {
|
||||
const event = (yield take(chan)) as StorageEvent;
|
||||
const event = yield* take(chan);
|
||||
|
||||
// only care about storage keys 'setting.*'
|
||||
if (!event.key?.startsWith('setting.')) {
|
||||
@@ -56,7 +54,7 @@ function* monitorLocalStorage(): Generator {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield put(didBooleanChange(id, stringToBoolean(event.newValue || 'false')));
|
||||
yield* put(didBooleanChange(id, stringToBoolean(event.newValue || 'false')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +66,7 @@ function* loadSettings(): Generator {
|
||||
storageValue === null ? defaultValue : stringToBoolean(storageValue);
|
||||
|
||||
if (value !== defaultValue) {
|
||||
yield put(didBooleanChange(id, value));
|
||||
yield* put(didBooleanChange(id, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,13 +78,13 @@ function* storeSetting(action: SettingsSetBooleanAction): Generator {
|
||||
try {
|
||||
localStorage.setItem(key, newValue);
|
||||
} catch (err) {
|
||||
yield put(didFailToSetBoolean(action.id, err));
|
||||
yield* put(didFailToSetBoolean(action.id, err));
|
||||
}
|
||||
|
||||
// storage event is only raised when a value is changed externally, so we
|
||||
// mimic the event when we call setItem(), whether it actually succeeded
|
||||
// or not.
|
||||
const oldState = (yield select((s: RootState) => s.settings[action.id])) as boolean;
|
||||
const oldState = yield* select((s: RootState) => s.settings[action.id]);
|
||||
if (action.newState !== oldState) {
|
||||
window.dispatchEvent(
|
||||
new StorageEvent('storage', {
|
||||
@@ -100,7 +98,7 @@ function* storeSetting(action: SettingsSetBooleanAction): Generator {
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield fork(monitorLocalStorage);
|
||||
yield takeEvery(AppActionType.DidStart, loadSettings);
|
||||
yield takeEvery(SettingsActionType.SetBoolean, storeSetting);
|
||||
yield* fork(monitorLocalStorage);
|
||||
yield* takeEvery(AppActionType.DidStart, loadSettings);
|
||||
yield* takeEvery(SettingsActionType.SetBoolean, storeSetting);
|
||||
}
|
||||
|
||||
+27
-31
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { Channel } from 'redux-saga';
|
||||
import {
|
||||
actionChannel,
|
||||
delay,
|
||||
@@ -12,16 +11,11 @@ import {
|
||||
select,
|
||||
take,
|
||||
takeEvery,
|
||||
} from 'redux-saga/effects';
|
||||
} from 'typed-redux-saga/macro';
|
||||
import PushStream from 'zen-push';
|
||||
import { Action } from '../actions';
|
||||
import { AppActionType, AppDidStartAction } from '../actions/app';
|
||||
import {
|
||||
BleUartActionType,
|
||||
BleUartNotifyAction,
|
||||
BleUartWriteAction,
|
||||
write,
|
||||
} from '../actions/ble-uart';
|
||||
import { BleUartActionType, BleUartNotifyAction, write } from '../actions/ble-uart';
|
||||
import { HubRuntimeStatusType, checksum, updateStatus } from '../actions/hub';
|
||||
import {
|
||||
TerminalActionType,
|
||||
@@ -32,13 +26,14 @@ import {
|
||||
import { SafeTxCharLength } from '../protocols/nrf-uart';
|
||||
import { RootState } from '../reducers';
|
||||
import { HubRuntimeState } from '../reducers/hub';
|
||||
import { defined } from '../utils';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const terminalDataSource = new PushStream<string>();
|
||||
|
||||
function* startup(_action: AppDidStartAction): Generator {
|
||||
yield put(setDataSource(terminalDataSource.observable));
|
||||
yield* put(setDataSource(terminalDataSource.observable));
|
||||
}
|
||||
|
||||
function* handleMatch(
|
||||
@@ -50,24 +45,24 @@ function* handleMatch(
|
||||
}
|
||||
|
||||
if (match[1]) {
|
||||
yield put(sendData(match[1]));
|
||||
yield* put(sendData(match[1]));
|
||||
}
|
||||
|
||||
yield put(updateStatus(status));
|
||||
yield* put(updateStatus(status));
|
||||
|
||||
if (match[2]) {
|
||||
yield put(sendData(match[2]));
|
||||
yield* put(sendData(match[2]));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function* receiveUartData(action: BleUartNotifyAction): Generator {
|
||||
const hubState = (yield select((s: RootState) => s.hub.runtime)) as HubRuntimeState;
|
||||
const hubState = yield* select((s: RootState) => s.hub.runtime);
|
||||
|
||||
if (hubState === HubRuntimeState.Loading && action.value.buffer.byteLength === 1) {
|
||||
const view = new DataView(action.value.buffer);
|
||||
yield put(checksum(view.getUint8(0)));
|
||||
yield* put(checksum(view.getUint8(0)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,40 +92,41 @@ function* receiveUartData(action: BleUartNotifyAction): Generator {
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(sendData(value));
|
||||
yield* put(sendData(value));
|
||||
}
|
||||
|
||||
function* receiveTerminalData(): Generator {
|
||||
const channel = (yield actionChannel(
|
||||
const channel = yield* actionChannel<TerminalDataReceiveDataAction>(
|
||||
TerminalActionType.ReceivedData,
|
||||
)) as Channel<TerminalDataReceiveDataAction>;
|
||||
);
|
||||
while (true) {
|
||||
// wait for input from terminal
|
||||
const action = (yield take(channel)) as TerminalDataReceiveDataAction;
|
||||
const action = yield* take(channel);
|
||||
let value = action.value;
|
||||
|
||||
// Try to collect more data so that we aren't sending just one byte at time
|
||||
while (value.length < SafeTxCharLength) {
|
||||
const [action, timeout] = (yield race([take(channel), delay(20)])) as [
|
||||
TerminalDataReceiveDataAction,
|
||||
boolean,
|
||||
];
|
||||
const { action, timeout } = yield* race({
|
||||
action: take(channel),
|
||||
timeout: delay(20),
|
||||
});
|
||||
if (timeout) {
|
||||
break;
|
||||
}
|
||||
defined(action);
|
||||
value += action.value;
|
||||
}
|
||||
|
||||
const nextMessageId = (yield getContext('nextMessageId')) as () => number;
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
|
||||
// stdin gets piped to BLE connection
|
||||
const data = encoder.encode(value);
|
||||
for (let i = 0; i < data.length; i += SafeTxCharLength) {
|
||||
const { id } = (yield put(
|
||||
const { id } = yield* put(
|
||||
write(nextMessageId(), data.slice(i, i + SafeTxCharLength)),
|
||||
)) as BleUartWriteAction;
|
||||
);
|
||||
|
||||
yield take(
|
||||
yield* take(
|
||||
(a: Action) =>
|
||||
(a.type === BleUartActionType.DidWrite ||
|
||||
a.type === BleUartActionType.DidFailToWrite) &&
|
||||
@@ -138,7 +134,7 @@ function* receiveTerminalData(): Generator {
|
||||
);
|
||||
|
||||
// wait for echo so tht we don't overrun the hub with messages
|
||||
yield race([take(BleUartActionType.Notify), delay(100)]);
|
||||
yield* race([take(BleUartActionType.Notify), delay(100)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,8 +145,8 @@ function sendTerminalData(action: TerminalDataReceiveDataAction): void {
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(AppActionType.DidStart, startup);
|
||||
yield takeEvery(BleUartActionType.Notify, receiveUartData);
|
||||
yield fork(receiveTerminalData);
|
||||
yield takeEvery(TerminalActionType.SendData, sendTerminalData);
|
||||
yield* takeEvery(AppActionType.DidStart, startup);
|
||||
yield* takeEvery(BleUartActionType.Notify, receiveUartData);
|
||||
yield* fork(receiveTerminalData);
|
||||
yield* takeEvery(TerminalActionType.SendData, sendTerminalData);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user