mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
add terminal and ble connection
This commit is contained in:
+3
-1
@@ -15,7 +15,8 @@
|
||||
"react-ace": "^8.1.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-scripts": "3.4.1",
|
||||
"typescript": "~3.7.2"
|
||||
"typescript": "~3.7.2",
|
||||
"xterm": "^4.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -40,6 +41,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/web-bluetooth": "^0.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
||||
"@typescript-eslint/parser": "^2.27.0",
|
||||
"eslint": "^6.8.0",
|
||||
|
||||
+13
-11
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import './App.css';
|
||||
import { Connection } from './Connection';
|
||||
import { Terminal } from './Terminal';
|
||||
|
||||
import 'ace-builds/src-noconflict/mode-python';
|
||||
import 'ace-builds/src-noconflict/theme-github';
|
||||
@@ -10,27 +12,27 @@ function onChange(newValue: string): void {
|
||||
}
|
||||
|
||||
function App(): JSX.Element {
|
||||
const connection = React.createRef<Connection>();
|
||||
const terminal = React.createRef<Terminal>();
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
<Connection
|
||||
onData={(e): void => terminal.current?.write(e)}
|
||||
ref={connection}
|
||||
/>
|
||||
</header>
|
||||
<Terminal
|
||||
onData={(d): void => connection.current?.write(d)}
|
||||
ref={terminal}
|
||||
/>
|
||||
<AceEditor
|
||||
mode="python"
|
||||
theme="github"
|
||||
onChange={onChange}
|
||||
name="editor"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
|
||||
const pybricksServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef';
|
||||
const bleNusServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusCharRXUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
const bleNusCharTXUUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
|
||||
|
||||
enum connectionState {
|
||||
disconnected,
|
||||
connecting,
|
||||
connected,
|
||||
disconnecting,
|
||||
}
|
||||
|
||||
function connectionStateToButtonText(state: connectionState): string {
|
||||
switch (state) {
|
||||
case connectionState.disconnected:
|
||||
return 'Connect';
|
||||
case connectionState.connecting:
|
||||
return 'Connecting...';
|
||||
case connectionState.connected:
|
||||
return 'Disconnect';
|
||||
case connectionState.disconnecting:
|
||||
return 'Disconnecting...';
|
||||
default:
|
||||
return 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectionProperties = {
|
||||
onData: (data: Uint8Array) => void;
|
||||
};
|
||||
type ConnectionState = {
|
||||
connection: connectionState;
|
||||
};
|
||||
|
||||
class Connection extends React.Component<ConnectionProperties, ConnectionState> {
|
||||
private device?: BluetoothDevice;
|
||||
private rxChar?: BluetoothRemoteGATTCharacteristic;
|
||||
|
||||
constructor(props: ConnectionProperties) {
|
||||
super(props);
|
||||
this.state = { connection: connectionState.disconnected };
|
||||
this.onConnectClicked = this.onConnectClicked.bind(this);
|
||||
}
|
||||
|
||||
public write(data: Uint8Array): void {
|
||||
this.rxChar?.writeValue(data);
|
||||
}
|
||||
|
||||
private async connect(): Promise<void> {
|
||||
try {
|
||||
if (this.device !== undefined) {
|
||||
throw Error('Already connected.');
|
||||
}
|
||||
if (navigator.bluetooth === undefined) {
|
||||
// TODO: custom exception type
|
||||
throw Error(
|
||||
'WebBluetooth API is not available. Please make sure the Web Bluetooth flag is enabled.',
|
||||
);
|
||||
}
|
||||
this.device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [pybricksServiceUUID] }],
|
||||
});
|
||||
if (this.device.gatt === undefined) {
|
||||
throw Error('Device does not support GATT');
|
||||
}
|
||||
this.device.addEventListener('gattserverdisconnected', () =>
|
||||
this.setState({ connection: connectionState.disconnected }),
|
||||
);
|
||||
} catch (err) {
|
||||
this.setState({ connection: connectionState.disconnected });
|
||||
throw err;
|
||||
}
|
||||
const server = await this.device.gatt.connect();
|
||||
const service = await server.getPrimaryService(bleNusServiceUUID);
|
||||
this.rxChar = await service.getCharacteristic(bleNusCharRXUUID);
|
||||
const txChar = await service.getCharacteristic(bleNusCharTXUUID);
|
||||
txChar.addEventListener('characteristicvaluechanged', () => {
|
||||
if (!txChar.value) {
|
||||
return;
|
||||
}
|
||||
this.props.onData(new Uint8Array(txChar.value.buffer));
|
||||
});
|
||||
await txChar.startNotifications();
|
||||
this.setState({ connection: connectionState.connected });
|
||||
}
|
||||
|
||||
private disconnect(): void {
|
||||
if (this.device !== undefined) {
|
||||
this.device.gatt?.disconnect();
|
||||
this.device = undefined;
|
||||
this.rxChar = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async onConnectClicked(): Promise<void> {
|
||||
if (this.state.connection === connectionState.disconnected) {
|
||||
this.setState({ connection: connectionState.connecting });
|
||||
try {
|
||||
await this.connect();
|
||||
} catch (err) {
|
||||
// FIXME: need proper error dialog
|
||||
alert(err);
|
||||
}
|
||||
} else {
|
||||
this.setState({ connection: connectionState.disconnecting });
|
||||
try {
|
||||
this.disconnect();
|
||||
} catch (err) {
|
||||
// FIXME: need proper error dialog
|
||||
alert(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
name="connect"
|
||||
onClick={this.onConnectClicked}
|
||||
disabled={
|
||||
this.state.connection === connectionState.connecting ||
|
||||
this.state.connection === connectionState.disconnecting
|
||||
}
|
||||
>
|
||||
{connectionStateToButtonText(this.state.connection)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Connection };
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
type TerminalProps = {
|
||||
onData: (data: Uint8Array) => void;
|
||||
};
|
||||
|
||||
export default class TerminalComponent extends React.Component<TerminalProps> {
|
||||
private term: Terminal;
|
||||
private terminalRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: TerminalProps) {
|
||||
super(props);
|
||||
this.term = new Terminal();
|
||||
this.terminalRef = React.createRef();
|
||||
this.term.onData((data) => this.props.onData(encoder.encode(data)));
|
||||
}
|
||||
|
||||
public write(data: Uint8Array): void {
|
||||
this.term.write(data);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if (!this.terminalRef.current) {
|
||||
return;
|
||||
}
|
||||
this.term.open(this.terminalRef.current);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<div id="terminal" ref={this.terminalRef}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export { TerminalComponent as Terminal };
|
||||
@@ -2183,6 +2183,11 @@
|
||||
"@types/testing-library__dom" "*"
|
||||
pretty-format "^25.1.0"
|
||||
|
||||
"@types/web-bluetooth@^0.0.5":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.5.tgz#f952d1481572798dd20a381135bef3d8b1ef305a"
|
||||
integrity sha512-gaSAxNePCVJUR1a+4jKMaukjEzE4XuJd9fzSx/mAy6BrVoJXnACmfaBkaXU/K0frxJVfHvMDJx96hbNt4NXd9Q==
|
||||
|
||||
"@types/yargs-parser@*":
|
||||
version "13.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228"
|
||||
@@ -11979,6 +11984,11 @@ xtend@^4.0.0, xtend@~4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
xterm@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589"
|
||||
integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig==
|
||||
|
||||
"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
|
||||
|
||||
Reference in New Issue
Block a user