mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
wire terminal to ble
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"react-scripts": "3.4.1",
|
||||
"redux": "^4.0.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-observable": "^1.2.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"typescript": "~3.7.2",
|
||||
"xterm": "^4.5.0"
|
||||
|
||||
+15
-3
@@ -2,9 +2,12 @@ import { Action } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
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: BluetoothRemoteGATTCharacteristic | undefined;
|
||||
@@ -30,7 +33,7 @@ export enum BLEConnectActionType {
|
||||
|
||||
type BLEConnectAction = Action<BLEConnectActionType>;
|
||||
|
||||
enum BLEDataActionType {
|
||||
export enum BLEDataActionType {
|
||||
/**
|
||||
* Send data.
|
||||
*/
|
||||
@@ -41,13 +44,13 @@ enum BLEDataActionType {
|
||||
ReceivedData = 'ble.data.receive',
|
||||
}
|
||||
|
||||
interface BLEDataAction extends Action<BLEDataActionType> {
|
||||
export interface BLEDataAction extends Action<BLEDataActionType> {
|
||||
value: DataView;
|
||||
}
|
||||
|
||||
type AnyBLEAction = BLEConnectAction | BLEDataAction;
|
||||
|
||||
type BLEThunkAction = ThunkAction<Promise<void>, {}, {}, AnyBLEAction>;
|
||||
export type BLEThunkAction = ThunkAction<Promise<void>, {}, {}, AnyBLEAction>;
|
||||
|
||||
function beginConnect(): BLEConnectAction {
|
||||
return { type: BLEConnectActionType.BeginConnect };
|
||||
@@ -117,3 +120,12 @@ export function disconnect(): BLEThunkAction {
|
||||
device?.gatt?.disconnect();
|
||||
};
|
||||
}
|
||||
|
||||
export function write(value: ArrayBuffer): BLEThunkAction {
|
||||
return async function (): Promise<void> {
|
||||
// TODO: do we need to dispatch any Action<>s here?
|
||||
for (let i = 0; i < value.byteLength; i += bleNusMaxSize) {
|
||||
await rxChar?.writeValue(value.slice(i, i + bleNusMaxSize));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Action } from 'redux';
|
||||
|
||||
export enum TerminalDataActionType {
|
||||
/**
|
||||
* Send data.
|
||||
*/
|
||||
SendData = 'terminal.data.send',
|
||||
/**
|
||||
* Data was received.
|
||||
*/
|
||||
ReceivedData = 'terminal.data.receive',
|
||||
}
|
||||
|
||||
export interface TerminalDataAction extends Action<TerminalDataActionType> {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function sendData(data: string): TerminalDataAction {
|
||||
return { type: TerminalDataActionType.SendData, value: data };
|
||||
}
|
||||
|
||||
export function receiveData(data: string): TerminalDataAction {
|
||||
return { type: TerminalDataActionType.ReceivedData, value: data };
|
||||
}
|
||||
@@ -2,14 +2,26 @@ import React from 'react';
|
||||
import { Terminal as XTerm } from 'xterm';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { receiveData } from '../actions/terminal';
|
||||
import { terminalOutput } from '../epics/terminal';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import 'xterm/css/xterm.css';
|
||||
|
||||
class Terminal extends React.Component {
|
||||
interface DispatchProps {
|
||||
onData: (data: string) => void;
|
||||
}
|
||||
|
||||
type TerminalProps = DispatchProps;
|
||||
|
||||
class Terminal extends React.Component<TerminalProps> {
|
||||
private xterm: XTerm;
|
||||
private terminalRef: React.RefObject<HTMLDivElement>;
|
||||
private subscription?: Subscription;
|
||||
|
||||
constructor(props: {}) {
|
||||
constructor(props: TerminalProps) {
|
||||
super(props);
|
||||
this.xterm = new XTerm({
|
||||
cursorBlink: true,
|
||||
@@ -24,7 +36,7 @@ class Terminal extends React.Component {
|
||||
selection: 'rgba(181,213,255,0.5)', // this should match AceEditor theme
|
||||
},
|
||||
});
|
||||
this.xterm.onData((data) => this.xterm.write(data));
|
||||
this.xterm.onData((d) => this.props.onData(d));
|
||||
this.terminalRef = React.createRef();
|
||||
}
|
||||
|
||||
@@ -34,6 +46,12 @@ class Terminal extends React.Component {
|
||||
return;
|
||||
}
|
||||
this.xterm.open(this.terminalRef.current);
|
||||
this.subscription = terminalOutput.subscribe((v) => this.xterm.write(v));
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
this.xterm.dispose();
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
@@ -47,4 +65,10 @@ class Terminal extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default Terminal;
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onData: (d): void => {
|
||||
dispatch(receiveData(d));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Terminal);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { combineEpics, Epic, ofType } from 'redux-observable';
|
||||
import { BLEDataActionType, BLEDataAction } from '../actions/ble';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { TerminalDataAction, sendData } from '../actions/terminal';
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const rxUartData: Epic = (action$) =>
|
||||
action$.pipe(
|
||||
ofType(BLEDataActionType.ReceivedData),
|
||||
map(
|
||||
(a: BLEDataAction): TerminalDataAction =>
|
||||
sendData(decoder.decode(a.value.buffer)),
|
||||
),
|
||||
);
|
||||
|
||||
export default combineEpics(rxUartData);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { combineEpics, Epic } from 'redux-observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import ble from './ble';
|
||||
import terminal from './terminal';
|
||||
|
||||
const rootEpic: Epic = (action$, store$, dependencies) =>
|
||||
combineEpics(ble, terminal)(action$, store$, dependencies).pipe(
|
||||
catchError((error, source) => {
|
||||
console.error(error);
|
||||
return source;
|
||||
}),
|
||||
);
|
||||
|
||||
export default rootEpic;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { combineEpics, Epic, ofType } from 'redux-observable';
|
||||
import { write, BLEThunkAction } from '../actions/ble';
|
||||
import { Subject } from 'rxjs';
|
||||
import { map, tap, ignoreElements } from 'rxjs/operators';
|
||||
import { TerminalDataAction, TerminalDataActionType } from '../actions/terminal';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const terminalOutputSubject = new Subject<string>();
|
||||
export const terminalOutput = terminalOutputSubject.asObservable();
|
||||
|
||||
// When terminal has focus this receives the input. All input is sent over BLE connection.
|
||||
const receiveTerminalData: Epic = (action$) =>
|
||||
action$.pipe(
|
||||
ofType(TerminalDataActionType.ReceivedData),
|
||||
map((a: TerminalDataAction): BLEThunkAction => write(encoder.encode(a.value))),
|
||||
);
|
||||
|
||||
// Request to write to the terminal are handled by an observable rather than
|
||||
// using redux state.
|
||||
const sendTerminalData: Epic = (action$) =>
|
||||
action$.pipe(
|
||||
ofType(TerminalDataActionType.SendData),
|
||||
tap((a: TerminalDataAction): void => terminalOutputSubject.next(a.value)),
|
||||
ignoreElements(),
|
||||
);
|
||||
|
||||
export default combineEpics(receiveTerminalData, sendTerminalData);
|
||||
+6
-1
@@ -1,7 +1,9 @@
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { createEpicMiddleware } from 'redux-observable';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import rootReducer from './reducers';
|
||||
import rootEpic from './epics';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import ReactDOM from 'react-dom';
|
||||
@@ -9,13 +11,16 @@ import './index.scss';
|
||||
import App from './components/App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
const epicMiddleware = createEpicMiddleware();
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunkMiddleware, loggerMiddleware),
|
||||
applyMiddleware(thunkMiddleware, epicMiddleware, loggerMiddleware),
|
||||
);
|
||||
|
||||
epicMiddleware.run(rootEpic);
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
|
||||
@@ -10256,6 +10256,11 @@ redux-logger@^3.0.6:
|
||||
dependencies:
|
||||
deep-diff "^0.3.5"
|
||||
|
||||
redux-observable@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.2.0.tgz#ff51b6c6be2598e9b5e89fc36639186bb0e669c7"
|
||||
integrity sha512-yeR90RP2WzZzCxxnQPlh2uFzyfFLsfXu8ROh53jGDPXVqj71uNDMmvi/YKQkd9ofiVoO4OYb1snbowO49tCEMg==
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
|
||||
Reference in New Issue
Block a user