mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
remove use of thunk action in ble and hub
This is a step towards moving these to sagas
This commit is contained in:
committed by
David Lechner
parent
99fd62100d
commit
24acab901c
@@ -0,0 +1,140 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import {
|
||||
BLEActionType,
|
||||
BLEConnectActionType,
|
||||
BLEDataActionType,
|
||||
connect as connectAction,
|
||||
didConnect,
|
||||
didDisconnect,
|
||||
disconnect as disconnectAction,
|
||||
notify,
|
||||
} from '../actions/ble';
|
||||
import * as notification from '../actions/notification';
|
||||
import { RootState } from '../reducers';
|
||||
import { BLEConnectionState } from '../reducers/ble';
|
||||
import {
|
||||
PolyfillBluetoothRemoteGATTCharacteristic,
|
||||
polyfillBluetoothRemoteGATTCharacteristic,
|
||||
} from '../utils/web-bluetooth';
|
||||
import { combineServices } from '.';
|
||||
|
||||
const pybricksServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef';
|
||||
|
||||
// nRF UART service (Nus)
|
||||
const bleNusServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusCharRXUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusCharTXUUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusMaxSize = 20;
|
||||
|
||||
let device: BluetoothDevice | undefined;
|
||||
let rxChar: PolyfillBluetoothRemoteGATTCharacteristic | undefined;
|
||||
|
||||
async function connect(action: Action, dispatch: Dispatch): Promise<void> {
|
||||
if (action.type !== BLEConnectActionType.Connect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device !== undefined) {
|
||||
dispatch(notification.add('error', 'A device is already connected.'));
|
||||
return;
|
||||
}
|
||||
if (navigator.bluetooth === undefined) {
|
||||
dispatch(
|
||||
notification.add(
|
||||
'error',
|
||||
'This web browser does not support Web Bluetooth or it is not enabled.',
|
||||
'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// TODO: check navigator.bluetooth.getAvailability()
|
||||
try {
|
||||
device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [pybricksServiceUUID] }],
|
||||
optionalServices: [bleNusServiceUUID],
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) {
|
||||
// this can happen if the use cancels the dialog
|
||||
console.debug('User cancelled connect');
|
||||
} else {
|
||||
console.error(err);
|
||||
dispatch(
|
||||
notification.add(
|
||||
'error',
|
||||
'Unexpected error, check developer console for details.',
|
||||
),
|
||||
);
|
||||
}
|
||||
dispatch(didDisconnect());
|
||||
return;
|
||||
}
|
||||
if (device.gatt === undefined) {
|
||||
dispatch(notification.add('error', 'Device does not support GATT.'));
|
||||
dispatch(didDisconnect());
|
||||
return;
|
||||
}
|
||||
device.addEventListener('gattserverdisconnected', () => {
|
||||
device = undefined;
|
||||
rxChar = undefined;
|
||||
dispatch(didDisconnect());
|
||||
});
|
||||
const server = await device.gatt.connect();
|
||||
try {
|
||||
const service = await server.getPrimaryService(bleNusServiceUUID);
|
||||
rxChar = polyfillBluetoothRemoteGATTCharacteristic(
|
||||
await service.getCharacteristic(bleNusCharRXUUID),
|
||||
);
|
||||
const txChar = await service.getCharacteristic(bleNusCharTXUUID);
|
||||
txChar.addEventListener('characteristicvaluechanged', () => {
|
||||
if (!txChar.value) {
|
||||
return;
|
||||
}
|
||||
dispatch(notify(txChar.value));
|
||||
});
|
||||
await txChar.startNotifications();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dispatch(notification.add('error', 'Getting nRF UART service failed.'));
|
||||
device.gatt.disconnect();
|
||||
return;
|
||||
}
|
||||
dispatch(didConnect());
|
||||
}
|
||||
|
||||
function disconnect(action: Action): void {
|
||||
if (action.type !== BLEConnectActionType.Disconnect) {
|
||||
return;
|
||||
}
|
||||
device?.gatt?.disconnect();
|
||||
}
|
||||
|
||||
async function write(action: Action): Promise<void> {
|
||||
if (action.type !== BLEDataActionType.Write) {
|
||||
return;
|
||||
}
|
||||
const value = action.value.buffer;
|
||||
for (let i = 0; i < value.byteLength; i += bleNusMaxSize) {
|
||||
await rxChar?.xWriteValueWithoutResponse(value.slice(i, i + bleNusMaxSize));
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(action: Action, dispatch: Dispatch, state: RootState): void {
|
||||
if (action.type !== BLEActionType.Toggle) {
|
||||
return;
|
||||
}
|
||||
switch (state.ble.connection) {
|
||||
case BLEConnectionState.Connected:
|
||||
dispatch(disconnectAction());
|
||||
break;
|
||||
case BLEConnectionState.Disconnected:
|
||||
dispatch(connectAction());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineServices(connect, disconnect, write, toggle);
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { AnyAction } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { Action } from '../actions';
|
||||
import { write } from '../actions/ble';
|
||||
import { HubActionType, HubRuntimeStatusType, updateStatus } from '../actions/hub';
|
||||
import { compile } from '../actions/mpy';
|
||||
import { getChecksum } from '../epics/hub';
|
||||
import { RootState } from '../reducers';
|
||||
import { combineServices } from '.';
|
||||
|
||||
// TODO: this file needs to be converted to a saga
|
||||
|
||||
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
|
||||
|
||||
const downloadChunkSize = 100;
|
||||
|
||||
async function downloadAndRun(
|
||||
action: Action,
|
||||
dispatch: Dispatch,
|
||||
state: RootState,
|
||||
): Promise<void> {
|
||||
if (action.type !== HubActionType.DownloadAndRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = state.editor.current?.getValue();
|
||||
// istanbul ignore next: it should not be possible to trigger this action without a current editor
|
||||
if (script === undefined) {
|
||||
console.log('no current editor');
|
||||
return;
|
||||
}
|
||||
const mpy = await dispatch(compile(script, ['-mno-unicode']));
|
||||
if (mpy.data === undefined) {
|
||||
console.log(`failed to compile: ${mpy.err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// let everyone know the runtime is busy loading the program
|
||||
dispatch(updateStatus(HubRuntimeStatusType.Loading));
|
||||
|
||||
// TODO: might need to flush checksum queue here
|
||||
|
||||
// first send payload size as big-endian 32-bit integer
|
||||
const checksum = getChecksum();
|
||||
const sizeBuf = new Uint8Array(4);
|
||||
const sizeView = new DataView(sizeBuf.buffer);
|
||||
sizeView.setUint32(0, mpy.data.byteLength, true);
|
||||
await dispatch(write(sizeBuf));
|
||||
// TODO: verify checksum
|
||||
console.log(await checksum);
|
||||
|
||||
// Then send payload in 100 byte chunks waiting for checksum after
|
||||
// each chunk
|
||||
for (let i = 0; i < mpy.data.byteLength; i += downloadChunkSize) {
|
||||
// need to subscribe to checksum before writing to prevent race condition
|
||||
const checksum = getChecksum();
|
||||
await dispatch(write(mpy.data.slice(i, i + downloadChunkSize)));
|
||||
// TODO: verify checksum
|
||||
console.log(await checksum);
|
||||
// TODO: dispatch progress
|
||||
}
|
||||
|
||||
// let everyone know the runtime is done loading the program
|
||||
dispatch(updateStatus(HubRuntimeStatusType.Loaded));
|
||||
}
|
||||
|
||||
// SPACE, SPACE, SPACE, SPACE
|
||||
const startReplCommand = new Uint8Array([0x20, 0x20, 0x20, 0x20]);
|
||||
|
||||
function startRepl(action: Action, dispatch: Dispatch): void {
|
||||
if (action.type !== HubActionType.Repl) {
|
||||
return;
|
||||
}
|
||||
dispatch(write(startReplCommand));
|
||||
}
|
||||
|
||||
// CTRL+C, CTRL+C, CTRL+D
|
||||
const stopCommand = new Uint8Array([0x03, 0x03, 0x04]);
|
||||
|
||||
function stop(action: Action, dispatch: Dispatch): void {
|
||||
if (action.type !== HubActionType.Stop) {
|
||||
return;
|
||||
}
|
||||
dispatch(write(stopCommand));
|
||||
}
|
||||
|
||||
export default combineServices(downloadAndRun, startRepl, stop);
|
||||
@@ -4,9 +4,11 @@
|
||||
import { Middleware } from 'redux';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { RootState } from '../reducers';
|
||||
import ble from './ble';
|
||||
import bootloader from './bootloader';
|
||||
import editor from './editor';
|
||||
import errorLog from './error-log';
|
||||
import hub from './hub';
|
||||
|
||||
type Service = (
|
||||
action: Action,
|
||||
@@ -37,7 +39,7 @@ export function combineServices(...services: Service[]): Service {
|
||||
};
|
||||
}
|
||||
|
||||
const rootService = combineServices(bootloader, editor, errorLog);
|
||||
const rootService = combineServices(ble, bootloader, editor, errorLog, hub);
|
||||
|
||||
const serviceMiddleware: Middleware = (store) => (next) => (action): unknown => {
|
||||
runService(rootService, action, store.dispatch, store.getState());
|
||||
|
||||
Reference in New Issue
Block a user