glider/App.js
2020-08-18 14:56:25 -07:00

447 lines
16 KiB
JavaScript

import React, { useReducer, useState, useEffect } from 'react';
import 'react-native-gesture-handler';
import {Alert, TextInput, Text} from 'react-native';
import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native';
import { AppearanceProvider, useColorScheme} from 'react-native-appearance';
import DraggableView from './draggable-view';
import CodeEditor from './code-editor';
import Status, { StatusSummary} from './status';
import { useAppState } from 'react-native-hooks'
import { stringToBytes, bytesToString } from 'convert-string';
import * as encoding from 'text-encoding';
import RNColorPalette from '@iomechs/rn-color-palette';
import Clipboard from "@react-native-community/clipboard";
import {
NativeEventEmitter,
NativeModules,
PermissionsAndroid,
Platform,
View,
TouchableOpacity,
SafeAreaView
} from 'react-native';
import BleManager from 'react-native-ble-manager';
import { ScrollView } from 'react-native-gesture-handler';
const BleManagerModule = NativeModules.BleManager;
const bleManagerEmitter = new NativeEventEmitter(BleManagerModule);
function peripheralReducer(state, action) {
if (action.action == "add") {
let peripheral = action.peripheral;
//console.log(peripheral);
if (state.has(peripheral.id)) {
return state; // No state change
}
if (!peripheral.advertising.serviceUUIDs || peripheral.advertising.serviceUUIDs.length == 0 || peripheral.advertising.serviceUUIDs[0].toLowerCase() != 'adaf0100-4369-7263-7569-74507974686e') {
return state;
}
//console.log(peripheral);
var newMap = new Map(state);
newMap.set(peripheral.id, peripheral);
return newMap;
} else if (action.action == "scan") {
BleManager.scan([], 3).then((results) => {
//console.log('Scanning...');
});
} else if (action.action == "clear") {
state.clear();
}
return state;
}
const service = 'adaf0100-4369-7263-7569-74507974686e';
const contentsCharacteristic = 'adaf0201-4369-7263-7569-74507974686e';
const filenameCharacteristic = 'adaf0200-4369-7263-7569-74507974686e';
const versionCharacteristic = 'adaf0203-4369-7263-7569-74507974686e';
const lengthCharacteristic = 'adaf0202-4369-7263-7569-74507974686e';
function codeReducer(state, action) {
let newState = {code: state.code, peripheral_id: state.peripheral_id, queue: state.queue, version: state.version + 1};
if (action.type == "clear") {
newState.code = "";
} else if (action.type == "connect") {
newState.peripheral_id = action.peripheral_id;
if (state.queue && state.queue.length > 0) {
//console.log("missed patches", state.queue);
}
} else if (action.type == "disconnect") {
newState.peripheral_id = null;
newState.queue = new Array();
} else if (action.type == "read") {
newState.code += action.data;
} else if (action.type == "patch") {
//console.log("TODO send data back to CP");
//console.log(state, action);
let encoder = new encoding.TextEncoder();
let encodedInsert = encoder.encode(action.newValue);
let totalLength = 2 + 2 + 4 + 4 + 4 + encodedInsert.length
let patch = new ArrayBuffer(totalLength);
let view = new DataView(patch);
view.setUint16(0, totalLength, true);
view.setUint16(2, 2, true);
view.setUint32(4, action.offset, true);
view.setUint32(8, action.oldValue.length, true);
view.setUint32(12, encodedInsert.length, true);
let byteView = new Uint8Array(patch, 16, encodedInsert.length);
byteView.set(encodedInsert);
// React native bridging can't handle Uint8Array so copy into a normal array.
let finalPatch = Array.from(new Uint8Array(patch));
if (state.peripheral_id) {
console.log("writing patch", finalPatch, patch);
BleManager.write(
state.peripheral_id,
service,
contentsCharacteristic,
finalPatch
).then(() => {
console.log('Wrote patch to device');
}).catch((error) => {
console.log("ERROR patching: ", error);
});
} else {
console.log("no peripheral", newState.queue);
newState.queue.push(patch);
}
//console.log("merging together", action, state.code);
newState.code = state.code.substring(0, action.offset) + action.newValue + state.code.substring(action.offset + action.oldValue.length, state.code.length);
}
//console.log("new code state", newState);
return newState;
}
export default function App() {
//check if color scheme is set to dark
const scheme = useColorScheme()
const [dark, setDark] = useState(false);
if (scheme == 'dark') {
setDark(true);
}
//initialize variables and functions for color palette and clipboard feature
const [pickedColor, colorPicked] = useState('#ff0000');
const [colors, addColor] = useState(['#ff0000']);
const copyToClipboard = (pickedColor) => {
let originalColor = pickedColor;
let prefix = '0x'
let newColor = originalColor.substring(1)
let newHexColor = prefix.concat(newColor)
console.log(newHexColor)
Clipboard.setString(newHexColor)
}
//initalize constants and states for app
const currentAppState = useAppState();
const [bleState, setBleState] = useState("stopped");
const [peripherals, changePeripherals] = useReducer(peripheralReducer, new Map());
const [peripheral, setPeripheral] = useState(null);
const [fileState, setFileState] = useState("unloaded");
const [fileLength, setFileLength] = useState(-1);
const [search, setSearch] = useState("");
const [code, changeCode] = useReducer(codeReducer, {code:"", version:0, peripheral_id: null});
useEffect(() => {
if (currentAppState === 'active') {
//console.log('App has come to the foreground!', bleState);
BleManager.start({showAlert: true});
BleManager.checkState();
} else {
if (peripheral) {
changeCode({"type": "disconnect", "peripheral_id": peripheral.id});
BleManager.disconnect(peripheral.id);
}
setBleState("disconnected");
}
}, [currentAppState]);
const handleUpdateValueForCharacteristic = (data) => {
//console.log(data);
//console.log('Received data from ' + data.peripheral + ' characteristic ' + data.characteristic, bytesToString(data.value));
changeCode({"type": "read", "data": bytesToString(data.value)});
}
const handleUpdateState = (data) => {
//console.log("update state", data);
if (data.state == "on") {
//console.log("started");
setBleState("started");
}
}
const handleStopScan = () => {
//console.log('Scan is stopped');
setBleState('selectPeripheral');
}
const handleDiscoverPeripheral = (peripheral) => {
//console.log('Got ble peripheral', peripheral);
peripheral.connected = false;
changePeripherals({"action": "add", "peripheral": peripheral});
}
const handleDisconnectedPeripheral = (data) => {
// let peripherals = this.state.peripherals;
// let peripheral = peripherals.get(data.peripheral);
// if (peripheral) {
// peripheral.connected = false;
// peripherals.set(peripheral.id, peripheral);
// this.setState({peripherals});
// }
setBleState("disconnected");
Alert.alert(
"Device Disconnected",
"You have lost connection with your device",
[
{
text: "Dismiss",
}
],
{ cancelable: false }
);
//console.log('Disconnected from ' + data.peripheral);
}
function handleConnectPeripheral() {
//console.log("handle connect to", peripheral);
BleManager.connect(peripheral.id).then(() => {
setBleState("connected");
changeCode({"type": "connect", "peripheral_id": peripheral.id});
setTimeout(() => {
BleManager.retrieveServices(peripheral.id).then((peripheralInfo) => {
//console.log(peripheralInfo);
setTimeout(() => {
BleManager.startNotification(peripheral.id, service, contentsCharacteristic).then(() => {
//console.log('Started notification on ' + peripheral.id);
setTimeout(() => {
BleManager.write(peripheral.id, service, filenameCharacteristic, stringToBytes("/code.py")).then(() => {
//console.log('Wrote filename');
setFileState("nameSet");
});
}, 500);
}).catch((error) => {
//console.log('Notification error', error);
});
}, 200);
});
}, 900);
}).catch((error) => {
setBleState("disconnected");
console.log('Connection error', error);
});
}
useEffect(() => {
if (fileState == "nameSet") {
// Load the file length and then load it all.
setTimeout(() => {
BleManager.read(peripheral.id, service, lengthCharacteristic).then((readData) => {
let length = readData[0] + (readData[1] << 8) + (readData[2] << 16) + (readData[3] << 24);
//console.log('fileLength', readData, length);
changeCode({"type": "clear"});
setFileLength(length);
setFileState("loading");
});
}, 500);
} else if (fileState == "loading") {
let command = new Array(4);
command.fill(0);
command[0] = 4;
command[2] = 1;
setTimeout(() => {
BleManager.write(peripheral.id, service, contentsCharacteristic, command)
.catch((error) => {
console.log('Command error', error);
});
}, 200);
}
}, [fileState]);
useEffect(() => {
//console.log("loaded", code.code.length, fileLength);
if (fileState == "loading" && code.code.length == fileLength) {
setFileState("loaded");
}
}, [code, fileState, fileLength]);
useEffect(() => {
if (peripheral != null && bleState != "connected") {
handleConnectPeripheral();
}
}, [peripheral, bleState]);
useEffect(() => {
//console.log("new blestate", bleState);
if (bleState == "permOk") {
} else if (bleState == "started") {
changePeripherals({"action": "clear"});
BleManager.getConnectedPeripherals([]).then((peripheralsArray) => {
for (p of peripheralsArray) {
//console.log(p);
p.connected = true;
BleManager.connect(p.id).then(() => {
BleManager.retrieveServices(p.id).then((peripheralInfo) => {
//console.log(peripheralInfo);
p.advertising.serviceUUIDs = [peripheralInfo.services[2].uuid];
changePeripherals({"action": "add", "peripheral": p});
});
});
}
//console.log('Connected peripherals: ' + peripheralsArray.length);
});
changePeripherals({"action": "scan"});
} else if (bleState == "disconnected") {
// set a timeout and try to reconnect
} else if (bleState == "selectPeripheral") {
//console.log(peripherals, peripherals.values());
if (peripherals.size == 1) {
let peripheral = [...peripherals][0][1];
//console.log("selecting", peripheral);
setPeripheral(peripheral);
}
}
}, [bleState]);
useEffect(() => {
const handlerDiscover = bleManagerEmitter.addListener('BleManagerDiscoverPeripheral', handleDiscoverPeripheral );
const handlerStop = bleManagerEmitter.addListener('BleManagerStopScan', handleStopScan );
const handlerDisconnect = bleManagerEmitter.addListener('BleManagerDisconnectPeripheral', handleDisconnectedPeripheral );
const handlerUpdate = bleManagerEmitter.addListener('BleManagerDidUpdateValueForCharacteristic', handleUpdateValueForCharacteristic );
const handlerUpdateState = bleManagerEmitter.addListener('BleManagerDidUpdateState', handleUpdateState);
if (Platform.OS === 'android' && Platform.Version >= 23) {
setBleState("permCheck");
PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION).then((result) => {
if (result) {
setBleState("permOk");
} else {
PermissionsAndroid.requestPermission(PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION).then((result) => {
if (result) {
setBleState("permOk");
} else {
setBleState("permNak");
}
});
}
});
} else {
setBleState("permOk");
}
return () => {
handlerDiscover.remove();
handlerStop.remove();
handlerDisconnect.remove();
handlerUpdate.remove();
handlerUpdateState.remove();
};
}, []);
return (
<AppearanceProvider><NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
<SafeAreaView style={{flex:1, backgroundColor: scheme === 'dark' ? 'rgb(18,18,18)' : 'white'}}>
<DraggableView
isInverseDirection={true}
bgColor={scheme === 'dark' ? 'rgb(18,18,18)' : 'white'}
initialDrawerSize={17}
renderContainerView={() => (
<View>
<ScrollView>
<StatusSummary bleState={bleState} />
<Text></Text>
<TextInput
style={{
color: dark ? 'white' : 'rgb(18,18,18)',
paddingLeft: 15,
paddingRight: 15,
paddingTop: 3,
paddingBottom: 2,
borderWidth: 1,
borderRadius: 30,
borderColor: dark ? 'white' : 'rgb(18,18,18)'}}
onChangeText={search => setSearch(search)}
underlineColorAndroid="black"
placeholder="Search through the code ..."
placeholderTextColor={scheme === 'dark' ? 'white' : 'rgb(18,18,18)'}
keyboardType="default"
clearButtonMode="while-editing"
/>
<Text></Text>
<View style={{
flexDirection: 'row',
paddingTop: 2,
paddingBottom: 3,
}}>
<RNColorPalette
colorList={colors}
value={pickedColor}
onItemSelect={colorPicked}
AddPickedColor={colour => addColor([...colors, colour])}
style={{
backgroundColor: pickedColor,
paddingLeft: 10,
paddingRight: 5,
paddingTop: 2,
paddingBottom: 2,
borderWidth: 1,
borderRadius: 5,
width: 140,
height: 30,
}}>
<View>
<Text>Color Picker</Text>
</View>
</RNColorPalette>
<TouchableOpacity onPress={copyToClipboard(pickedColor)} style={{
backgroundColor: pickedColor,
paddingLeft: 10,
paddingRight: 5,
paddingTop: 2,
paddingBottom: 2,
borderWidth: 1,
borderRadius: 5,
width: 140,
height: 30,
}}>
<Text>Copy to Clipboard</Text>
</TouchableOpacity>
</View>
<ScrollView horizontal={true}>
<CodeEditor
searchBar={search}
code={code.code}
changeCode={changeCode}
fileState={fileState}
fileName="/code.py"
fileVersion={code.version}
/>
</ScrollView>
</ScrollView>
</View>)}
renderDrawerView={() => (<Status bleState={bleState} peripherals={peripherals} setPeripheral={setPeripheral} />)}
renderInitDrawerView={() => (<StatusSummary bleState={bleState}/>)}
/>
</SafeAreaView>
</NavigationContainer></AppearanceProvider>
);
}