Compare commits
No commits in common. "96db91f027ed571fe352f95db593827ffa4f670d" and "e5a5e141a990e9ae65b6ecb1449bd0cb832f732f" have entirely different histories.
96db91f027
...
e5a5e141a9
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,15 +0,0 @@
|
||||||
> Why do I have a folder named ".expo" in my project?
|
|
||||||
|
|
||||||
The ".expo" folder is created when an Expo project is started using "expo start" command.
|
|
||||||
|
|
||||||
> What do the files contain?
|
|
||||||
|
|
||||||
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
|
|
||||||
- "packager-info.json": contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator.
|
|
||||||
- "settings.json": contains the server configuration that is used to serve the application manifest.
|
|
||||||
|
|
||||||
> Should I commit the ".expo" folder?
|
|
||||||
|
|
||||||
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
|
|
||||||
|
|
||||||
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"hostType": "lan",
|
|
||||||
"lanType": "ip",
|
|
||||||
"dev": true,
|
|
||||||
"minify": false,
|
|
||||||
"urlRandomness": null,
|
|
||||||
"https": false
|
|
||||||
}
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
node_modules/**/*
|
||||||
|
.expo/*
|
||||||
|
npm-debug.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
*.orig.*
|
||||||
|
web-build/
|
||||||
|
web-report/
|
||||||
|
/dist
|
||||||
|
/venv
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import AppLoading from 'expo-app-loading';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import * as Font from 'expo-font';
|
import * as Font from 'expo-font';
|
||||||
|
|
||||||
|
@ -13,6 +13,12 @@ import { persistStore, persistReducer } from 'redux-persist'
|
||||||
import hardSet from 'redux-persist/lib/stateReconciler/hardSet'
|
import hardSet from 'redux-persist/lib/stateReconciler/hardSet'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
|
|
||||||
|
// Internationalization
|
||||||
|
import * as Localization from 'expo-localization';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import en from "./locales/en/translations";
|
||||||
|
import de from "./locales/de/translations";
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
@ -26,6 +32,16 @@ import ConnectingView from './views/ConnectingView';
|
||||||
import WifiSelectionView from './views/WifiSelectionView';
|
import WifiSelectionView from './views/WifiSelectionView';
|
||||||
import WifiPasswordView from './views/WifiPasswordView';
|
import WifiPasswordView from './views/WifiPasswordView';
|
||||||
|
|
||||||
|
|
||||||
|
// Set the key-value pairs for the different languages you want to support.
|
||||||
|
i18n.translations = {
|
||||||
|
en: en,
|
||||||
|
de: de,
|
||||||
|
};
|
||||||
|
i18n.locale = Localization.locale; // Set the locale once at the beginning of your app.
|
||||||
|
//i18n.locale = "en-US";
|
||||||
|
i18n.fallbacks = true; // When a value is missing from a language it'll fallback to another language with the key present.
|
||||||
|
|
||||||
const persistConfig = {
|
const persistConfig = {
|
||||||
key: 'root',
|
key: 'root',
|
||||||
storage: AsyncStorage,
|
storage: AsyncStorage,
|
||||||
|
@ -37,9 +53,6 @@ const store = createStore(persistedReducer);
|
||||||
const persistor = persistStore(store);
|
const persistor = persistStore(store);
|
||||||
const Stack = createStackNavigator();
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
// Keep the splash screen visible while we fetch resources
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
|
||||||
|
|
||||||
export default class App extends React.Component {
|
export default class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -73,10 +86,9 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if(this.state.isReady) {
|
if (!this.state.isReady) {
|
||||||
SplashScreen.hideAsync();
|
return <AppLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenOptions = {
|
const screenOptions = {
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
};
|
};
|
||||||
|
@ -141,8 +153,8 @@ export default class App extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PersistGate persistor={persistor}>
|
<PersistGate loading={<AppLoading />} persistor={persistor}>
|
||||||
<NavigationContainer >
|
<NavigationContainer>
|
||||||
<Stack.Navigator >
|
<Stack.Navigator >
|
||||||
{activeView}
|
{activeView}
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
|
@ -1,3 +0,0 @@
|
||||||
/node_modules
|
|
||||||
/.expo
|
|
||||||
.directory
|
|
|
@ -1,45 +0,0 @@
|
||||||
let config = {
|
|
||||||
"expo": {
|
|
||||||
"name": "SwimTracker",
|
|
||||||
"slug": "SwimTracker",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"icon": "./assets/icon.png",
|
|
||||||
"userInterfaceStyle": "light",
|
|
||||||
"splash": {
|
|
||||||
"image": "./assets/splash.png",
|
|
||||||
"resizeMode": "cover",
|
|
||||||
"backgroundColor": "#ffffff"
|
|
||||||
},
|
|
||||||
"assetBundlePatterns": [
|
|
||||||
"**/*"
|
|
||||||
],
|
|
||||||
"ios": {
|
|
||||||
"supportsTablet": false,
|
|
||||||
"icon": "./assets/icon-ios.png",
|
|
||||||
"bundleIdentifier": "tech.bauer.swimtracker"
|
|
||||||
},
|
|
||||||
"android": {
|
|
||||||
"package": "tech.bauer.swimtracker",
|
|
||||||
"permissions": [ "INTERNET" ]
|
|
||||||
},
|
|
||||||
"web": {
|
|
||||||
"favicon": "./assets/favicon.png"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"expo-localization",
|
|
||||||
["expo-build-properties", {
|
|
||||||
"android": {
|
|
||||||
"usesCleartextTraffic": true
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
],
|
|
||||||
"extra": {
|
|
||||||
"eas": {
|
|
||||||
"projectId": "04899022-7d11-43b0-a31a-394a62339352"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
|
@ -1,17 +0,0 @@
|
||||||
> Why do I have a folder named ".expo" in my project?
|
|
||||||
|
|
||||||
The ".expo" folder is created when an Expo project is started using "expo start" command.
|
|
||||||
|
|
||||||
> What does the "packager-info.json" file contain?
|
|
||||||
|
|
||||||
The "packager-info.json" file contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator.
|
|
||||||
|
|
||||||
> What does the "settings.json" file contain?
|
|
||||||
|
|
||||||
The "settings.json" file contains the server configuration that is used to serve the application manifest.
|
|
||||||
|
|
||||||
> Should I commit the ".expo" folder?
|
|
||||||
|
|
||||||
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
|
|
||||||
|
|
||||||
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
|
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,109 +0,0 @@
|
||||||
import SwimTrackerWebsocketConnection from "../data_processing/SwimTrackerWebsocketConnection.js";
|
|
||||||
import yargs from 'yargs';
|
|
||||||
import WS from 'ws';
|
|
||||||
import { SerialPort } from 'serialport'
|
|
||||||
import { ReadlineParser } from '@serialport/parser-readline'
|
|
||||||
import chalk from 'chalk';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens serial port and calls provided function on each line
|
|
||||||
*
|
|
||||||
* @param {string} serialPortPath
|
|
||||||
* @param {(str) => any} onLineReceived
|
|
||||||
*/
|
|
||||||
function openSerialConnection(serialPortPath, onLineReceived) {
|
|
||||||
const port = new SerialPort({
|
|
||||||
path: serialPortPath,
|
|
||||||
baudRate: 115200
|
|
||||||
});
|
|
||||||
const parser = port.pipe(new ReadlineParser());
|
|
||||||
parser.on('data', onLineReceived);
|
|
||||||
}
|
|
||||||
|
|
||||||
function basicTest(deviceIpOrHostname, serialPort) {
|
|
||||||
|
|
||||||
let lastHostLog = undefined;
|
|
||||||
let lastDeviceLog = undefined;
|
|
||||||
|
|
||||||
function log(color, main, startCol) {
|
|
||||||
if(lastHostLog === undefined)
|
|
||||||
lastHostLog = Date.now();
|
|
||||||
const currentTime = Date.now();
|
|
||||||
const dt = currentTime - lastHostLog;
|
|
||||||
lastHostLog = currentTime;
|
|
||||||
|
|
||||||
console.log(color((startCol).padEnd(9), "|", String(dt).padStart(5), "|", main));
|
|
||||||
}
|
|
||||||
|
|
||||||
function logDeviceTime(color, main, column1, time) {
|
|
||||||
if(lastDeviceLog === undefined)
|
|
||||||
lastDeviceLog = time;
|
|
||||||
const dt = time - lastDeviceLog;
|
|
||||||
lastDeviceLog = time;
|
|
||||||
|
|
||||||
console.log(color((column1).padEnd(9), "|", String(dt).padStart(5), "|", main));
|
|
||||||
}
|
|
||||||
|
|
||||||
const onStarted = (sessionid) => { log(chalk.yellow, "Session started " + sessionid, "WS Msg") };
|
|
||||||
const onStopped = () => { log(chalk.yellow, "Session stopped", "WS Msg") };
|
|
||||||
const onWifiStateInfo = (wifiInfo) => { log(chalk.cyan, JSON.stringify(wifiInfo), "WS Wifi") };
|
|
||||||
const onWebsocketLog = (logMsg) => {logDeviceTime(chalk.blue, logMsg.msg, "WS Log", String(logMsg.time))};
|
|
||||||
const onDisconnect = () => { log(chalk.red, "Disconnected", "WS Msg") };
|
|
||||||
const onConnect = () => {
|
|
||||||
log(chalk.blue,"Connected to device", "WS Msg")
|
|
||||||
conn.sendLogStreamStartCommand();
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = [];
|
|
||||||
|
|
||||||
const onNewMeasurementData = (newDataAsUint16Arr) => {
|
|
||||||
const arr = Array.from(newDataAsUint16Arr);
|
|
||||||
newDataAsUint16Arr.forEach(element => {
|
|
||||||
data.push(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
const existing = "(" + String(data.length).padStart(5) + ") ";
|
|
||||||
if(arr.length > 5) {
|
|
||||||
log(chalk.yellow, existing + "Bulk " + arr.length + " measurements", "WS Data");
|
|
||||||
} else {
|
|
||||||
log(chalk.gray, existing + JSON.stringify(Array.from(newDataAsUint16Arr)), "WS Data");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if(serialPort) {
|
|
||||||
openSerialConnection(serialPort, (receivedLine) => {
|
|
||||||
log(chalk.gray, receivedLine, "Serial");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const conn = new SwimTrackerWebsocketConnection(deviceIpOrHostname, onNewMeasurementData, onStarted, onStopped,
|
|
||||||
onWifiStateInfo, onConnect, onDisconnect,
|
|
||||||
{ WebSocket: WS });
|
|
||||||
conn.onLogMessage = onWebsocketLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
basicTest("192.168.178.56", "/dev/ttyUSB1");
|
|
||||||
//basicTest("192.168.178.56", undefined);
|
|
||||||
|
|
||||||
// {"speeds":[0.4, -0.3],"durations":[1500, 1500]}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Testrun:
|
|
||||||
Captures:
|
|
||||||
- streamed data from websocket
|
|
||||||
- log messages from serial port
|
|
||||||
- output of serial port
|
|
||||||
File format:
|
|
||||||
- timestamp of aquisition
|
|
||||||
- type
|
|
||||||
- websocket message
|
|
||||||
- serial port line
|
|
||||||
Logging:
|
|
||||||
- websocket errors always
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
- replay file
|
|
||||||
- data as csv
|
|
||||||
*/
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"cli": {
|
|
||||||
"version": ">= 5.4.0"
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"development": {
|
|
||||||
"developmentClient": true,
|
|
||||||
"distribution": "internal",
|
|
||||||
"android": {
|
|
||||||
"buildType": "apk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"preview": {
|
|
||||||
"distribution": "internal",
|
|
||||||
"android": {
|
|
||||||
"buildType": "apk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"android": {
|
|
||||||
"buildType": "apk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"submit": {
|
|
||||||
"production": {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
// This file was generated by "npx expo customize metro.config.js"
|
|
||||||
// to fix an error in make-plural (from i18n) when building for android
|
|
||||||
|
|
||||||
|
|
||||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
|
||||||
const { getDefaultConfig } = require('expo/metro-config');
|
|
||||||
|
|
||||||
/** @type {import('expo/metro-config').MetroConfig} */
|
|
||||||
const config = getDefaultConfig(__dirname);
|
|
||||||
|
|
||||||
// I added this line, the rest was already there, after the command run
|
|
||||||
config.watcher.additionalExts.push('mjs', 'cjs');
|
|
||||||
|
|
||||||
module.exports = config;
|
|
|
@ -1,46 +0,0 @@
|
||||||
{
|
|
||||||
"name": "swimtracker",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "node_modules/expo/AppEntry.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "expo start",
|
|
||||||
"android": "expo start --android",
|
|
||||||
"ios": "expo start --ios",
|
|
||||||
"web": "expo start --web"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/webpack-config": "^19.0.0",
|
|
||||||
"@react-native-async-storage/async-storage": "1.18.2",
|
|
||||||
"@react-navigation/native": "^6.1.8",
|
|
||||||
"@react-navigation/stack": "^6.3.18",
|
|
||||||
"expo": "~49.0.13",
|
|
||||||
"expo-build-properties": "~0.8.3",
|
|
||||||
"expo-dev-client": "~2.4.11",
|
|
||||||
"expo-localization": "~14.3.0",
|
|
||||||
"expo-splash-screen": "~0.20.5",
|
|
||||||
"expo-status-bar": "~1.6.0",
|
|
||||||
"i18n-js": "^4.3.2",
|
|
||||||
"immutable": "^5.0.0-beta.4",
|
|
||||||
"moment": "^2.29.4",
|
|
||||||
"msgpack-lite": "^0.1.26",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-native": "0.72.6",
|
|
||||||
"react-native-gesture-handler": "~2.12.0",
|
|
||||||
"react-native-safe-area-context": "4.6.3",
|
|
||||||
"react-native-screens": "~3.22.0",
|
|
||||||
"react-native-svg": "13.9.0",
|
|
||||||
"react-native-swipe-list-view": "^3.2.9",
|
|
||||||
"react-native-web": "~0.19.6",
|
|
||||||
"react-redux": "^8.1.2",
|
|
||||||
"react-xml-parser": "^1.1.8",
|
|
||||||
"reconnecting-websocket": "^4.4.0",
|
|
||||||
"redux": "^4.2.1",
|
|
||||||
"redux-persist": "^6.0.0",
|
|
||||||
"websocket": "^1.0.34"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.20.0"
|
|
||||||
},
|
|
||||||
"private": true
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import {i18n} from './i18n';
|
|
||||||
|
|
||||||
const locale = i18n.locale;
|
|
||||||
|
|
||||||
const fullDateFormat = Intl.DateTimeFormat(locale, {
|
|
||||||
weekday: "short",
|
|
||||||
year: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
const timeFormatter = Intl.DateTimeFormat(locale, {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
// Doesn't work on android yet (so use full time for now instead of relative time)
|
|
||||||
const relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto"});
|
|
||||||
|
|
||||||
const DIVISIONS = [
|
|
||||||
{ amount: 60, name: "seconds" },
|
|
||||||
{ amount: 60, name: "minutes" },
|
|
||||||
{ amount: 24, name: "hours" },
|
|
||||||
{ amount: 7, name: "days" },
|
|
||||||
{ amount: 4.34524, name: "weeks" },
|
|
||||||
{ amount: 12, name: "months" },
|
|
||||||
{ amount: Number.POSITIVE_INFINITY, name: "years" },
|
|
||||||
];
|
|
||||||
|
|
||||||
function formatTimeAgo(date) {
|
|
||||||
let duration = (date - new Date()) / 1000
|
|
||||||
for (let i = 0; i < DIVISIONS.length; i++) {
|
|
||||||
const division = DIVISIONS[i]
|
|
||||||
if (Math.abs(duration) < division.amount) {
|
|
||||||
const suffix = (division.name === "days") ? ", " + timeFormatter.format(date) : "";
|
|
||||||
return relativeTimeFormatter.format(Math.round(duration), division.name) + suffix
|
|
||||||
}
|
|
||||||
duration /= division.amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function timeSince(timeStamp) {
|
|
||||||
const now = Math.floor((new Date()).getTime() / 1000);
|
|
||||||
const secondsPassed = now - timeStamp;
|
|
||||||
const daysPassed = secondsPassed / 60 / 60 / 24;
|
|
||||||
const timeStampDate = new Date(timeStamp * 1000);
|
|
||||||
//if (daysPassed < 2)
|
|
||||||
// return formatTimeAgo(timeStampDate);
|
|
||||||
//else
|
|
||||||
return fullDateFormat.format(timeStampDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const toTimeStr = seconds => {
|
|
||||||
let minuteStr = String(Math.floor(seconds / 60));
|
|
||||||
if (minuteStr.length < 2)
|
|
||||||
minuteStr = "0" + minuteStr;
|
|
||||||
let secondStr = String(Math.floor(seconds % 60));
|
|
||||||
if (secondStr.length < 2)
|
|
||||||
secondStr = "0" + secondStr;
|
|
||||||
return minuteStr + ":" + secondStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { toTimeStr, timeSince };
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { I18n } from "i18n-js";
|
|
||||||
import * as Localization from 'expo-localization';
|
|
||||||
|
|
||||||
const translation_store = {
|
|
||||||
en: {
|
|
||||||
connecting: "Connecting",
|
|
||||||
connectSubtext: "Please connect your phone to the WiFi of your SwimTracker",
|
|
||||||
simpleMode: "Simple Mode",
|
|
||||||
advancedMode: "Advanced Mode",
|
|
||||||
help: "Need help?",
|
|
||||||
settings: "Settings",
|
|
||||||
lastSessions: "Last Sessions",
|
|
||||||
mainMenu_social: "Social",
|
|
||||||
mainMenu_swimNow: "Swim now",
|
|
||||||
delete_session_button: "Delete",
|
|
||||||
},
|
|
||||||
de : {
|
|
||||||
connecting: "Verbindung aufbauen",
|
|
||||||
connectSubtext: "Gehe entweder in das WLAN deines SwimTrackers oder in dein eigenes WLAN, falls du den SwimTracker schon eingerichtet hast.",
|
|
||||||
simpleMode: "Weniger Einstellungen",
|
|
||||||
advancedMode: "Mehr Einstellungen",
|
|
||||||
settings: "Einstellungen",
|
|
||||||
help: "Hilfe",
|
|
||||||
lastSessions: "Letzte Sessions",
|
|
||||||
mainMenu_social: "Freunde",
|
|
||||||
mainMenu_swimNow: "Jetzt schwimmen",
|
|
||||||
delete_session_button: "Löschen",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const i18n = new I18n();
|
|
||||||
i18n.store(translation_store);
|
|
||||||
i18n.defaultLocale = "en";
|
|
||||||
i18n.enableFallback = true;
|
|
||||||
i18n.locale = Localization.locale;
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "SwimTracker",
|
||||||
|
"slug": "swimtracker",
|
||||||
|
"privacy": "public",
|
||||||
|
"platforms": [
|
||||||
|
"ios",
|
||||||
|
"android",
|
||||||
|
"web"
|
||||||
|
],
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/splash.png",
|
||||||
|
"resizeMode": "cover",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"updates": {
|
||||||
|
"fallbackToCacheTimeout": 0
|
||||||
|
},
|
||||||
|
"assetBundlePatterns": [
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": false,
|
||||||
|
"icon": "./assets/icon-ios.png",
|
||||||
|
"bundleIdentifier": "tech.bauer.swimtracker"
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"android": {
|
||||||
|
"package": "tech.bauer.swimtracker"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 419 KiB After Width: | Height: | Size: 419 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 253 KiB |
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 340 KiB |
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 602 KiB |
Before Width: | Height: | Size: 740 KiB After Width: | Height: | Size: 740 KiB |
Before Width: | Height: | Size: 899 KiB After Width: | Height: | Size: 899 KiB |
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
DATABASE_URL = "sqlite:///db.sqlite"
|
||||||
|
JWT_SECRET = "4SmRyfsvG86R9jZQfTshfoDlcxYlueHmkMXJbszp"
|
|
@ -0,0 +1,21 @@
|
||||||
|
import databases
|
||||||
|
import sqlalchemy
|
||||||
|
from starlette import requests
|
||||||
|
from config import DATABASE_URL
|
||||||
|
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
|
||||||
|
database = databases.Database(DATABASE_URL)
|
||||||
|
Base: DeclarativeMeta = declarative_base()
|
||||||
|
|
||||||
|
engine = sqlalchemy.create_engine(
|
||||||
|
DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
DbSession = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db(request: Request):
|
||||||
|
return request.state.db
|
|
@ -0,0 +1,38 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from users import add_user_routers, User
|
||||||
|
from db import database, engine, Base, DbSession
|
||||||
|
from starlette.requests import Request
|
||||||
|
from routes import router as api_router
|
||||||
|
import models
|
||||||
|
|
||||||
|
|
||||||
|
def get_app() -> FastAPI:
|
||||||
|
application = FastAPI(title="swimtracker", debug=True, version="0.1")
|
||||||
|
application.include_router(api_router)
|
||||||
|
add_user_routers(application)
|
||||||
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def db_session_middleware(request: Request, call_next):
|
||||||
|
request.state.db = DbSession()
|
||||||
|
response = await call_next(request)
|
||||||
|
request.state.db.close()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup() -> None:
|
||||||
|
print("creating")
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
if not database.is_connected:
|
||||||
|
await database.connect()
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown() -> None:
|
||||||
|
if database.is_connected:
|
||||||
|
await database.disconnect()
|
|
@ -0,0 +1,42 @@
|
||||||
|
from db import Base
|
||||||
|
from sqlalchemy import Column, Integer, Index, LargeBinary, ForeignKey, and_, or_
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class Session(Base):
|
||||||
|
__tablename__ = "session"
|
||||||
|
|
||||||
|
device_id = Column(Integer, primary_key=True)
|
||||||
|
start_time = Column(Integer, primary_key=True)
|
||||||
|
data = Column(LargeBinary(1024 * 1024 * 2), nullable=False)
|
||||||
|
user = Column(ForeignKey("user.id"), nullable=False)
|
||||||
|
|
||||||
|
value_right_shift = Column(Integer)
|
||||||
|
tare_value = Column(Integer)
|
||||||
|
kg_factor = Column(Integer)
|
||||||
|
|
||||||
|
Index('device_id', 'start_time', unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FriendRequest(Base):
|
||||||
|
__tablename__ = "friend_request"
|
||||||
|
requesting_user = Column(ForeignKey("user.id"), primary_key=True)
|
||||||
|
receiving_user = Column(ForeignKey("user.id"), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Friendship(Base):
|
||||||
|
__tablename__ = "friendship"
|
||||||
|
user_id = Column(ForeignKey("user.id"), primary_key=True)
|
||||||
|
friend_id = Column(ForeignKey("user.id"), primary_key=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def befriend(db, userid1, userid2):
|
||||||
|
db.add(Friendship(user_id=userid1, friend_id=userid2))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def are_friends(db, userid1, userid2):
|
||||||
|
query_filter = or_(
|
||||||
|
and_(Friendship.user_id == userid1, Friendship.friend_id == userid2),
|
||||||
|
and_(Friendship.user_id == userid2, Friendship.friend_id == userid1),
|
||||||
|
)
|
||||||
|
return db.query(Friendship).filter(query_filter).count() > 0
|
|
@ -0,0 +1,94 @@
|
||||||
|
import base64
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from typing import List
|
||||||
|
import schemas
|
||||||
|
from users import User, UserDB, UserTable, current_user
|
||||||
|
from db import get_db
|
||||||
|
import models
|
||||||
|
from sqlalchemy.orm import Session as DbSession, lazyload
|
||||||
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from fastapi import status
|
||||||
|
from sqlalchemy.sql import select
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sessions",
|
||||||
|
response_model=schemas.Session,
|
||||||
|
tags=["sessions"],
|
||||||
|
status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_session(session: schemas.SessionBase,
|
||||||
|
db: DbSession = Depends(get_db),
|
||||||
|
user: User = Depends(current_user)):
|
||||||
|
session_props = session.dict()
|
||||||
|
session_props['user'] = user.id
|
||||||
|
session_props['data'] = base64.b64decode(session_props['data'])
|
||||||
|
db_obj = models.Session(**session_props)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sessions", response_model=List[schemas.Session], tags=["sessions"])
|
||||||
|
def list_sessions(skip=0,
|
||||||
|
limit=100,
|
||||||
|
db: DbSession = Depends(get_db),
|
||||||
|
user: User = Depends(current_user)):
|
||||||
|
return db.query(models.Session).filter(models.Session.user == user.id).order_by(
|
||||||
|
models.Session.start_time.desc()).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/friends/request_friendship/{user_id}", tags=["friends"])
|
||||||
|
def create_friend_request(other_user_id: str,
|
||||||
|
db: DbSession = Depends(get_db),
|
||||||
|
user: User = Depends(current_user)):
|
||||||
|
if models.Friendship.are_friends(db, other_user_id, user.id):
|
||||||
|
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, detail="already friends")
|
||||||
|
|
||||||
|
FR = models.FriendRequest
|
||||||
|
friend_request_from_other_user = db.query(FR).filter(FR.requesting_user == other_user_id,
|
||||||
|
FR.receiving_user == user.id).count()
|
||||||
|
if friend_request_from_other_user > 0:
|
||||||
|
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE,
|
||||||
|
detail="Friend request exist from other user, accept it")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
new_friend_request = FR(requesting_user=user.id, receiving_user=other_user_id)
|
||||||
|
db.add(new_friend_request)
|
||||||
|
db.commit()
|
||||||
|
return {"msg": "ok"}
|
||||||
|
except IntegrityError:
|
||||||
|
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE,
|
||||||
|
detail="Friend request already exists")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/friends/accept_friendship/{user_id}", tags=["friends"])
|
||||||
|
def accept_friend_request(other_user_id: str,
|
||||||
|
db: DbSession = Depends(get_db),
|
||||||
|
user: User = Depends(current_user)):
|
||||||
|
FR = models.FriendRequest
|
||||||
|
try:
|
||||||
|
friend_request = db.query(FR).filter(FR.requesting_user == other_user_id,
|
||||||
|
FR.receiving_user == user.id).one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise HTTPException(status_code=404, detail="No matching friend request found")
|
||||||
|
|
||||||
|
models.Friendship.befriend(db, other_user_id, user.id)
|
||||||
|
db.delete(friend_request)
|
||||||
|
db.commit()
|
||||||
|
return {"msg": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/friends", tags=["friends"], response_model=schemas.FriendsInfo)
|
||||||
|
def list_friends_info(db: DbSession = Depends(get_db), user: User = Depends(current_user)):
|
||||||
|
user_obj = db.query(UserTable).filter(UserTable.id == user.id).one()
|
||||||
|
return schemas.FriendsInfo(incoming_requests=user_obj.friend_requests_in,
|
||||||
|
outgoing_requests=user_obj.friend_requests_out)
|
||||||
|
|
||||||
|
|
||||||
|
# todo: remove friend requests
|
||||||
|
# todo: remove friendship
|
||||||
|
# todo: search user by email
|
||||||
|
# todo: add usernames to users
|
||||||
|
# todo: search by username
|
|
@ -0,0 +1,40 @@
|
||||||
|
from typing import Optional, List
|
||||||
|
from pydantic import BaseModel, conint, UUID4
|
||||||
|
from pydantic.networks import EmailStr
|
||||||
|
|
||||||
|
|
||||||
|
class SessionBase(BaseModel):
|
||||||
|
device_id: int
|
||||||
|
start_time: conint(gt=1546297200)
|
||||||
|
data: str
|
||||||
|
|
||||||
|
value_right_shift: Optional[conint(ge=0, le=32)]
|
||||||
|
tare_value: Optional[conint(ge=0)]
|
||||||
|
kg_factor: Optional[conint(ge=0)]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class UserInfo(BaseModel):
|
||||||
|
id: UUID4
|
||||||
|
email: EmailStr
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class Session(SessionBase):
|
||||||
|
user: UUID4
|
||||||
|
|
||||||
|
|
||||||
|
class FriendRequestCreate(BaseModel):
|
||||||
|
other_user_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class FriendsInfo(BaseModel):
|
||||||
|
incoming_requests: List[UserInfo]
|
||||||
|
outgoing_requests: List[UserInfo]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
|
@ -0,0 +1,92 @@
|
||||||
|
from fastapi_users import FastAPIUsers, models
|
||||||
|
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
|
||||||
|
from fastapi_users.authentication import JWTAuthentication
|
||||||
|
from config import JWT_SECRET
|
||||||
|
from fastapi import Request
|
||||||
|
from db import database, Base
|
||||||
|
from sqlalchemy.orm import relationship, backref
|
||||||
|
from sqlalchemy import Integer, Column
|
||||||
|
from fastapi_users.models import BaseUser
|
||||||
|
|
||||||
|
|
||||||
|
class User(models.BaseUser):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(models.BaseUserCreate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(User, models.BaseUserUpdate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserDB(User, models.BaseUserDB):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserTable(Base, SQLAlchemyBaseUserTable):
|
||||||
|
#id = Column(Integer, primary_key=True)
|
||||||
|
sessions = relationship("Session")
|
||||||
|
friend_requests_in = relationship(
|
||||||
|
"UserTable",
|
||||||
|
secondary="friend_request",
|
||||||
|
primaryjoin=("UserTable.id == FriendRequest.receiving_user"),
|
||||||
|
secondaryjoin=("UserTable.id == FriendRequest.requesting_user"),
|
||||||
|
backref=backref("friend_requests_out"))
|
||||||
|
friends = relationship('UserTable',
|
||||||
|
secondary="friendship",
|
||||||
|
primaryjoin=("UserTable.id == Friendship.user_id"),
|
||||||
|
secondaryjoin=("UserTable.id == Friendship.friend_id"))
|
||||||
|
|
||||||
|
|
||||||
|
user_db = SQLAlchemyUserDatabase(UserDB, database, UserTable.__table__)
|
||||||
|
jwt_authentication = JWTAuthentication(secret=JWT_SECRET,
|
||||||
|
lifetime_seconds=60 * 60 * 8,
|
||||||
|
tokenUrl="auth/jwt/login")
|
||||||
|
|
||||||
|
fastapi_users = FastAPIUsers(
|
||||||
|
user_db,
|
||||||
|
[jwt_authentication],
|
||||||
|
User,
|
||||||
|
UserCreate,
|
||||||
|
UserUpdate,
|
||||||
|
UserDB,
|
||||||
|
)
|
||||||
|
current_user = fastapi_users.current_user(active=True, verified=True)
|
||||||
|
current_superuser = fastapi_users.current_user(active=True, superuser=True, verified=True)
|
||||||
|
|
||||||
|
|
||||||
|
def on_after_register(user: UserDB, request: Request):
|
||||||
|
print(f"User {user.id} has registered.")
|
||||||
|
|
||||||
|
|
||||||
|
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||||
|
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||||
|
|
||||||
|
|
||||||
|
def after_verification_request(user: UserDB, token: str, request: Request):
|
||||||
|
print(f"Verification requested for user {user.id}. Verification token: {token}")
|
||||||
|
|
||||||
|
|
||||||
|
def add_user_routers(app):
|
||||||
|
app.include_router(fastapi_users.get_auth_router(jwt_authentication),
|
||||||
|
prefix="/auth/jwt",
|
||||||
|
tags=["auth"])
|
||||||
|
|
||||||
|
app.include_router(fastapi_users.get_register_router(on_after_register),
|
||||||
|
prefix="/auth",
|
||||||
|
tags=["auth"])
|
||||||
|
app.include_router(
|
||||||
|
fastapi_users.get_reset_password_router(JWT_SECRET,
|
||||||
|
after_forgot_password=on_after_forgot_password),
|
||||||
|
prefix="/auth",
|
||||||
|
tags=["auth"],
|
||||||
|
)
|
||||||
|
app.include_router(
|
||||||
|
fastapi_users.get_verify_router(JWT_SECRET,
|
||||||
|
after_verification_request=after_verification_request),
|
||||||
|
prefix="/auth",
|
||||||
|
tags=["auth"],
|
||||||
|
)
|
||||||
|
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
|
|
@ -0,0 +1,73 @@
|
||||||
|
import os
|
||||||
|
from typing import Any, Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from src.db import Base, get_db
|
||||||
|
from src.main import app as _app
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
# Default to using sqlite in memory for fast tests.
|
||||||
|
# Can be overridden by environment variable for testing in CI against other
|
||||||
|
# database engines
|
||||||
|
SQLALCHEMY_DATABASE_URL = os.getenv('TEST_DATABASE_URL', "sqlite://")
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def app() -> Generator[FastAPI, Any, None]:
|
||||||
|
"""
|
||||||
|
Create a fresh database on each test case.
|
||||||
|
"""
|
||||||
|
Base.metadata.create_all(engine) # Create the tables.
|
||||||
|
yield _app
|
||||||
|
Base.metadata.drop_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def db_session(app: FastAPI) -> Generator[Session, Any, None]:
|
||||||
|
"""
|
||||||
|
Creates a fresh sqlalchemy session for each test that operates in a
|
||||||
|
transaction. The transaction is rolled back at the end of each test ensuring
|
||||||
|
a clean state.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# connect to the database
|
||||||
|
connection = engine.connect()
|
||||||
|
# begin a non-ORM transaction
|
||||||
|
transaction = connection.begin()
|
||||||
|
# bind an individual Session to the connection
|
||||||
|
session = Session(bind=connection)
|
||||||
|
yield session # use the session in tests.
|
||||||
|
session.close()
|
||||||
|
# rollback - everything that happened with the
|
||||||
|
# Session above (including calls to commit())
|
||||||
|
# is rolled back.
|
||||||
|
transaction.rollback()
|
||||||
|
# return connection to the Engine
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client(app: FastAPI, db_session: Session) -> Generator[TestClient, Any, None]:
|
||||||
|
"""
|
||||||
|
Create a new FastAPI TestClient that uses the `db_session` fixture to override
|
||||||
|
the `get_db` dependency that is injected into routes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _get_test_db():
|
||||||
|
try:
|
||||||
|
yield db_session
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = _get_test_db
|
||||||
|
with TestClient(app) as client:
|
||||||
|
yield client
|
|
@ -0,0 +1,11 @@
|
||||||
|
from backend.src.db import DbSession
|
||||||
|
from src.schemas import Session
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from src.db import DbSession
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
#----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_create(app: FastAPI, db_session: DbSession, client: TestClient):
|
||||||
|
pass
|
|
@ -22,7 +22,7 @@ const IconCard = props => {
|
||||||
<Text style={{ color: 'white', fontSize: props.fontSize, textAlign: "center" }}> {props.value}</Text>
|
<Text style={{ color: 'white', fontSize: props.fontSize, textAlign: "center" }}> {props.value}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ alignItems: 'center', justifyContent: 'center', paddingLeft: 20 }}>
|
<View style={{ alignItems: 'center', justifyContent: 'center', paddingLeft: 20 }}>
|
||||||
<IconClass style={{ color: 'white', fontSize: 30 }} name={props.iconName} />
|
<IconClass style={{ color: 'white', fontSize: 40 }} name={props.iconName} />
|
||||||
<Text style={{ color: 'white', marginTop: 5 }}> {props.label}</Text>
|
<Text style={{ color: 'white', marginTop: 5 }}> {props.label}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
|
@ -15,7 +15,6 @@ const OpCodes = {
|
||||||
WIFI_STATE_RESPONSE: 8,
|
WIFI_STATE_RESPONSE: 8,
|
||||||
WIFI_SCAN_RESPONSE: 9,
|
WIFI_SCAN_RESPONSE: 9,
|
||||||
APP_LAYER_PING: 10,
|
APP_LAYER_PING: 10,
|
||||||
LOG_UPDATE: 11,
|
|
||||||
|
|
||||||
// from frontend to device
|
// from frontend to device
|
||||||
START_SESSION: 128,
|
START_SESSION: 128,
|
||||||
|
@ -26,26 +25,13 @@ const OpCodes = {
|
||||||
WIFI_STATE_SET: 133,
|
WIFI_STATE_SET: 133,
|
||||||
WIFI_STATE_GET: 134,
|
WIFI_STATE_GET: 134,
|
||||||
WIFI_TRIGGER_SCAN: 135,
|
WIFI_TRIGGER_SCAN: 135,
|
||||||
LOG_STREAMING_START: 136,
|
|
||||||
LOG_STREAMING_STOP: 137
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const HEARTBEAT_TIMEOUT = 3000;
|
const HEARTBEAT_TIMEOUT = 3000;
|
||||||
const PROVISIONING_IP = "192.168.42.1";
|
const PROVISIONING_IP = "192.168.42.1";
|
||||||
|
|
||||||
export default class SwimTrackerWebsocketConnection {
|
export default class SwimTrackerWebsocketConnection {
|
||||||
/**
|
constructor(swimTrackerHost, onData, onStarted, onStopped, onWifiStateInfo, onConnect, onDisconnect) {
|
||||||
* Creates a new persistent websocket connection to a swimtracker device
|
|
||||||
*
|
|
||||||
* @param {string} swimTrackerHost hostname or ip of the swimtracker device
|
|
||||||
* @param {(data: Uint16Array) => any} onData called whenever new measurement data is available
|
|
||||||
* @param {(sessionId: number) => any} onStarted called when a new measurement session was started
|
|
||||||
* @param {() => any} onStopped called when session was stopped
|
|
||||||
* @param {(wifistate : object) => any} onWifiStateInfo wifi state contains "state" (STATION_MODE|AP_PROVISIONING|AP_SECURE) and "hostname"
|
|
||||||
* @param {() => any} onConnect called when websocket connection was established
|
|
||||||
* @param {() => any} onDisconnect called when websocket disconnected
|
|
||||||
*/
|
|
||||||
constructor(swimTrackerHost, onData, onStarted, onStopped, onWifiStateInfo, onConnect, onDisconnect, reconnectingWsOptions={}) {
|
|
||||||
this.swimTrackerHost = swimTrackerHost;
|
this.swimTrackerHost = swimTrackerHost;
|
||||||
|
|
||||||
this.onData = onData;
|
this.onData = onData;
|
||||||
|
@ -54,14 +40,14 @@ export default class SwimTrackerWebsocketConnection {
|
||||||
this.onWifiStateInfo = onWifiStateInfo;
|
this.onWifiStateInfo = onWifiStateInfo;
|
||||||
this.onConnect = onConnect;
|
this.onConnect = onConnect;
|
||||||
this.onDisconnect = onDisconnect;
|
this.onDisconnect = onDisconnect;
|
||||||
this.onLogMessage = () => {};
|
|
||||||
|
|
||||||
// try configured URL and provisioning URL
|
// try configured URL and provisioning URL
|
||||||
const urls = [`ws://${swimTrackerHost}:81`, `ws://${PROVISIONING_IP}:81`];
|
const urls = [`ws://${swimTrackerHost}:81`, `ws://${PROVISIONING_IP}:81`];
|
||||||
let urlIndex = 0;
|
let urlIndex = 0;
|
||||||
const urlProvider = () => urls[urlIndex++ % urls.length]; // round robin url provider
|
const urlProvider = () => urls[urlIndex++ % urls.length]; // round robin url provider
|
||||||
|
|
||||||
this.ws = new ReconnectingWebSocket(urlProvider, [], { ...reconnectingWsOptions, maxReconnectionDelay: 3000 });
|
this.ws = new ReconnectingWebSocket(urlProvider, [], { maxReconnectionDelay: 3000 });
|
||||||
this.ws.onmessage = this._onMessage;
|
this.ws.onmessage = this._onMessage;
|
||||||
this.ws.onopen = this.onConnect;
|
this.ws.onopen = this.onConnect;
|
||||||
this.ws.onclose = this.onDisconnect;
|
this.ws.onclose = this.onDisconnect;
|
||||||
|
@ -112,14 +98,6 @@ export default class SwimTrackerWebsocketConnection {
|
||||||
this._sendMsg(OpCodes.TARE);
|
this._sendMsg(OpCodes.TARE);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendLogStreamStartCommand = () => {
|
|
||||||
this._sendMsg(OpCodes.LOG_STREAMING_START);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendLogStreamStopCommand = () => {
|
|
||||||
this._sendMsg(OpCodes.LOG_STREAMING_STOP);
|
|
||||||
}
|
|
||||||
|
|
||||||
scanWifiNetworks() {
|
scanWifiNetworks() {
|
||||||
console.log("Trigger wifi scan");
|
console.log("Trigger wifi scan");
|
||||||
this._sendMsg(OpCodes.WIFI_TRIGGER_SCAN);
|
this._sendMsg(OpCodes.WIFI_TRIGGER_SCAN);
|
||||||
|
@ -199,15 +177,12 @@ export default class SwimTrackerWebsocketConnection {
|
||||||
} else if (opCode === OpCodes.WIFI_STATE_RESPONSE) {
|
} else if (opCode === OpCodes.WIFI_STATE_RESPONSE) {
|
||||||
const wifiInfo = msgpack.decode(payload, { codec: this.msgpackCodec });
|
const wifiInfo = msgpack.decode(payload, { codec: this.msgpackCodec });
|
||||||
this.onWifiStateInfo(wifiInfo);
|
this.onWifiStateInfo(wifiInfo);
|
||||||
} else if (opCode === OpCodes.LOG_UPDATE) {
|
|
||||||
const logMsg = msgpack.decode(payload, { codec: this.msgpackCodec });
|
|
||||||
this.onLogMessage(logMsg);
|
|
||||||
} else if (opCode === OpCodes.APP_LAYER_PING) {
|
} else if (opCode === OpCodes.APP_LAYER_PING) {
|
||||||
//console.log("got heartbeat");
|
//console.log("got heartbeat");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onError = (ev) => {
|
_onError = (ev) => {
|
||||||
console.log("Websocket error", ev, ev.error, ev.message);
|
console.log("Websocket error", ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -9,12 +9,13 @@ import tempfile
|
||||||
|
|
||||||
# --------------------------------------------- Config ----------------------------------------------------------
|
# --------------------------------------------- Config ----------------------------------------------------------
|
||||||
|
|
||||||
NODE_PATH = "/home/martin/node-v18.18.0-linux-x64/bin"
|
NODE_PATH = "/home/martin/node-v14.17.0-linux-x64/bin"
|
||||||
JDK8_PATH = "/usr/lib/jvm/java-11-openjdk-amd64/bin/"
|
JDK8_PATH = "/usr/lib/jvm/java-8-openjdk-amd64/bin/"
|
||||||
|
|
||||||
PUBLIC_URL = "https://swimtracker.bauer.tech"
|
PUBLIC_URL = "https://swimtracker.bauer.tech"
|
||||||
DEPLOY_HOST = "server"
|
DEPLOY_HOST = "server"
|
||||||
DEPLOY_PATH = "/docker/web/volumes/static-sites/swimtracker"
|
DEPLOY_PATH = "/volumes/swimtracker"
|
||||||
|
|
||||||
|
|
||||||
APP_UPDATE_FOLDER = "app-update"
|
APP_UPDATE_FOLDER = "app-update"
|
||||||
APP_FOLDER = "app"
|
APP_FOLDER = "app"
|
||||||
|
@ -90,7 +91,8 @@ def deploy_apk_update_files():
|
||||||
def deploy_apk():
|
def deploy_apk():
|
||||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
apk_file = os.path.join(tmpdirname, "app.apk")
|
apk_file = os.path.join(tmpdirname, "app.apk")
|
||||||
cmd = ["eas", "build", "--platform", "android", "--local", "--profile", "production", "--output", "app.apk"]
|
cmd = ["turtle", "build:android", "--type", "apk", "--public-url", android_idx_url,
|
||||||
|
"-o", apk_file]
|
||||||
subprocess.check_call(cmd, env=env_with_node_path)
|
subprocess.check_call(cmd, env=env_with_node_path)
|
||||||
target_file = os.path.join(DEPLOY_PATH, APP_FOLDER, f"swimtracker-{version_name}.apk")
|
target_file = os.path.join(DEPLOY_PATH, APP_FOLDER, f"swimtracker-{version_name}.apk")
|
||||||
scp.put(apk_file, target_file)
|
scp.put(apk_file, target_file)
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
swimtracker-4525a0 Garten
|
|
||||||
swimtracker-452700 Testgerat
|
|
|
@ -1,68 +0,0 @@
|
||||||
# Environment Setup
|
|
||||||
|
|
||||||
as of 2023-09
|
|
||||||
|
|
||||||
|
|
||||||
1. nodejs
|
|
||||||
---------
|
|
||||||
|
|
||||||
- Downloaded version `node-v18.18.0-linux-x64` and put the `node-v18.18.0-linux-x64/bin` into the path via `~/.config/fish/config.fish`
|
|
||||||
- Installed yarn as package manager (Source `https://yarnpkg.com/getting-started/install`)
|
|
||||||
- `corepack enable`
|
|
||||||
|
|
||||||
2. expo
|
|
||||||
-------
|
|
||||||
|
|
||||||
(Source `https://reactnative.dev/docs/environment-setup`)
|
|
||||||
|
|
||||||
- `yarn create expo-app SwimTracker`
|
|
||||||
|
|
||||||
- Starting with `yarn start -w` doesn't work and suggests runnig
|
|
||||||
`npx expo install react-native-web react-dom @expo/webpack-config`
|
|
||||||
|
|
||||||
|
|
||||||
3. Run
|
|
||||||
------
|
|
||||||
npx expo start -w
|
|
||||||
|
|
||||||
|
|
||||||
4. Install Dependencies
|
|
||||||
-----------------------
|
|
||||||
use `npx expo install` instead of `yarn add` !
|
|
||||||
|
|
||||||
|
|
||||||
# Redux stuff
|
|
||||||
- react-redux
|
|
||||||
- redux-persist
|
|
||||||
- @react-native-async-storage/async-storage
|
|
||||||
|
|
||||||
# Translation
|
|
||||||
i18n-js
|
|
||||||
expo-localization
|
|
||||||
|
|
||||||
|
|
||||||
react-native-gesture-handler
|
|
||||||
react-native-reanimated
|
|
||||||
expo-app-loading # TODO deprecated
|
|
||||||
@react-navigation/native
|
|
||||||
@react-navigation/stack
|
|
||||||
react-native-safe-area-context # to not cover status bar etc
|
|
||||||
react-native-screens # not used directly, but somehow required
|
|
||||||
|
|
||||||
# Data handling
|
|
||||||
msgpack-lite
|
|
||||||
immutable
|
|
||||||
reconnecting-websocket
|
|
||||||
|
|
||||||
# Webdav reponse parsing
|
|
||||||
react-xml-parser
|
|
||||||
|
|
||||||
|
|
||||||
# Graph
|
|
||||||
react-native-svg
|
|
||||||
|
|
||||||
# Time since
|
|
||||||
|
|
||||||
|
|
||||||
# to be replaced
|
|
||||||
react-native-swipe-list-view
|
|
|
@ -8,23 +8,8 @@ npm -g install turtle-cli --legacy-peer-deps
|
||||||
expo export --public-url https://swimtracker.bauer.tech/app-update
|
expo export --public-url https://swimtracker.bauer.tech/app-update
|
||||||
scp -r dist/* root@server:"/volumes/swimtracker/app-update/"
|
scp -r dist/* root@server:"/volumes/swimtracker/app-update/"
|
||||||
|
|
||||||
|
|
||||||
# Needs jdk 8
|
# Needs jdk 8
|
||||||
apt install openjdk-8-jdk-headless
|
apt install openjdk-8-jdk-headless
|
||||||
export PATH=/usr/lib/jvm/java-8-openjdk-amd64/bin/:$PATH
|
export PATH=/usr/lib/jvm/java-8-openjdk-amd64/bin/:$PATH
|
||||||
turtle build:android --type apk --public-url https://swimtracker.bauer.tech/app-update/android-index.json -o app.apk
|
turtle build:android --type apk --public-url https://swimtracker.bauer.tech/app-update/android-index.json -o app.apk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# New
|
|
||||||
|
|
||||||
|
|
||||||
npm install -g eas-cli
|
|
||||||
|
|
||||||
|
|
||||||
# check eas.json from profiles
|
|
||||||
eas build --platform android --local --profile production
|
|
||||||
|
|
||||||
eas build --platform android --local --profile preview --output swimtracker-0.1.apk
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export default {
|
||||||
|
connecting: "Verbindung aufbauen",
|
||||||
|
connectSubtext: "Gehe entweder in das WLAN deines SwimTrackers oder in dein eigenes WLAN, falls du den SwimTracker schon eingerichtet hast.",
|
||||||
|
simpleMode: "Weniger Einstellungen",
|
||||||
|
advancedMode: "Mehr Einstellungen",
|
||||||
|
settings: "Einstellungen",
|
||||||
|
help: "Hilfe",
|
||||||
|
lastSessions: "Letzte Sessions",
|
||||||
|
mainMenu_social: "Freunde",
|
||||||
|
mainMenu_swimNow: "Jetzt schwimmen",
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export default {
|
||||||
|
connecting: "Connecting",
|
||||||
|
connectSubtext: "Please connect your phone to the WiFi of your SwimTracker",
|
||||||
|
simpleMode: "Simple Mode",
|
||||||
|
advancedMode: "Advanced Mode",
|
||||||
|
help: "Need help?",
|
||||||
|
settings: "Settings",
|
||||||
|
lastSessions: "Last Sessions",
|
||||||
|
mainMenu_social: "Social",
|
||||||
|
mainMenu_swimNow: "Swim now"
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"main": "node_modules/expo/AppEntry.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start",
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"web": "expo start --web",
|
||||||
|
"eject": "expo eject",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "jest-expo",
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||||
|
"@react-native-community/masked-view": "0.1.10",
|
||||||
|
"@react-navigation/native": "^5.4.2",
|
||||||
|
"@react-navigation/stack": "^5.3.9",
|
||||||
|
"expo": "^41.0.0",
|
||||||
|
"expo-app-loading": "^1.0.3",
|
||||||
|
"expo-blur": "~9.0.3",
|
||||||
|
"expo-keep-awake": "~9.1.2",
|
||||||
|
"expo-linear-gradient": "~9.1.0",
|
||||||
|
"expo-localization": "~10.1.0",
|
||||||
|
"i18n-js": "^3.8.0",
|
||||||
|
"immutable": "^4.0.0-rc.12",
|
||||||
|
"moment": "^2.27.0",
|
||||||
|
"msgpack-lite": "^0.1.26",
|
||||||
|
"msgpack5": "^4.2.1",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react": "16.13.1",
|
||||||
|
"react-dom": "16.13.1",
|
||||||
|
"react-native": "0.63.4",
|
||||||
|
"react-native-chart-kit": "^3.13.0",
|
||||||
|
"react-native-gesture-handler": "~1.10.2",
|
||||||
|
"react-native-reanimated": "~2.1.0",
|
||||||
|
"react-native-safe-area-context": "3.2.0",
|
||||||
|
"react-native-screens": "~3.0.0",
|
||||||
|
"react-native-svg": "12.1.0",
|
||||||
|
"react-native-svg-web": "^1.0.7",
|
||||||
|
"react-native-swipe-list-view": "^3.2.3",
|
||||||
|
"react-native-unimodules": "~0.13.3",
|
||||||
|
"react-native-web": "~0.13.12",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
|
"react-xml-parser": "^1.1.6",
|
||||||
|
"reconnecting-websocket": "^4.4.0",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-persist": "^6.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-preset-expo": "8.3.0",
|
||||||
|
"jest-expo": "^41.0.0",
|
||||||
|
"react-test-renderer": "^16.13.1"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
|
@ -1,88 +0,0 @@
|
||||||
import React, { Component } from 'react'
|
|
||||||
import { View, PanResponder, GestureResponderEvent } from 'react-native'
|
|
||||||
import Svg, {
|
|
||||||
Circle,
|
|
||||||
Ellipse,
|
|
||||||
G,
|
|
||||||
LinearGradient,
|
|
||||||
RadialGradient,
|
|
||||||
Line,
|
|
||||||
Path,
|
|
||||||
Polygon,
|
|
||||||
Polyline,
|
|
||||||
Rect,
|
|
||||||
Symbol,
|
|
||||||
Use,
|
|
||||||
Defs,
|
|
||||||
Stop
|
|
||||||
} from 'react-native-svg';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default class Foo extends Component {
|
|
||||||
|
|
||||||
panResponder = null;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = { x: 200, y: 200, initX: 0, initY: 0 }
|
|
||||||
|
|
||||||
this.panResponder = PanResponder.create({
|
|
||||||
onStartShouldSetPanResponder: (evt, gestureState) => true,
|
|
||||||
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
|
|
||||||
onMoveShouldSetPanResponder: (evt, gestureState) => true,
|
|
||||||
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
|
|
||||||
onPanResponderGrant: () => {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
onPanResponderStart: (evt, gestureState) => {
|
|
||||||
console.log("start", gestureState);
|
|
||||||
this.setState({ initX: this.state.x, initY: this.state.y });
|
|
||||||
},
|
|
||||||
|
|
||||||
onPanResponderMove: (evt, gs) => {
|
|
||||||
//console.log(gs.dx + ' ' + gs.dy)
|
|
||||||
|
|
||||||
const newX = this.state.initX + gs.dx;
|
|
||||||
const newY = this.state.initY + gs.dy;
|
|
||||||
|
|
||||||
this.setState({ x: newX, y: newY });
|
|
||||||
},
|
|
||||||
onPanResponderTerminationRequest: (evt, gestureState) => true,
|
|
||||||
|
|
||||||
onPanResponderRelease: (evt, gs) => {
|
|
||||||
console.log('Release ' + gs.dx + ' ' + gs.dy);
|
|
||||||
//this.setState({ x: this.state.x, y: 0 });
|
|
||||||
},
|
|
||||||
onShouldBlockNativeResponder: (evt, gestureState) => {
|
|
||||||
// Returns whether this component should block native components from becoming
|
|
||||||
// the JS responder. Returns true by default. Is currently only supported on
|
|
||||||
// android.
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Svg height="500" width="500">
|
|
||||||
<Circle
|
|
||||||
{...this.panResponder.panHandlers}
|
|
||||||
x={this.state.x}
|
|
||||||
y={this.state.y}
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
r="20"
|
|
||||||
stroke="blue"
|
|
||||||
strokeWidth="3.5"
|
|
||||||
fill="white" />
|
|
||||||
</Svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
import React, { useRef } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
StyleSheet,
|
|
||||||
View,
|
|
||||||
StatusBar,
|
|
||||||
ImageBackground,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
Animated,
|
|
||||||
PanResponder,
|
|
||||||
} from "react-native";
|
|
||||||
import EntypoIcon from "react-native-vector-icons/Entypo";
|
|
||||||
import Svg, { G, Polyline, Line, Circle, Rect, Text as SvgText } from 'react-native-svg';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
||||||
let AnimatedG = Animated.createAnimatedComponent(G);
|
|
||||||
|
|
||||||
function SliderDraftView(props) {
|
|
||||||
const pan = useRef(new Animated.ValueXY()).current;
|
|
||||||
|
|
||||||
/*
|
|
||||||
const panResponder = useRef(
|
|
||||||
PanResponder.create({
|
|
||||||
onPanResponderTerminationRequest: () => { console.log("p1"); return false; },
|
|
||||||
onStartShouldSetPanResponder: () => { console.log("p2"); return true; },
|
|
||||||
onMoveShouldSetPanResponder: () => { console.log("p3"); return true; },
|
|
||||||
//onPanResponderMove: Animated.event([
|
|
||||||
// null,
|
|
||||||
// { dx: pan.x, dy: pan.y }
|
|
||||||
//]),
|
|
||||||
onPanResponderMove: (e, gesture) => {
|
|
||||||
console.log(gesture);
|
|
||||||
},
|
|
||||||
onPanResponderRelease: () => {
|
|
||||||
console.log("release");
|
|
||||||
Animated.spring(pan, { toValue: { x: 0, y: 0 } }).start();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);//.current;
|
|
||||||
*/
|
|
||||||
const panResponder = useRef(PanResponder.create({
|
|
||||||
onStartShouldSetPanResponder: () => true,
|
|
||||||
onMoveShouldSetPanResponderCapture: () => true,
|
|
||||||
onPanResponderMove: (_, { dx, dy }) => {
|
|
||||||
console.log("bla", dx, dy);
|
|
||||||
cx.setValue(dx);
|
|
||||||
cy.setValue(dy);
|
|
||||||
setCurrentPoint({ x: dx, y: dy });
|
|
||||||
},
|
|
||||||
onPanResponderRelease: (e, { dx, dy }) => {
|
|
||||||
console.log("release", dx, dy);
|
|
||||||
cx.extractOffset();
|
|
||||||
cy.extractOffset();
|
|
||||||
offsetX = offsetX + dx;
|
|
||||||
offsetY = offsetY + dy;
|
|
||||||
}
|
|
||||||
})).current;
|
|
||||||
|
|
||||||
|
|
||||||
console.log({ ...panResponder.panHandlers });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
|
|
||||||
<StatusBar barStyle="light-content" backgroundColor="rgba(0,0,0,0.4)" translucent={true} />
|
|
||||||
<ImageBackground
|
|
||||||
source={require("../assets/pool_sky_background_blurred.jpg")}
|
|
||||||
resizeMode="cover"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<View style={setupViewStyles.container}>
|
|
||||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
|
||||||
{props.backButton &&
|
|
||||||
<TouchableOpacity onPress={() => props.navigation.goBack()}>
|
|
||||||
<EntypoIcon name="chevron-left" style={setupViewStyles.backButton}></EntypoIcon>
|
|
||||||
</TouchableOpacity>
|
|
||||||
}
|
|
||||||
<Text style={setupViewStyles.headerText}>
|
|
||||||
Slider Draft
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
|
||||||
<Svg height="100%" width="100%" viewBox="0 0 200 200" {...panResponder.panHandler}>
|
|
||||||
<Rect x="10" y="10" rx="15" width="160" height="40" fill="rgba(255, 255, 255, 0.7)" />
|
|
||||||
<SvgText x="85" y="34" fill="rgba(80, 80, 80, 1)">42</SvgText>
|
|
||||||
|
|
||||||
|
|
||||||
<AnimatedCircle
|
|
||||||
x={pan.x} y={pan.y} r="20"
|
|
||||||
></AnimatedCircle>
|
|
||||||
|
|
||||||
<G >
|
|
||||||
<Line x1="25" y1="20" x2="25" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
|
|
||||||
<Line x1="45" y1="20" x2="45" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
|
|
||||||
<Line x1="65" y1="20" x2="65" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
|
|
||||||
<Line x1="85" y1="20" x2="85" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
|
|
||||||
<Line x1="105" y1="20" x2="105" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
|
|
||||||
<Line x1="125" y1="20" x2="125" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
|
|
||||||
<Line x1="145" y1="20" x2="145" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
|
|
||||||
</G>
|
|
||||||
</Svg>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</ImageBackground>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const setupViewStyles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
width: "80%",
|
|
||||||
marginLeft: 40,
|
|
||||||
marginTop: 60,
|
|
||||||
},
|
|
||||||
headerText: {
|
|
||||||
color: "rgba(255,255,255,1)",
|
|
||||||
fontSize: 25,
|
|
||||||
},
|
|
||||||
subtext: {
|
|
||||||
color: "rgba(255,255,255,1)",
|
|
||||||
textAlign: "left",
|
|
||||||
fontSize: 18,
|
|
||||||
lineHeight: 25,
|
|
||||||
width: "80%",
|
|
||||||
marginBottom: 50,
|
|
||||||
},
|
|
||||||
backButton: {
|
|
||||||
color: "rgba(255,255,255,1)",
|
|
||||||
fontSize: 40
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default SliderDraftView;
|
|
|
@ -1,22 +0,0 @@
|
||||||
# %%
|
|
||||||
import msgpack
|
|
||||||
|
|
||||||
#data = [130, 164, 116, 105, 109, 101, 206, 0, 7, 200, 131, 163, 109, 115, 103, 217, 56, 110, 101, 119, 32, 119, 101, 98, 115, 111, 99, 107, 101, 116, 32, 99, 111, 110, 110, 101, 99, 116, 105, 111, 110, 44, 32, 115, 116, 111, 114, 105, 110, 103, 32, 97, 116, 32, 112, 111, 115, 32, 49, 32, 45, 32, 111, 99, 99, 117, 112, 97, 110, 99, 121, 58]
|
|
||||||
#data = [130, 164, 116, 105, 109, 101, 206, 0, 4, 35, 94, 163, 109, 115, 103, 217, 55, 110, 101, 119, 32, 119, 101, 98, 115, 111, 99, 107, 101, 116, 32, 99, 111, 110, 110, 101, 99, 116, 105, 111, 110, 44, 32, 115, 116, 111, 114, 105, 110, 103, 32, 97, 116, 32, 112, 111, 115, 32, 49, 32, 45, 32, 111, 99, 99, 117, 112, 97, 110, 99, 121]
|
|
||||||
data = [130, 164, 116, 105, 109, 101, 205, 14, 216, 163, 109, 115, 103, 217, 37, 83, 112, 105, 102, 102, 115, 32, 115, 105, 122, 101, 58, 32, 49, 50, 32, 77, 66, 44, 32, 115, 101, 116, 117, 112, 32, 116, 105, 109, 101, 32, 49, 32, 115, 101, 99]
|
|
||||||
byte_data = b''
|
|
||||||
for e in data:
|
|
||||||
byte_data += e.to_bytes(1, "big")
|
|
||||||
|
|
||||||
#print("length", len(byte_data))
|
|
||||||
#print(byte_data.decode(errors="ignore"))
|
|
||||||
#print(msgpack.unpackb(byte_data))
|
|
||||||
|
|
||||||
# %%
|
|
||||||
for i, e in enumerate(data):
|
|
||||||
print(i, ":", e, bin(e), chr(e))
|
|
||||||
# %%
|
|
||||||
#last_msg = data[17:]
|
|
||||||
#print("len last msg", len(last_msg))
|
|
||||||
|
|
||||||
# %%
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
import moment from 'moment/min/moment-with-locales';
|
||||||
|
|
||||||
|
|
||||||
|
function timeSince(timeStamp, lang = 'de') {
|
||||||
|
moment.locale(lang);
|
||||||
|
|
||||||
|
const now = Math.floor((new Date()).getTime() / 1000);
|
||||||
|
const secondsPast = now - timeStamp;
|
||||||
|
if (secondsPast <= 6 * 3600) {
|
||||||
|
return moment().seconds(-secondsPast).fromNow();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
const timeStampDate = new Date(timeStamp * 1000);
|
||||||
|
const dateNow = new Date();
|
||||||
|
|
||||||
|
const timeStampMoment = moment.unix(timeStamp);
|
||||||
|
|
||||||
|
let dateStr = "";
|
||||||
|
|
||||||
|
if (timeStampDate.getDate() == dateNow.getDate())
|
||||||
|
dateStr = "Heute, " + timeStampMoment.format("HH:mm");
|
||||||
|
else if (timeStampDate.getDate() + 1 == dateNow.getDate())
|
||||||
|
dateStr = "Gestern, " + timeStampMoment.format("HH:mm");
|
||||||
|
else {
|
||||||
|
dateStr = timeStampMoment.format("ddd, DD.MM.YY um HH:mm");
|
||||||
|
}
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const toTimeStr = seconds => {
|
||||||
|
let minuteStr = String(Math.floor(seconds / 60));
|
||||||
|
if (minuteStr.length < 2)
|
||||||
|
minuteStr = "0" + minuteStr;
|
||||||
|
let secondStr = String(Math.floor(seconds % 60));
|
||||||
|
if (secondStr.length < 2)
|
||||||
|
secondStr = "0" + secondStr;
|
||||||
|
return minuteStr + ":" + secondStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export { toTimeStr, timeSince };
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import SetupView from '../components/SetupView';
|
import SetupView from '../components/SetupView';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { changeSwimTrackerHostname } from '../state/Reducer';
|
import { changeSwimTrackerHostname } from '../state/Reducer';
|
||||||
import { i18n } from '../utility/i18n';
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
const validHostnameRegex = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))|(^\s*((?=.{1,255}$)(?=.*[A-Za-z].*)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*)\s*$)/;
|
const validHostnameRegex = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))|(^\s*((?=.{1,255}$)(?=.*[A-Za-z].*)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*)\s*$)/;
|
||||||
|
|
|
@ -20,7 +20,7 @@ import DataAnalysis from '../data_processing/DataAnalysis';
|
||||||
import * as msgpack from 'msgpack-lite';
|
import * as msgpack from 'msgpack-lite';
|
||||||
import { timeSince } from '../utility/TimeUtils';
|
import { timeSince } from '../utility/TimeUtils';
|
||||||
import XMLParser from 'react-xml-parser';
|
import XMLParser from 'react-xml-parser';
|
||||||
import {i18n} from '../utility/i18n';
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
|
|
||||||
function SessionCard(props) {
|
function SessionCard(props) {
|
||||||
|
@ -55,7 +55,7 @@ function SessionCardBehindSwipe(props) {
|
||||||
style={sessionCardStyles.deleteButton}
|
style={sessionCardStyles.deleteButton}
|
||||||
onPress={props.onDelete}
|
onPress={props.onDelete}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: 18, color: "white" }}>{i18n.t('delete_session_button')}</Text>
|
<Text style={{ fontSize: 18, color: "white" }}>Löschen</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -137,6 +137,9 @@ function parsePropfind(text) {
|
||||||
const parser = new XMLParser();
|
const parser = new XMLParser();
|
||||||
const xmlDoc = parser.parseFromString(text);
|
const xmlDoc = parser.parseFromString(text);
|
||||||
|
|
||||||
|
//const parser = new DOMParser();
|
||||||
|
//const xmlDoc = parser.parseFromString(text, "text/xml");
|
||||||
|
|
||||||
const responses = xmlDoc.getElementsByTagName("D:response");
|
const responses = xmlDoc.getElementsByTagName("D:response");
|
||||||
let result = [];
|
let result = [];
|
||||||
for (let i = 0; i < responses.length; ++i) {
|
for (let i = 0; i < responses.length; ++i) {
|
||||||
|
@ -180,8 +183,7 @@ async function getFullData(swimTrackerHost, analysisSettings) {
|
||||||
const da = new DataAnalysis();
|
const da = new DataAnalysis();
|
||||||
e.analysis = da.analyze(analysisSettings, e.startTime, e.values);
|
e.analysis = da.analyze(analysisSettings, e.startTime, e.values);
|
||||||
}
|
}
|
||||||
//console.log("full data", parsed);
|
return parsed;
|
||||||
return parsed.reverse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
@ -201,11 +203,9 @@ class LastSessionsView extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const deleteSession = async sessionFileName => {
|
const deleteSession = async sessionFileName => {
|
||||||
const filteredSession = this.state.sessions.filter((element) => element.name != sessionFileName);
|
this.setState({ sessions: null });
|
||||||
this.setState({ sessions: filteredSession });
|
await request({ url: "http://" + this.props.swimTrackerHost + "/webdav/" + sessionFileName, method: "DELETE" });
|
||||||
request({ url: "http://" + this.props.swimTrackerHost + "/webdav/" + sessionFileName, method: "DELETE" })
|
this.setState({ sessions: await getFullData(this.props.swimTrackerHost, this.props.analysisSettings) });
|
||||||
.then((value) => console.log("Successfully deleted", sessionFileName, value))
|
|
||||||
.catch((err) => console.error("Failed to delete", sessionFileName, err));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRefresh = async () => {
|
const onRefresh = async () => {
|
||||||
|
@ -221,13 +221,13 @@ class LastSessionsView extends React.Component {
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
keyExtractor={item => item.startTime.toString()}
|
keyExtractor={item => item.startTime.toString()}
|
||||||
disableRightSwipe={true}
|
disableRightSwipe={true}
|
||||||
data={this.state.sessions}
|
data={this.state.sessions.reverse()}
|
||||||
renderItem={(data, rowMap) => (
|
renderItem={(data, rowMap) => (
|
||||||
<SessionCard
|
<SessionCard
|
||||||
textFirstLine={timeSince(data.item.startTime)}
|
textFirstLine={timeSince(data.item.startTime)}
|
||||||
laps={(data.item.analysis.peaks.size / this.props.peaksPerLap).toFixed(1)}
|
laps={(data.item.analysis.peaks.size / this.props.peaksPerLap).toFixed(1)}
|
||||||
momentum={Math.trunc(data.item.analysis.totalMomentum * this.props.kgFactor / 10 / 60)}
|
momentum={Math.trunc(data.item.analysis.totalMomentum * this.props.kgFactor / 10 / 60)}
|
||||||
activeTime={Math.round(data.item.analysis.activeTime / 60, 0)} />
|
activeTime={data.item.analysis.activeTime} />
|
||||||
)}
|
)}
|
||||||
renderHiddenItem={(data, rowMap) => <SessionCardBehindSwipe onDelete={() => { deleteSession(data.item.name) }} />}
|
renderHiddenItem={(data, rowMap) => <SessionCardBehindSwipe onDelete={() => { deleteSession(data.item.name) }} />}
|
||||||
leftOpenValue={0}
|
leftOpenValue={0}
|
|
@ -13,7 +13,7 @@ import MaterialCommIcon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||||
|
|
||||||
import EntypoIcon from "react-native-vector-icons/Entypo";
|
import EntypoIcon from "react-native-vector-icons/Entypo";
|
||||||
|
|
||||||
import { i18n } from '../utility/i18n';
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
import { ConnState, startSession } from '../state/DeviceReduxCoupling';
|
import { ConnState, startSession } from '../state/DeviceReduxCoupling';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -113,7 +113,7 @@ const buttonGridStyles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
margin: 14,
|
margin: 20,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
width: 120,
|
width: 120,
|
||||||
height: 130,
|
height: 130,
|
||||||
|
@ -125,7 +125,7 @@ const buttonGridStyles = StyleSheet.create({
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: "rgba(255,255,255,1)",
|
color: "rgba(255,255,255,1)",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
color: "rgba(255,255,255,1)",
|
color: "rgba(255,255,255,1)",
|
||||||
|
@ -203,7 +203,7 @@ function MainMenuView(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onStartButtonPress = () => {
|
const onStartButtonPress = () => {
|
||||||
if (props.connState !== ConnState.CONNECTED_RUNNING) {
|
if (!props.connState !== ConnState.CONNECTED_RUNNING) {
|
||||||
props.dispatch(startSession());
|
props.dispatch(startSession());
|
||||||
}
|
}
|
||||||
props.navigation.navigate('Training')
|
props.navigation.navigate('Training')
|
|
@ -8,11 +8,13 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import themeColors from '../components/themeColors';
|
import themeColors from '../components/themeColors';
|
||||||
|
import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||||
|
import EntypoIcon from "react-native-vector-icons/Entypo";
|
||||||
import ImageHeader from "../components/ImageHeader";
|
import ImageHeader from "../components/ImageHeader";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { TouchableOpacity } from "react-native-gesture-handler";
|
import { TouchableOpacity } from "react-native-gesture-handler";
|
||||||
import request from '../utility/PromiseRequest';
|
import request from '../utility/PromiseRequest';
|
||||||
import { i18n } from '../utility/i18n';
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
@ -191,7 +193,6 @@ async function queryDeviceFirmwareVersion(swimTrackerHost) {
|
||||||
async function queryNewestFirmwareVersion() {
|
async function queryNewestFirmwareVersion() {
|
||||||
const QUERY_URL = "https://swimtracker-update.bauer.tech/VERSION";
|
const QUERY_URL = "https://swimtracker-update.bauer.tech/VERSION";
|
||||||
const result = await request({ url: QUERY_URL, responseType: "text" });
|
const result = await request({ url: QUERY_URL, responseType: "text" });
|
||||||
console.log("newest firmware version, got", result);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|