WIP More Wifi setup

This commit is contained in:
Martin Bauer 2021-06-13 12:43:50 +02:00
parent a6db96ef29
commit 0bb0e2f121
8 changed files with 263 additions and 126 deletions

133
App.js
View File

@ -6,7 +6,7 @@ import * as Font from 'expo-font';
// Redux + Storage // Redux + Storage
import swimtrackerReducer from './state/Reducer'; import swimtrackerReducer from './state/Reducer';
import { createStore } from 'redux'; import { createStore } from 'redux';
import { DeviceReduxCoupling } from './state/DeviceReduxCoupling'; import { ConnState, WifiState, DeviceReduxCoupling } from './state/DeviceReduxCoupling';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { persistStore, persistReducer } from 'redux-persist' import { persistStore, persistReducer } from 'redux-persist'
@ -43,17 +43,32 @@ export default class App extends React.Component {
super(props); super(props);
this.state = { this.state = {
isReady: false, isReady: false,
disconnected: true,
isProvisioning: false,
}; };
this.unsubscribe = undefined;
} }
async componentDidMount() { componentDidMount() {
/*await Font.loadAsync({
Roboto: require('native-base/Fonts/Roboto.ttf'),
Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'),
...Ionicons.font,
});*/
this.setState({ isReady: true }); this.setState({ isReady: true });
this.device = new DeviceReduxCoupling(store); this.device = new DeviceReduxCoupling(store);
console.log("subscribing");
let theApp = this;
this.unsubscribe = store.subscribe(() => {
const state = store.getState();
theApp.setState({
disconnected: state.deviceState.connState == ConnState.DISCONNECTED,
isProvisioning: state.deviceState.wifiState == WifiState.AP_PROVISIONING || state.deviceState.wifiState == WifiState.UNKNOWN,
});
});
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
console.log("unsubscribe");
}
} }
render() { render() {
@ -64,61 +79,77 @@ export default class App extends React.Component {
headerShown: false, headerShown: false,
}; };
let disconnectedView = (
<>
<Stack.Screen
name="ConnectingView"
options={screenOptions}
component={ConnectingView} />
</>
);
let provisioningView = (
<>
<Stack.Screen
name="WifiSelectionView"
options={screenOptions} >
{props => <WifiSelectionView {...props} device={this.device} />}
</Stack.Screen>
<Stack.Screen
name="WifiPasswordView"
options={screenOptions}
component={WifiPasswordView}
>
</Stack.Screen>
</>
);
let normalView = (
<>
<Stack.Screen
name="Home"
component={MainMenuView}
options={screenOptions}
/>
<Stack.Screen
name="Settings"
component={SettingsView}
options={screenOptions}
/>
<Stack.Screen
name="Training"
component={TrainingView}
options={screenOptions}
/>
<Stack.Screen
name="LastSessions"
component={LastSessionsView}
options={screenOptions}
/>
</>
);
let activeView;
if (this.state.disconnected)
activeView = disconnectedView;
else if (this.state.isProvisioning)
activeView = provisioningView;
else
activeView = normalView;
return ( return (
<Provider store={store}> <Provider store={store}>
<PersistGate loading={<AppLoading />} persistor={persistor}> <PersistGate loading={<AppLoading />} persistor={persistor}>
<NavigationContainer> <NavigationContainer>
<Stack.Navigator initialRouteName="WifiSelectionView"> <Stack.Navigator >
<Stack.Screen {activeView}
name="WifiSelectionView"
options={screenOptions} >
{props => <WifiSelectionView {...props} device={this.device} />}
</Stack.Screen>
<Stack.Screen
name="WifiPasswordView"
options={screenOptions}
component={WifiPasswordView}
>
</Stack.Screen>
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
</PersistGate> </PersistGate>
</Provider> </Provider>
); );
/*
return (
<Provider store={store}>
<PersistGate loading={<AppLoading />} persistor={persistor}>
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={MainMenuView}
options={screenOptions}
/>
<Stack.Screen
name="Settings"
component={SettingsView}
options={screenOptions}
/>
<Stack.Screen
name="Training"
component={TrainingView}
options={screenOptions}
/>
<Stack.Screen
name="LastSessions"
component={LastSessionsView}
options={screenOptions}
/>
</Stack.Navigator>
</NavigationContainer>
</PersistGate>
</Provider>
);
*/
} }
} }

View File

@ -25,6 +25,9 @@
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true
}, },
"description": "" "description": "",
"android": {
"package": "tech.bauer.swimtracker"
}
} }
} }

View File

@ -14,12 +14,12 @@ function AdditionalOptionsBottomBar(props) {
return ( return (
<View style={bottomBarStyles.container}> <View style={bottomBarStyles.container}>
{ props.leftText ? { props.leftText ?
<TouchableOpacity onPress={props.onLeftPress}> <TouchableOpacity onPress={props.onLeftPress} style={bottomBarStyles.button}>
<Text style={bottomBarStyles.text}>{props.leftText} </Text> <Text style={bottomBarStyles.text}>{props.leftText} </Text>
</TouchableOpacity> : <View></View> </TouchableOpacity> : <View></View>
} }
{props.rightText && {props.rightText &&
<TouchableOpacity onPress={props.onRightPress}> <TouchableOpacity onPress={props.onRightPress} style={bottomBarStyles.button}>
<Text style={bottomBarStyles.text}>{props.rightText}</Text> <Text style={bottomBarStyles.text}>{props.rightText}</Text>
</TouchableOpacity> </TouchableOpacity>
} }
@ -35,6 +35,9 @@ const bottomBarStyles = StyleSheet.create({
}, },
text: { text: {
color: "rgba(255,255,255,0.5)", color: "rgba(255,255,255,0.5)",
},
button: {
borderStyle: "dotted"
} }
}) })
@ -55,7 +58,7 @@ function SetupView(props) {
<View style={setupViewStyles.container}> <View style={setupViewStyles.container}>
<View style={{ flexDirection: "row", alignItems: "center" }}> <View style={{ flexDirection: "row", alignItems: "center" }}>
{props.backButton && {props.backButton &&
<TouchableOpacity onPress={props.navigation.goBack}> <TouchableOpacity onPress={() => props.navigation.goBack()}>
<EntypoIcon name="chevron-left" style={setupViewStyles.backButton}></EntypoIcon> <EntypoIcon name="chevron-left" style={setupViewStyles.backButton}></EntypoIcon>
</TouchableOpacity> </TouchableOpacity>
} }
@ -63,7 +66,9 @@ function SetupView(props) {
{props.headerText} {props.headerText}
</Text> </Text>
</View> </View>
{props.children} <View style={{flex: 1, justifyContent: "center"}}>
{props.children}
</View>
<AdditionalOptionsBottomBar leftText={props.lowerLeftButtonText} <AdditionalOptionsBottomBar leftText={props.lowerLeftButtonText}
onLeftPress={props.onLowerLeftButtonPress} onLeftPress={props.onLowerLeftButtonPress}
rightText={props.lowerRightButtonText} rightText={props.lowerRightButtonText}
@ -93,6 +98,7 @@ const setupViewStyles = StyleSheet.create({
fontSize: 18, fontSize: 18,
lineHeight: 25, lineHeight: 25,
width: "80%", width: "80%",
marginBottom: 50,
}, },
backButton: { backButton: {
color: "rgba(255,255,255,1)", color: "rgba(255,255,255,1)",

View File

@ -28,12 +28,13 @@ const OpCodes = {
export default class SwimTrackerWebsocketConnection { export default class SwimTrackerWebsocketConnection {
constructor(swimTrackerHost, onData, onStarted, onStopped, onConnect, onDisconnect) { constructor(swimTrackerHost, onData, onStarted, onStopped, onWifiStateInfo, onConnect, onDisconnect) {
this.swimTrackerHost = swimTrackerHost; this.swimTrackerHost = swimTrackerHost;
this.onData = onData; this.onData = onData;
this.onStarted = onStarted; this.onStarted = onStarted;
this.onStopped = onStopped; this.onStopped = onStopped;
this.onWifiStateInfo = onWifiStateInfo;
this.onConnect = onConnect; this.onConnect = onConnect;
this.onDisconnect = onDisconnect; this.onDisconnect = onDisconnect;
@ -54,48 +55,68 @@ export default class SwimTrackerWebsocketConnection {
return result; return result;
}); });
this._wifiScanPromise = null; this._wifiScanPromises = [];
} }
sendStartCommand() { sendStartCommand() {
const data = new Uint8Array(1); this._sendMsg(OpCodes.START_SESSION);
data[0] = OpCodes.START_SESSION;
this.ws.send(data);
} }
sendStopCommand() { sendStopCommand() {
const data = new Uint8Array(1); this._sendMsg(OpCodes.STOP_SESSION);
data[0] = OpCodes.STOP_SESSION;
this.ws.send(data);
} }
sendTareCommand() { sendTareCommand() {
const data = new Uint8Array(1); this._sendMsg(OpCodes.TARE);
data[0] = OpCodes.TARE;
this.ws.send(data);
} }
scanWifiNetworks() { scanWifiNetworks() {
// trigger scan console.log("Trigger wifi scan");
const data = new Uint8Array(1); this._sendMsg(OpCodes.WIFI_TRIGGER_SCAN);
data[0] = OpCodes.WIFI_TRIGGER_SCAN;
this.ws.send(data);
if (this._wifiScanPromise !== null) { let conn = this;
return Promise.reject("Scan in progress"); return new Promise((resolve, reject) => {
} conn._wifiScanPromises.push({ resolve: resolve, reject: reject });
else { });
let conn = this; }
return new Promise((resolve, reject) => {
conn._wifiScanPromise = { resolve: resolve, reject: reject }; wifiResetToProvisioning() {
}); this._sendMsg(OpCodes.WIFI_STATE_SET, {
"reset_to_provisioning": true,
});
}
wifiSetModeAP(password) {
this._sendMsg(OpCodes.WIFI_STATE_SET, {
"ap_password": password,
});
}
wifiSetModeSTA(ssid, password) {
this._sendMsg(OpCodes.WIFI_STATE_SET, {
"sta_ssid": ssid,
"sta_password": password,
});
}
_sendMsg(code, data) {
let msg = undefined;
if (data) {
const serializedData = msgpack.encode(data);
msg = new Uint8Array([code, ...serializedData]);
} else {
msg = new Uint8Array(1);
msg[0] = OpCodes.WIFI_TRIGGER_SCAN;
} }
this.ws.send(msg);
} }
_onMessage = (e) => { _onMessage = (e) => {
const dv = new DataView(e.data); const dv = new DataView(e.data);
const opCode = dv.getInt8(0); const opCode = dv.getInt8(0);
const payload = new Uint8Array(e.data).slice(1);
if (opCode === OpCodes.INITIAL_INFO) { if (opCode === OpCodes.INITIAL_INFO) {
const headerSize = 6; const headerSize = 6;
@ -116,14 +137,15 @@ export default class SwimTrackerWebsocketConnection {
const data = new Uint16Array(e.data.slice(1)); const data = new Uint16Array(e.data.slice(1));
this.onData(data); this.onData(data);
} else if (opCode === OpCodes.WIFI_SCAN_RESPONSE) { } else if (opCode === OpCodes.WIFI_SCAN_RESPONSE) {
console.log("got data", e.data); const scanResult = msgpack.decode(payload, { codec: this.msgpackCodec });
const scanResult = msgpack.decode(new Uint8Array(e.data).slice(1), { codec: this.msgpackCodec });
if (this._wifiScanPromise !== null) { for (let i = 0; i < this._wifiScanPromises.length; ++i) {
this._wifiScanPromise.resolve(scanResult); this._wifiScanPromises[i].resolve(scanResult);
this._wifiScanPromise = null;
} else {
console.log("Got unexpected WiFi scan result", scanResult);
} }
this._wifiScanPromises.length = 0;
} else if (opCode === OpCodes.WIFI_STATE_RESPONSE) {
const wifiInfo = msgpack.decode(payload, { codec: this.msgpackCodec });
this.onWifiStateInfo(wifiInfo);
} }
} }

View File

@ -87,6 +87,7 @@ export class DeviceReduxCoupling {
this._onNewData, this._onNewData,
(sessionId) => this.reduxStore.dispatch(reportSessionStarted(sessionId)), (sessionId) => this.reduxStore.dispatch(reportSessionStarted(sessionId)),
() => this.reduxStore.dispatch(reportSessionStopped()), () => this.reduxStore.dispatch(reportSessionStopped()),
(response) => this.reduxStore.dispatch(reportNewWifiState(response["state"])),
() => this.reduxStore.dispatch(reportDeviceConnect()), () => this.reduxStore.dispatch(reportDeviceConnect()),
() => this.reduxStore.dispatch(reportDeviceDisconnect()) () => this.reduxStore.dispatch(reportDeviceDisconnect())
); );
@ -152,15 +153,14 @@ export const deviceStateReducer = (state = INITIAL_DEVICE_STATE, action) => {
return state; return state;
return { ...INITIAL_DEVICE_STATE, connState: ConnState.CONNECTED_STOPPING }; return { ...INITIAL_DEVICE_STATE, connState: ConnState.CONNECTED_STOPPING };
case WIFI_SET_STATE: case WIFI_SET_STATE:
console.log("here");
let wifState = WifiState.UNKNOWN; let wifState = WifiState.UNKNOWN;
if (action.data === "STATION_MODE") { wifState = WifiState.STA; } if (action.newStateStr === "STATION_MODE") { wifState = WifiState.STA; }
else if (action.data === "AP_PROVISIONING") { wifState = WifiState.AP_PROVISIONING; } else if (action.newStateStr === "AP_PROVISIONING") { wifState = WifiState.AP_PROVISIONING; }
else if (action.data === "AP_SECURE") { wifState = WifiState.AP_SECURE; } else if (action.newStateStr === "AP_SECURE") { wifState = WifiState.AP_SECURE; }
return { ...state, wifiState: wifState }; return { ...state, wifiState: wifState };
default: default:
console.log("Unhandled state in deviceStateReducer", action, action.type); //console.log("Unhandled state in deviceStateReducer", action, action.type, "state", state);
return state return state;
} }
}; };

View File

@ -10,6 +10,11 @@ import {
import themeColors from '../components/themeColors'; import themeColors from '../components/themeColors';
import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
import EntypoIcon from "react-native-vector-icons/Entypo"; import EntypoIcon from "react-native-vector-icons/Entypo";
import FeatherIcon from "react-native-vector-icons/Feather";
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { ConnState, startSession } from '../state/DeviceReduxCoupling'; import { ConnState, startSession } from '../state/DeviceReduxCoupling';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -83,7 +88,7 @@ function ButtonGrid(props) {
style={[{ backgroundColor: themeColors["MIDNIGHT BLUE"] }, buttonGridStyles.button]} style={[{ backgroundColor: themeColors["MIDNIGHT BLUE"] }, buttonGridStyles.button]}
activeOpacity={0.6} activeOpacity={0.6}
> >
<MaterialIcon name="settings-outline" style={buttonGridStyles.icon}></MaterialIcon> <MaterialCommunityIcons name="settings" style={buttonGridStyles.icon}></MaterialCommunityIcons>
<Text style={buttonGridStyles.buttonText}>EINSTELLUNGEN</Text> <Text style={buttonGridStyles.buttonText}>EINSTELLUNGEN</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View File

@ -1,33 +1,69 @@
import React from "react"; import React, { useState, useEffect } from "react";
import { import {
StyleSheet, StyleSheet,
Text, Text,
View, View,
TouchableOpacity, TouchableOpacity,
TextInput, TextInput,
Keyboard
} from "react-native"; } from "react-native";
import SetupView from '../components/SetupView'; import SetupView from '../components/SetupView';
import EvilIcon from "react-native-vector-icons/EvilIcons"; import EvilIcon from "react-native-vector-icons/EvilIcons";
import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
import themeColors from '../components/themeColors'; import themeColors from '../components/themeColors';
function WifiPasswordView(props) { function WifiPasswordView(props) {
props = {...props, ...props.route.params}; props = { ...props, ...props.route.params };
useEffect(() => {
Keyboard.addListener("keyboardDidShow", _keyboardDidShow);
Keyboard.addListener("keyboardDidHide", _keyboardDidHide);
// cleanup function
return () => {
Keyboard.removeListener("keyboardDidShow", _keyboardDidShow);
Keyboard.removeListener("keyboardDidHide", _keyboardDidHide);
};
}, []);
const [keyboardStatus, setKeyboardStatus] = useState(undefined);
const [password1, setPassword1] = useState("");
const [password2, setPassword2] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const _keyboardDidShow = () => setKeyboardStatus(true);
const _keyboardDidHide = () => setKeyboardStatus(false);
let iconName = "wifi-strength-" + props.strength; let iconName = "wifi-strength-" + props.strength;
if (props.lock) { if (props.lock) {
iconName += "-lock"; iconName += "-lock";
} }
const onSubmit = () => {
if (props.confirmPwInput && password1 != password2)
setErrorMsg("Passwords don't match");
else if (password1.length < 8)
setErrorMsg("Password has to be at least 8 characters long")
else if (password1.length > 128)
setErrorMsg("Password too long");
else
setErrorMsg("");
};
return ( return (
<SetupView <SetupView
headerText="WiFi Password" headerText="WiFi Password"
lowerRightButtonText="Need help?" lowerRightButtonText="Need help?"
backButton={true}
navigation={props.navigation}
> >
<Text style={styles.subtext}>
{props.subText} {!keyboardStatus &&
</Text> <Text style={styles.subtext}>
{props.subText}
</Text>
}
<View style={styles.formContainer}> <View style={styles.formContainer}>
<View style={[styles.row, { backgroundColor: "rgba(155,155,155,0.8)" }]}> <View style={[styles.row, { backgroundColor: "rgba(155,155,155,0.8)" }]}>
@ -38,6 +74,7 @@ function WifiPasswordView(props) {
<View style={styles.row}> <View style={styles.row}>
<EvilIcon name="lock" style={styles.ssidIcon}></EvilIcon> <EvilIcon name="lock" style={styles.ssidIcon}></EvilIcon>
<TextInput style={styles.passwordInput} <TextInput style={styles.passwordInput}
onChangeText={setPassword1}
autoCompleteType="password" autoCompleteType="password"
placeholder="Password" placeholder="Password"
placeholderTextColor="rgba(255,255,255,0.5)" placeholderTextColor="rgba(255,255,255,0.5)"
@ -49,6 +86,7 @@ function WifiPasswordView(props) {
< View style={styles.row}> < View style={styles.row}>
<EvilIcon name="lock" style={styles.ssidIcon}></EvilIcon> <EvilIcon name="lock" style={styles.ssidIcon}></EvilIcon>
<TextInput style={styles.passwordInput} <TextInput style={styles.passwordInput}
onChangeText={setPassword2}
autoCompleteType="password" autoCompleteType="password"
placeholder="Repeat Password" placeholder="Repeat Password"
placeholderTextColor="rgba(255,255,255,0.5)" placeholderTextColor="rgba(255,255,255,0.5)"
@ -57,10 +95,15 @@ function WifiPasswordView(props) {
</View> </View>
} }
<TouchableOpacity style={[styles.row, styles.button]}> {errorMsg.length > 0 &&
<View>
<Text style={{ color: "red", paddingTop: 10, paddingLeft: 55 }}>{errorMsg}</Text>
</View>
}
<TouchableOpacity style={[styles.row, styles.button]} onPress={onSubmit}>
<Text style={[styles.ssidLabel, { alignSelf: "center", textAlign: "center" }]}>{props.buttonText}</Text> <Text style={[styles.ssidLabel, { alignSelf: "center", textAlign: "center" }]}>{props.buttonText}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</SetupView > </SetupView >
@ -91,9 +134,10 @@ const styles = StyleSheet.create({
subtext: { subtext: {
color: "rgba(255,255,255,1)", color: "rgba(255,255,255,1)",
textAlign: "left", textAlign: "left",
fontSize: 18, fontSize: 16,
lineHeight: 25, lineHeight: 25,
width: "80%", width: "80%",
paddingBottom: 30,
}, },
formContainer: { formContainer: {
}, },

View File

@ -56,6 +56,7 @@ class WifiSelectionView extends React.Component {
constructor() { constructor() {
super(); super();
this.state = { wifiInfo: [] }; this.state = { wifiInfo: [] };
this.mounted = false;
} }
processDeviceResponse(response) { processDeviceResponse(response) {
@ -91,39 +92,56 @@ class WifiSelectionView extends React.Component {
} }
componentDidMount() { componentDidMount() {
let component = this;
component.mounted = true;
this.props.device.conn.scanWifiNetworks().then( this.props.device.conn.scanWifiNetworks().then(
(result) => { (result) => {
this.setState({ wifiInfo: this.processDeviceResponse(result) }) if(component.mounted) {
this.setState({ wifiInfo: this.processDeviceResponse(result) })
}
} }
); );
} }
componentWillUnmount() {
this.mounted = false;
}
render() { render() {
let inner; let inner;
if (this.state.wifiInfo.length > 0) { if (this.state.wifiInfo.length > 0) {
inner = ( inner = (
<ScrollView> <View style={styles.listContainer}>
{this.state.wifiInfo.map(e => ( <ScrollView style={{backgroundColor: "red", centerContent: true, paddingTop: 20}}>
<WifiListElement {this.state.wifiInfo.map(e => (
text={e.ssid} <WifiListElement
strength={e.strength} text={e.ssid}
lock={e.locked} strength={e.strength}
key={e.ssid} lock={e.locked}
onPress={() => { this.props.navigation.navigate("WifiPasswordView", { key={e.ssid}
ssid: e.ssid, onPress={() => {
lock: e.locked, this.props.navigation.navigate("WifiPasswordView", {
strength: e.strength, ssid: e.ssid,
buttonText: "Set Password", lock: e.locked,
subText: "Please enter password for your home WiFi", strength: e.strength,
}); }}> confirmPwInput: false,
</WifiListElement>) buttonText: "OK",
)} subText: "Please enter the password for your home WiFi",
</ScrollView>) });
}}>
</WifiListElement>)
)}
</ScrollView>
</View>
)
} }
else { else {
inner = (<ActivityIndicator size="large" color="#ffffff" /> inner = (
<View style={{ alignItems: "center", justifyContent:"center", height: "100%" }}>
<View style={{ paddingBottom: 20 }}><Text style={{ fontSize: 16, color: "#fff"}}>Scanning WiFi networks</Text></View>
<ActivityIndicator size="large" color="#ffffff" />
</View>
) )
} }
@ -131,11 +149,19 @@ class WifiSelectionView extends React.Component {
<SetupView <SetupView
headerText="WiFi Connection" headerText="WiFi Connection"
lowerLeftButtonText="My WiFi wasn't found" lowerLeftButtonText="My WiFi wasn't found"
onLowerLeftButtonPress={() => {
this.props.navigation.navigate("WifiPasswordView", {
ssid: "swimtracker-E2842S", // todo real id here
lock: true,
strength: 4,
confirmPwInput: true,
buttonText: "Set Password",
subText: "Use this option only if you're home WiFi doesn't reach the SwimTracker. The SwimTracker creates its own WiFi with the password you set here.",
});
}}
lowerRightButtonText="Need help?" lowerRightButtonText="Need help?"
> >
<View style={styles.listContainer}> {inner}
{inner}
</View>
</SetupView> </SetupView>
) )
} }
@ -145,7 +171,7 @@ class WifiSelectionView extends React.Component {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
listContainer: { listContainer: {
height: "75%", height: "75%",
//backgroundColor: "red", flex: 1,
} }
}); });