New setup from scratch - all modules updated - app now in subfolder
|  | @ -1 +0,0 @@ | |||
| {} | ||||
|  | @ -1,14 +0,0 @@ | |||
| node_modules/**/* | ||||
| .expo/* | ||||
| npm-debug.* | ||||
| *.jks | ||||
| *.p8 | ||||
| *.p12 | ||||
| *.key | ||||
| *.mobileprovision | ||||
| *.orig.* | ||||
| web-build/ | ||||
| web-report/ | ||||
| /dist | ||||
| venv | ||||
| __pycache__ | ||||
|  | @ -1 +0,0 @@ | |||
| {} | ||||
|  | @ -0,0 +1,3 @@ | |||
| /node_modules | ||||
| /.expo | ||||
| .directory | ||||
|  | @ -13,12 +13,6 @@ import { persistStore, persistReducer } from 'redux-persist' | |||
| import hardSet from 'redux-persist/lib/stateReconciler/hardSet' | ||||
| 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
 | ||||
| import { NavigationContainer } from '@react-navigation/native'; | ||||
| import { createStackNavigator } from '@react-navigation/stack'; | ||||
|  | @ -32,16 +26,6 @@ import ConnectingView from './views/ConnectingView'; | |||
| import WifiSelectionView from './views/WifiSelectionView'; | ||||
| 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 = { | ||||
|     key: 'root', | ||||
|     storage: AsyncStorage, | ||||
|  | @ -0,0 +1,33 @@ | |||
| { | ||||
|   "expo": { | ||||
|     "name": "SwimTracker", | ||||
|     "slug": "SwimTracker", | ||||
|     "version": "1.0.0", | ||||
|     "orientation": "portrait", | ||||
|     "icon": "./assets/icon.png", | ||||
|     "userInterfaceStyle": "light", | ||||
|     "splash": { | ||||
|       "image": "./assets/splash.png", | ||||
|       "resizeMode": "contain", | ||||
|       "backgroundColor": "#ffffff" | ||||
|     }, | ||||
|     "assetBundlePatterns": [ | ||||
|       "**/*" | ||||
|     ], | ||||
|     "ios": { | ||||
|       "supportsTablet": true | ||||
|     }, | ||||
|     "android": { | ||||
|       "adaptiveIcon": { | ||||
|         "foregroundImage": "./assets/adaptive-icon.png", | ||||
|         "backgroundColor": "#ffffff" | ||||
|       } | ||||
|     }, | ||||
|     "web": { | ||||
|       "favicon": "./assets/favicon.png" | ||||
|     }, | ||||
|     "plugins": [ | ||||
|       "expo-localization" | ||||
|     ] | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,17 @@ | |||
| > 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. | ||||
| After Width: | Height: | Size: 17 KiB | 
| Before Width: | Height: | Size: 419 KiB After Width: | Height: | Size: 419 KiB | 
| After Width: | Height: | Size: 1.4 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,109 @@ | |||
| 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 | ||||
| */ | ||||
|  | @ -15,6 +15,7 @@ const OpCodes = { | |||
|     WIFI_STATE_RESPONSE: 8, | ||||
|     WIFI_SCAN_RESPONSE: 9, | ||||
|     APP_LAYER_PING: 10, | ||||
|     LOG_UPDATE: 11, | ||||
| 
 | ||||
|     // from frontend to device
 | ||||
|     START_SESSION: 128, | ||||
|  | @ -25,13 +26,26 @@ const OpCodes = { | |||
|     WIFI_STATE_SET: 133, | ||||
|     WIFI_STATE_GET: 134, | ||||
|     WIFI_TRIGGER_SCAN: 135, | ||||
|     LOG_STREAMING_START: 136, | ||||
|     LOG_STREAMING_STOP: 137 | ||||
| }; | ||||
| 
 | ||||
| const HEARTBEAT_TIMEOUT = 3000; | ||||
| const PROVISIONING_IP = "192.168.42.1"; | ||||
| 
 | ||||
| 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.onData = onData; | ||||
|  | @ -40,14 +54,14 @@ export default class SwimTrackerWebsocketConnection { | |||
|         this.onWifiStateInfo = onWifiStateInfo; | ||||
|         this.onConnect = onConnect; | ||||
|         this.onDisconnect = onDisconnect; | ||||
| 
 | ||||
|         this.onLogMessage = () => {}; | ||||
|          | ||||
|         // try configured URL and provisioning URL
 | ||||
|         const urls = [`ws://${swimTrackerHost}:81`, `ws://${PROVISIONING_IP}:81`]; | ||||
|         let urlIndex = 0; | ||||
|         const urlProvider = () => urls[urlIndex++ % urls.length]; // round robin url provider
 | ||||
|          | ||||
|         this.ws = new ReconnectingWebSocket(urlProvider, [], { maxReconnectionDelay: 3000 }); | ||||
|         this.ws = new ReconnectingWebSocket(urlProvider, [], { ...reconnectingWsOptions,  maxReconnectionDelay: 3000 }); | ||||
|         this.ws.onmessage = this._onMessage; | ||||
|         this.ws.onopen = this.onConnect; | ||||
|         this.ws.onclose = this.onDisconnect; | ||||
|  | @ -98,6 +112,14 @@ export default class SwimTrackerWebsocketConnection { | |||
|         this._sendMsg(OpCodes.TARE); | ||||
|     } | ||||
| 
 | ||||
|     sendLogStreamStartCommand = () => { | ||||
|         this._sendMsg(OpCodes.LOG_STREAMING_START); | ||||
|     } | ||||
| 
 | ||||
|     sendLogStreamStopCommand = () => { | ||||
|         this._sendMsg(OpCodes.LOG_STREAMING_STOP); | ||||
|     } | ||||
| 
 | ||||
|     scanWifiNetworks() { | ||||
|         console.log("Trigger wifi scan"); | ||||
|         this._sendMsg(OpCodes.WIFI_TRIGGER_SCAN); | ||||
|  | @ -177,6 +199,9 @@ export default class SwimTrackerWebsocketConnection { | |||
|         } else if (opCode === OpCodes.WIFI_STATE_RESPONSE) { | ||||
|             const wifiInfo = msgpack.decode(payload, { codec: this.msgpackCodec }); | ||||
|             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) { | ||||
|             //console.log("got heartbeat");
 | ||||
|         } | ||||
|  | @ -0,0 +1,44 @@ | |||
| { | ||||
|   "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-app-loading": "^2.1.1", | ||||
|     "expo-localization": "~14.3.0", | ||||
|     "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.5", | ||||
|     "react-native-gesture-handler": "~2.12.0", | ||||
|     "react-native-reanimated": "~3.3.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" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "^7.20.0" | ||||
|   }, | ||||
|   "private": true | ||||
| } | ||||
|  | @ -0,0 +1,34 @@ | |||
| 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" | ||||
|   }, | ||||
|   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", | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export const i18n = new I18n(); | ||||
| i18n.store(translation_store); | ||||
| i18n.defaultLocale = "en"; | ||||
| i18n.enableFallback = true; | ||||
| i18n.locale = Localization.locale; | ||||
|  | @ -10,7 +10,7 @@ import { | |||
| import SetupView from '../components/SetupView'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { changeSwimTrackerHostname } from '../state/Reducer'; | ||||
| import i18n from 'i18n-js'; | ||||
| import { i18n } from '../utility/i18n'; | ||||
| 
 | ||||
| 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 { timeSince } from '../utility/TimeUtils'; | ||||
| import XMLParser from 'react-xml-parser'; | ||||
| import i18n from 'i18n-js'; | ||||
| import {i18n} from '../utility/i18n'; | ||||
| 
 | ||||
| 
 | ||||
| function SessionCard(props) { | ||||
|  | @ -13,7 +13,7 @@ import MaterialCommIcon from "react-native-vector-icons/MaterialCommunityIcons"; | |||
| 
 | ||||
| import EntypoIcon from "react-native-vector-icons/Entypo"; | ||||
| 
 | ||||
| import i18n from 'i18n-js'; | ||||
| import { i18n } from '../utility/i18n'; | ||||
| 
 | ||||
| import { ConnState, startSession } from '../state/DeviceReduxCoupling'; | ||||
| import { connect } from 'react-redux'; | ||||
|  | @ -8,13 +8,11 @@ import { | |||
|   Switch, | ||||
| } from "react-native"; | ||||
| 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 { connect } from 'react-redux'; | ||||
| import { TouchableOpacity } from "react-native-gesture-handler"; | ||||
| import request from '../utility/PromiseRequest'; | ||||
| import i18n from 'i18n-js'; | ||||
| import { i18n } from '../utility/i18n'; | ||||
| 
 | ||||
| 
 | ||||
| // ---------------------------------------------------------------------------------------------
 | ||||
							
								
								
									
										35
									
								
								app.json
								
								
								
								
							
							
						
						|  | @ -1,35 +0,0 @@ | |||
| { | ||||
|   "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" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -16,7 +16,6 @@ PUBLIC_URL = "https://swimtracker.bauer.tech" | |||
| DEPLOY_HOST = "server" | ||||
| DEPLOY_PATH = "/volumes/swimtracker" | ||||
| 
 | ||||
| 
 | ||||
| APP_UPDATE_FOLDER = "app-update" | ||||
| APP_FOLDER = "app" | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,58 @@ | |||
| # 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. 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 | ||||
| 
 | ||||
|  | @ -8,7 +8,6 @@ npm -g install turtle-cli --legacy-peer-deps | |||
| expo export --public-url https://swimtracker.bauer.tech/app-update | ||||
| scp -r dist/* root@server:"/volumes/swimtracker/app-update/" | ||||
| 
 | ||||
| 
 | ||||
| # Needs jdk 8 | ||||
| apt install openjdk-8-jdk-headless | ||||
| export PATH=/usr/lib/jvm/java-8-openjdk-amd64/bin/:$PATH | ||||
|  | @ -1,11 +0,0 @@ | |||
| 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", | ||||
| } | ||||
|  | @ -1,11 +0,0 @@ | |||
| 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" | ||||
| } | ||||
							
								
								
									
										58
									
								
								package.json
								
								
								
								
							
							
						
						|  | @ -1,58 +0,0 @@ | |||
| { | ||||
|   "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-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 | ||||
| } | ||||
|  | @ -0,0 +1,88 @@ | |||
| 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> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,139 @@ | |||
| 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; | ||||
|  | @ -0,0 +1,22 @@ | |||
| # %% | ||||
| 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)) | ||||
| 
 | ||||
| # %% | ||||