mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
hub/sagas: fix error message not shown when starting repl
- Add check for repl start command success/fail. - Show error message on fail. - Rename start repl action. - Add new did start/did fail to start repl actions. - Change terminal saga to use did start repl action to focus terminal. - Add more tests.
This commit is contained in:
committed by
David Lechner
parent
70020275a7
commit
be3afa167a
+11
-3
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
|
||||
import { createAction } from '../actions';
|
||||
import { FileFormat } from '../ble-pybricks-service/protocol';
|
||||
@@ -43,7 +43,15 @@ export const stop = createAction(() => ({
|
||||
type: 'hub.action.stop',
|
||||
}));
|
||||
|
||||
export const repl = createAction((useLegacyDownload: boolean) => ({
|
||||
type: 'hub.action.repl',
|
||||
export const hubStartRepl = createAction((useLegacyDownload: boolean) => ({
|
||||
type: 'hub.action.startRepl',
|
||||
useLegacyDownload,
|
||||
}));
|
||||
|
||||
export const hubDidStartRepl = createAction(() => ({
|
||||
type: 'hub.action.didStartRepl',
|
||||
}));
|
||||
|
||||
export const hubDidFailToStartRepl = createAction(() => ({
|
||||
type: 'hub.action.didFailToStartRepl',
|
||||
}));
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
didFinishDownload,
|
||||
didProgressDownload,
|
||||
didStartDownload,
|
||||
hubDidFailToStartRepl,
|
||||
hubStartRepl,
|
||||
} from './actions';
|
||||
import reducers, { HubRuntimeState } from './reducers';
|
||||
|
||||
@@ -170,4 +172,20 @@ describe('runtime', () => {
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Idle);
|
||||
});
|
||||
|
||||
test('hubStartRepl', () => {
|
||||
expect(
|
||||
reducers({ runtime: HubRuntimeState.Running } as State, hubStartRepl(false))
|
||||
.runtime,
|
||||
).toBe(HubRuntimeState.StartingRepl);
|
||||
});
|
||||
|
||||
test('hubDidFailToStartRepl', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ runtime: HubRuntimeState.Running } as State,
|
||||
hubDidFailToStartRepl(),
|
||||
).runtime,
|
||||
).toBe(HubRuntimeState.Unknown);
|
||||
});
|
||||
});
|
||||
|
||||
+21
-18
@@ -26,36 +26,28 @@ import {
|
||||
didFinishDownload,
|
||||
didProgressDownload,
|
||||
didStartDownload,
|
||||
hubDidFailToStartRepl,
|
||||
hubStartRepl,
|
||||
} from './actions';
|
||||
|
||||
/**
|
||||
* Describes the state of the MicroPython runtime on the hub.
|
||||
*/
|
||||
export enum HubRuntimeState {
|
||||
/**
|
||||
* The hub is not connected.
|
||||
*/
|
||||
/** The hub is not connected. */
|
||||
Disconnected = 'hub.runtime.disconnected',
|
||||
/**
|
||||
* The hub is connected but the state is not known yet.
|
||||
*/
|
||||
/** The hub is connected but the state is not known yet. */
|
||||
Unknown = 'hub.runtime.unknown',
|
||||
/**
|
||||
* The runtime is idle waiting for command after soft reboot.
|
||||
*/
|
||||
/** The runtime is idle waiting for command after soft reboot. */
|
||||
Idle = 'hub.runtime.idle',
|
||||
/**
|
||||
* A user program is being copied to the hub.
|
||||
*/
|
||||
/** A user program is being copied to the hub. */
|
||||
Loading = 'hub.runtime.loading',
|
||||
/**
|
||||
* A user program has been copied to the hub.
|
||||
*/
|
||||
/** A user program has been copied to the hub. */
|
||||
Loaded = 'hub.runtime.loaded',
|
||||
/**
|
||||
* A user program is running.
|
||||
*/
|
||||
/** A user program is running. */
|
||||
Running = 'hub.runtime.running',
|
||||
/** Busy starting the REPL. */
|
||||
StartingRepl = 'hub.runtime.startingRepl',
|
||||
}
|
||||
|
||||
const runtime: Reducer<HubRuntimeState> = (
|
||||
@@ -116,6 +108,17 @@ const runtime: Reducer<HubRuntimeState> = (
|
||||
return HubRuntimeState.Idle;
|
||||
}
|
||||
|
||||
if (hubStartRepl.matches(action)) {
|
||||
return HubRuntimeState.StartingRepl;
|
||||
}
|
||||
|
||||
// NB: hubDidStartRepl will trigger user program running, so we don't
|
||||
// change state for both to avoid race condition
|
||||
|
||||
if (hubDidFailToStartRepl.matches(action)) {
|
||||
return HubRuntimeState.Unknown;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
|
||||
+50
-8
@@ -1,10 +1,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
// Copyright (c) 2020-2023 The Pybricks Authors
|
||||
|
||||
import { AsyncSaga } from '../../test';
|
||||
import { alertsShowAlert } from '../alerts/actions';
|
||||
import { didWrite, write } from '../ble-nordic-uart-service/actions';
|
||||
import {
|
||||
didFailToSendCommand,
|
||||
didSendCommand,
|
||||
sendStartReplCommand,
|
||||
sendStopUserProgramCommand,
|
||||
} from '../ble-pybricks-service/actions';
|
||||
import { FileFormat } from '../ble-pybricks-service/protocol';
|
||||
@@ -17,7 +20,9 @@ import {
|
||||
didProgressDownload,
|
||||
didStartDownload,
|
||||
downloadAndRun,
|
||||
repl,
|
||||
hubDidFailToStartRepl,
|
||||
hubDidStartRepl,
|
||||
hubStartRepl,
|
||||
stop,
|
||||
} from './actions';
|
||||
import hub from './sagas';
|
||||
@@ -83,15 +88,52 @@ describe('downloadAndRun', () => {
|
||||
// TODO: need to test error paths
|
||||
});
|
||||
|
||||
test('repl', async () => {
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
describe('hubStartRepl', () => {
|
||||
test('legacy UART download', async () => {
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(repl(true));
|
||||
saga.put(hubStartRepl(true));
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toEqual(write(0, new Uint8Array([32, 32, 32, 32])));
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
write(0, new Uint8Array([32, 32, 32, 32])),
|
||||
);
|
||||
|
||||
await saga.end();
|
||||
await expect(saga.take()).resolves.toEqual(hubDidStartRepl());
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('success', async () => {
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(hubStartRepl(false));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendStartReplCommand(0));
|
||||
|
||||
saga.put(didSendCommand(0));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(hubDidStartRepl());
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('failure due to disconnect', async () => {
|
||||
const saga = new AsyncSaga(hub, { nextMessageId: createCountFunc() });
|
||||
|
||||
saga.put(hubStartRepl(false));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(sendStartReplCommand(0));
|
||||
|
||||
saga.put(didFailToSendCommand(0, new DOMException('test', 'NetworkError')));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
alertsShowAlert('ble', 'disconnected'),
|
||||
);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(hubDidFailToStartRepl());
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('stop', async () => {
|
||||
|
||||
+37
-4
@@ -46,7 +46,9 @@ import {
|
||||
didProgressDownload,
|
||||
didStartDownload,
|
||||
downloadAndRun,
|
||||
repl,
|
||||
hubDidFailToStartRepl,
|
||||
hubDidStartRepl,
|
||||
hubStartRepl,
|
||||
stop,
|
||||
} from './actions';
|
||||
|
||||
@@ -343,15 +345,46 @@ function* handleDownloadAndRun(action: ReturnType<typeof downloadAndRun>): Gener
|
||||
// SPACE, SPACE, SPACE, SPACE
|
||||
const legacyStartReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
|
||||
|
||||
function* handleRepl(action: ReturnType<typeof repl>): Generator {
|
||||
function* handleHubStartRepl(action: ReturnType<typeof hubStartRepl>): Generator {
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
|
||||
if (action.useLegacyDownload) {
|
||||
yield* put(write(nextMessageId(), legacyStartReplCommand));
|
||||
yield* put(hubDidStartRepl());
|
||||
return;
|
||||
}
|
||||
|
||||
yield* put(sendStartReplCommand(nextMessageId()));
|
||||
const id = nextMessageId();
|
||||
yield* put(sendStartReplCommand(id));
|
||||
|
||||
const { didFailToSend } = yield* race({
|
||||
didStart: take(didSendCommand.when((a) => a.id === id)),
|
||||
didFailToSend: take(didFailToSendCommand.when((a) => a.id === id)),
|
||||
});
|
||||
|
||||
if (didFailToSend) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(didFailToSend.error);
|
||||
}
|
||||
|
||||
if (
|
||||
didFailToSend.error instanceof DOMException &&
|
||||
didFailToSend.error.name === 'NetworkError'
|
||||
) {
|
||||
yield* put(alertsShowAlert('ble', 'disconnected'));
|
||||
} else {
|
||||
yield* put(
|
||||
alertsShowAlert('alerts', 'unexpectedError', {
|
||||
error: didFailToSend.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
yield* put(hubDidFailToStartRepl());
|
||||
return;
|
||||
}
|
||||
|
||||
yield* put(hubDidStartRepl());
|
||||
}
|
||||
|
||||
function* handleStop(): Generator {
|
||||
@@ -373,7 +406,7 @@ function* handleStop(): Generator {
|
||||
|
||||
export default function* (): Generator {
|
||||
yield* takeEvery(downloadAndRun, handleDownloadAndRun);
|
||||
yield* takeEvery(repl, handleRepl);
|
||||
yield* takeEvery(hubStartRepl, handleHubStartRepl);
|
||||
yield* takeEvery(stop, handleStop);
|
||||
// calling stop right after connecting should get the hub into a known state
|
||||
yield* takeEvery(bleDidConnectPybricks, handleStop);
|
||||
|
||||
Reference in New Issue
Block a user