wire terminal to ble

This commit is contained in:
David Lechner
2020-04-16 23:58:35 -05:00
parent b4d81fd3e5
commit a67656def6
9 changed files with 138 additions and 8 deletions
+15 -3
View File
@@ -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));
}
};
}
+24
View File
@@ -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 };
}
+28 -4
View File
@@ -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);
+17
View File
@@ -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);
+14
View File
@@ -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;
+28
View File
@@ -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
View File
@@ -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}>