From a71b463677e69c3ef59baa5e669e797d38d6fe10 Mon Sep 17 00:00:00 2001 From: Trivikram Battalapalli <vikram.battalapalli@gmail.com> Date: Sat, 16 Nov 2024 23:28:53 -0600 Subject: [PATCH] added flask server for data processing --- client/api/api_client.js | 4 +- client/api/user_api.js | 4 +- client/package.json | 2 +- client/screens/WorkoutAnalysisScreen.js | 2 +- client/screens/WorkoutScreen.js | 190 +++++++++++------------- server/controllers/post.js | 23 ++- server/controllers/put.js | 16 +- server/data_processing.py | 118 +++++++++++++++ server/helpers/timeStreamController.js | 73 +++------ server/index.js | 15 -- server/models/IMUDataSchema.js | 2 +- server/package.json | 6 +- 12 files changed, 255 insertions(+), 200 deletions(-) create mode 100644 server/data_processing.py diff --git a/client/api/api_client.js b/client/api/api_client.js index 9ea8f79..d5c0867 100644 --- a/client/api/api_client.js +++ b/client/api/api_client.js @@ -1,7 +1,7 @@ import axios from 'axios'; -// const client = axios.create({ baseURL: 'http://3.139.131.0:5000/api/' }); +const client = axios.create({ baseURL: 'http://3.139.131.0:5000/api/' }); -const client = axios.create({ baseURL: 'http://10.194.199.185:3000/api/' }); +// const client = axios.create({ baseURL: 'http://10.194.199.185:3000/api/' }); export default client; diff --git a/client/api/user_api.js b/client/api/user_api.js index 9d16769..0129ddd 100644 --- a/client/api/user_api.js +++ b/client/api/user_api.js @@ -15,9 +15,9 @@ export const createUser = async (userData) => { export const loginUser = async (loginData) => { try { - console.log('sending'); + // console.log('sending'); const response = await client.post('/users/login', loginData); // Send email and password for login - console.log(response); + // console.log(response); return handleSuccess(response); } catch (error) { return handleError(error); diff --git a/client/package.json b/client/package.json index 210c63e..ba94341 100644 --- a/client/package.json +++ b/client/package.json @@ -19,7 +19,6 @@ "nativewind": "^4.0.36", "react": "18.3.1", "react-native": "0.76.1", - "react-native-bluetooth-classic": "^1.73.0-rc.12", "react-native-flash-message": "^0.4.2", "react-native-gesture-handler": "~2.20.2", "react-native-heroicons": "^4.0.0", @@ -29,6 +28,7 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.0.0", "react-native-svg": "^15.8.0", + "react-native-usb-serialport-for-android": "^0.5.0", "superagent": "^10.1.0", "victory-native": "^41.9.0" }, diff --git a/client/screens/WorkoutAnalysisScreen.js b/client/screens/WorkoutAnalysisScreen.js index 557bc0f..3d10413 100644 --- a/client/screens/WorkoutAnalysisScreen.js +++ b/client/screens/WorkoutAnalysisScreen.js @@ -17,7 +17,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import HeaderBar from '../components/HeaderBar'; import RepCard from '../components/RepCard'; -function WorkoutAnalysisScreen() { +function WorkoutAnalysisScreen({ navigation }) { const route = useRoute(); const { workoutId } = route.params; const [workoutData, setWorkoutData] = useState(null); diff --git a/client/screens/WorkoutScreen.js b/client/screens/WorkoutScreen.js index 2a5a4c7..fc098dd 100644 --- a/client/screens/WorkoutScreen.js +++ b/client/screens/WorkoutScreen.js @@ -1,9 +1,10 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { View, Text, TouchableOpacity, Alert } from 'react-native'; import { Picker } from '@react-native-picker/picker'; -import RNBluetoothClassic, { - BluetoothDevice, -} from 'react-native-bluetooth-classic'; +import { + UsbSerialManager, + Parity, +} from 'react-native-usb-serialport-for-android'; import { createWorkout, completeWorkout } from '../api/workout_api'; import { createRep } from '../api/rep_api'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -14,8 +15,6 @@ function WorkoutScreen({ navigation }) { const [repInProgress, setRepInProgress] = useState(false); const [selectedDistance, setSelectedDistance] = useState(''); const [workoutId, setWorkoutId] = useState(null); - const [device, setDevice] = useState(null); - const [receivedData, setReceivedData] = useState([]); const [startTime, setStartTime] = useState(null); const distances = [ @@ -28,66 +27,6 @@ function WorkoutScreen({ navigation }) { '800m', '1600m', ]; - const HC05_DEVICE_NAME = 'HC-05'; - - useEffect(() => { - const initBluetooth = async () => { - console.log('started'); - console.log(RNBluetoothClassic); - const enabled = await RNBluetoothClassic.isBluetoothEnabled(); - - console.log(enabled); - if (!enabled) { - await RNBluetoothClassic.requestEnable(); - } - - const devices = await RNBluetoothClassic.getBondedDevices(); - const hc05 = devices.find( - (device) => device.name === HC05_DEVICE_NAME - ); - - if (hc05) { - setDevice(hc05); - Alert.alert( - 'Connected', - `Device ${HC05_DEVICE_NAME} is ready.` - ); - } else { - Alert.alert('Error', `${HC05_DEVICE_NAME} not found.`); - } - }; - - initBluetooth(); - }, []); - - const handleConnectDevice = async () => { - if (!device) { - Alert.alert('Error', 'No device to connect to.'); - return; - } - - try { - const connected = await device.connect(); - if (connected) { - Alert.alert('Connected', `Connected to ${device.name}`); - device.onDataReceived(handleDataReceived); - } else { - Alert.alert('Error', 'Could not connect to the device.'); - } - } catch (error) { - Alert.alert('Error', 'Failed to connect to the device.'); - console.error(error); - } - }; - - const handleDataReceived = (data) => { - try { - console.log('Data received:', data.data); - setReceivedData((prevData) => [...prevData, data.data]); - } catch (error) { - console.error('Error processing data:', error); - } - }; const handleStartWorkout = async () => { try { @@ -98,23 +37,20 @@ function WorkoutScreen({ navigation }) { if (workoutId) { setWorkoutId(workoutId); setWorkoutStarted(true); + Alert.alert( + 'Workout Started', + 'You can now perform your reps.' + ); } else { Alert.alert('Error', 'Failed to start workout.'); } } catch (error) { Alert.alert('Error', 'Could not start workout.'); + console.error(error); } }; const handleStartRep = () => { - if (!device) { - Alert.alert( - 'Error', - 'No Bluetooth device connected. Please connect and try again.' - ); - return; - } - if (!selectedDistance) { Alert.alert('Error', 'Please select a sprint distance'); return; @@ -126,47 +62,19 @@ function WorkoutScreen({ navigation }) { }; const handleRepComplete = async () => { - if (!device) { - Alert.alert( - 'Error', - 'Bluetooth device disconnected. Please reconnect.' - ); - return; - } - try { const endTime = new Date().toISOString(); - // Parse received data into `timeSeriesData` - const timeSeriesData = receivedData.map((line) => { - const [timestamp, ax, ay, az, gx, gy, gz] = line.split(','); - return { - timestamp: new Date(timestamp).toISOString(), - accelerometer: { - x: parseFloat(ax), - y: parseFloat(ay), - z: parseFloat(az), - }, - gyroscope: { - x: parseFloat(gx), - y: parseFloat(gy), - z: parseFloat(gz), - }, - }; - }); - const repData = { workoutId, sprintLength: selectedDistance, startTime: startTime.toISOString(), endTime, - timeSeriesData, }; const response = await createRep(repData); if (response.success) { setRepInProgress(false); - setReceivedData([]); // Clear data after successful submission Alert.alert('Rep Complete', 'Rep data has been logged!'); } else { throw new Error('Failed to log rep'); @@ -179,17 +87,89 @@ function WorkoutScreen({ navigation }) { const handleEndWorkout = async () => { try { + // Ensure USB connection and data retrieval + const usbSerial = await connectToUSB(); + if (!usbSerial) { + Alert.alert('Error', 'USB device connection failed.'); + return; + } + + // Read all data from USB + const rawData = await usbSerial.read(); + if (!rawData) { + throw new Error('No data received from USB device.'); + } + + console.log('Raw Data received:', rawData); + + // Parse USB data + const { metadata, data: timeSeriesData } = JSON.parse(rawData); + + // Validate the format (optional) + if (!Array.isArray(timeSeriesData)) { + throw new Error('Invalid data format from USB device.'); + } + + // Complete the workout with metadata and time-series data const response = await completeWorkout(workoutId, { endTime: new Date().toISOString(), + metadata, // Include metadata + timeSeriesData, // Include time-series data }); + if (response.success) { - Alert.alert('Workout Ended', 'Workout complete.'); + Alert.alert( + 'Workout Complete', + 'Your workout data has been saved.' + ); navigation.navigate('Home'); } else { - throw new Error('Failed to complete workout'); + throw new Error('Failed to complete workout.'); + } + } catch (error) { + Alert.alert( + 'Error', + error.message || 'Failed to complete workout.' + ); + console.error('Error completing workout:', error); + } + }; + + const connectToUSB = async () => { + try { + // List available USB devices + const devices = await UsbSerialManager.list(); + if (devices.length === 0) { + Alert.alert('Error', 'No USB devices found.'); + return null; } + + // Request permission for the first available device + const granted = await UsbSerialManager.tryRequestPermission( + devices[0].deviceId + ); + if (!granted) { + Alert.alert('Error', 'USB permission denied.'); + return null; + } + + // Open the USB device for communication + const usbSerial = await UsbSerialManager.open(devices[0].deviceId, { + baudRate: 115200, + parity: Parity.None, + dataBits: 8, + stopBits: 1, + }); + + Alert.alert( + 'USB Connected', + `Connected to device: ${devices[0].productName}` + ); + return usbSerial; } catch (error) { - Alert.alert('Error', 'Failed to complete workout'); + Alert.alert('Error', 'Failed to connect to USB device.'); + console.error('Error connecting to USB:', error); + return null; } }; diff --git a/server/controllers/post.js b/server/controllers/post.js index 43f2f50..3b5419c 100644 --- a/server/controllers/post.js +++ b/server/controllers/post.js @@ -36,11 +36,17 @@ const loginUser = async (req, res) => { try { const { email, password } = req.body; + console.log(req.body); + + console.log(email); + // Find user by email const user = await User.findOne({ email }); // console.log(user); + console.log(user); + if (!user) { return res.status(404).json({ message: 'User not found' }); } @@ -78,16 +84,10 @@ const loginUser = async (req, res) => { }; const postRep = async (req, res) => { - const { - workoutId, - sprintLength, - startTime, - endTime, - timeSeriesData, // Array of time series data points - } = req.body; + const { workoutId, sprintLength, startTime, endTime } = req.body; try { - // 1. Create the rep metadata in MongoDB + // Create the rep metadata in MongoDB const rep = new Rep({ workoutId, sprintLength, @@ -97,12 +97,7 @@ const postRep = async (req, res) => { await rep.save(); - // 2. Write the time series data and calculate metrics - await writeRepTimeSeriesData(rep._id, timeSeriesData); - - // Respond with the saved rep object including the calculated metrics - const updatedRep = await Rep.findById(rep._id); // Fetch updated rep with calculated fields - res.status(201).json(updatedRep); + res.status(201).json(rep); } catch (error) { console.error('Error in postRep:', error); res.status(500).json({ message: 'Error creating rep', error }); diff --git a/server/controllers/put.js b/server/controllers/put.js index b14e82d..da9eb6d 100644 --- a/server/controllers/put.js +++ b/server/controllers/put.js @@ -22,7 +22,7 @@ const putUser = async (req, res) => { // Update workout const completeWorkout = async (req, res) => { - const { workoutId, endTime } = req.body; // Workout ID and endTime sent from frontend + const { workoutId, endTime, timeSeriesData } = req.body; // Add timeSeriesData to the payload try { // Fetch the workout by ID @@ -39,6 +39,16 @@ const completeWorkout = async (req, res) => { .json({ message: 'No reps found for this workout' }); } + // Collect rep IDs associated with the workout + const repIds = reps.map((rep) => rep._id); + + // Update the workout's end time + workout.endTime = endTime; + await workout.save(); + + // Write time series data using rep IDs + await writeRepTimeSeriesData(repIds, workout.startTime, timeSeriesData); + const numReps = reps.length; // Calculate averages for the workout based on the reps @@ -52,8 +62,7 @@ const completeWorkout = async (req, res) => { reps.reduce((acc, rep) => acc + rep.angularVelocityDriveAvg, 0) / numReps; - // Update the workout with endTime and the calculated averages - workout.endTime = endTime; + // Update the workout with calculated averages workout.groundContactTimeAvg = groundContactTimeAvg; workout.angularVelocityAccAvg = angularVelocityAccAvg; workout.angularVelocityDriveAvg = angularVelocityDriveAvg; @@ -81,6 +90,7 @@ const completeWorkout = async (req, res) => { // Return both the completed workout and the generated report res.status(200).json({ workout, workoutReport }); } catch (error) { + console.error('Error completing workout:', error); res.status(500).json({ message: 'Error completing workout', error }); } }; diff --git a/server/data_processing.py b/server/data_processing.py new file mode 100644 index 0000000..237a35f --- /dev/null +++ b/server/data_processing.py @@ -0,0 +1,118 @@ +from flask import Flask, request, jsonify +from pymongo import MongoClient +from bson import ObjectId +import math +from datetime import datetime, timedelta +from dotenv import load_dotenv +import os +from flask_cors import CORS +import json + +# Load environment variables from .env file +load_dotenv() +# load_dotenv(dotenv_path='/etc/storm_app.env') + +app = Flask(__name__) +CORS(app) # Allow cross-origin requests + +# MongoDB Connection +mongo_uri = os.getenv('MONGO_ATLAS_URI') +db_name = 'storm_db' + +client = MongoClient(mongo_uri) +db = client[db_name] +imu_collection = db['imudata'] +rep_collection = db['reps'] + +# total_reps = rep_collection.count_documents({}) +# print(f"Total number of reps in the database: {total_reps}") + +# Helper function for processing time-series data +def process_time_series_data(rep_ids, workout_start_time, time_series_data): + """ + Process time-series data and associate data points with their respective reps. + """ + # Fetch start and end times for each rep + rep_data = list(rep_collection.find( + {"_id": {"$in": [ObjectId(rid) for rid in rep_ids]}}, + {"startTime": 1, "endTime": 1} + )) + + # Convert rep intervals to a dictionary for easy access + rep_intervals = { + str(rep["_id"]): { + "start": datetime.fromisoformat(rep["startTime"]) if isinstance(rep["startTime"], str) else rep["startTime"], + "end": datetime.fromisoformat(rep["endTime"]) if isinstance(rep["endTime"], str) else rep["endTime"], + } + for rep in rep_data + } + + # print(rep_intervals) + + imu_records = [] # To store IMU data records for MongoDB + for point in time_series_data: + offset = timedelta(seconds=point[0]) + absolute_timestamp = workout_start_time + offset + + # Determine which rep this data point belongs to + for rep_id, interval in rep_intervals.items(): + rep_start = interval["start"] + rep_end = interval["end"] + + if rep_start <= absolute_timestamp <= rep_end: + # Compute relative timestamp for the specific rep + relative_timestamp = (absolute_timestamp - rep_start).total_seconds() + + # Prepare MongoDB document + imu_records.append({ + "timestamp": round(relative_timestamp, 1), # Relative to the rep + "repId": str(ObjectId(rep_id)), + "accelerometer": { + "x": point[1][0], + "y": point[1][1], + "z": point[1][2], + }, + "gyroscope": { + "x": point[2][0], + "y": point[2][1], + "z": point[2][2], + }, + }) + break # Move to the next data point once matched + return imu_records + +@app.route('/api/workouts/process_workout_data', methods=['POST']) +def process_rep_data(): + """ + Flask route to process IMU time-series data and store it in MongoDB. + """ + try: + # Parse JSON data from the request + data = request.json + rep_ids = data['repIds'] # List of rep IDs + workout_start_time = datetime.fromisoformat(data['startTime']) # Workout start time + time_series_data = data['timeSeriesData'] # Time series data + + print(type(rep_ids[0])) + print(workout_start_time) + print(time_series_data[:5]) + + # Process time-series data + imu_records = process_time_series_data(rep_ids, workout_start_time, time_series_data) + + # print(json.dumps(imu_records[599:605], indent=4)) + + # Insert the processed IMU records into MongoDB + if imu_records: + imu_collection.insert_many(imu_records) + print(f"Inserted {len(imu_records)} IMU records into MongoDB") + + return jsonify({"message": "Processing complete", "success": True}), 200 + + except Exception as e: + print("Error processing rep data:", e) + return jsonify({"message": "Error processing rep data", "error": str(e)}), 500 + +# Run the Flask server +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5001) diff --git a/server/helpers/timeStreamController.js b/server/helpers/timeStreamController.js index 2d28cfb..c6271a9 100644 --- a/server/helpers/timeStreamController.js +++ b/server/helpers/timeStreamController.js @@ -1,64 +1,27 @@ -const IMUData = require('../models/IMUDataSchema'); -const Rep = require('../models/RepSchema'); +const axios = require('axios'); -// Helper functions for calculations -const calculateThighAngularVelocity = (gyroscopeData) => { - return Math.sqrt( - Math.pow(gyroscopeData.x, 2) + - Math.pow(gyroscopeData.y, 2) + - Math.pow(gyroscopeData.z, 2) - ); -}; - -const calculateGroundContactTime = (accelerometerData) => { - // Example calculation placeholder - return 200; // Replace with logic for ground contact time -}; - -// Function to write raw and calculated metrics data to MongoDB -const writeRepTimeSeriesData = async (repId, timeSeriesData) => { +const writeRepTimeSeriesData = async (repIds, startTime, timeSeriesData) => { try { - // Step 1: Insert raw IMU data in batches to MongoDB Time Series Collection - const rawRecords = timeSeriesData.map((data) => ({ - timestamp: new Date(data.timestamp), - repId: repId, - accelerometer: data.accelerometer, - gyroscope: data.gyroscope, - })); - - await IMUData.insertMany(rawRecords); - console.log( - 'Successfully wrote raw sensor records to MongoDB Time Series' + // Send POST request to the Flask server on port 5001 + const response = await axios.post( + 'http://3.139.131.0:5001/api/workouts/process_rep_data', + { + repIds, + startTime, + timeSeriesData, + } ); - // Step 2: Calculate metrics for the entire batch - let totalThighAngularVelocity = 0; - let totalGroundContactTime = 0; - - timeSeriesData.forEach((data) => { - totalThighAngularVelocity += calculateThighAngularVelocity( - data.gyroscope + if (response.data.success) { + console.log('Time series data processed successfully'); + } else { + console.error( + 'Error processing time series data:', + response.data.message ); - totalGroundContactTime += calculateGroundContactTime( - data.accelerometer - ); - }); - - const dataCount = timeSeriesData.length; - const avgThighAngularVelocity = totalThighAngularVelocity / dataCount; - const avgGroundContactTime = totalGroundContactTime / dataCount; - - // Step 3: Update the Rep document with averaged metrics for this batch - await Rep.findByIdAndUpdate(repId, { - angularVelocityAccAvg: avgThighAngularVelocity, - groundContactTimeAvg: avgGroundContactTime, - }); - - console.log( - 'Successfully calculated and saved metrics in MongoDB for this batch' - ); + } } catch (error) { - console.error('Error writing rep data to MongoDB:', error); + console.error('Error calling Flask server:', error.message); } }; diff --git a/server/index.js b/server/index.js index acb9f42..a9b4742 100644 --- a/server/index.js +++ b/server/index.js @@ -26,21 +26,6 @@ app.get('/', (req, res) => { res.send('API is running...'); }); -// const testpwd = async (req, res) => { -// try { -// const hashedPassword = await bcrypt.compare( -// 'Vikboss123*', -// '$2b$10$mqpAhmuH/qZMQpizVwRKFeg7yJdNnzbgRi/ys7Sm5zHDVCG2FNubS' -// ); - -// console.log(hashedPassword); -// } catch (error) { -// console.error('oog booga'); // Add this line -// } -// }; - -// testpwd(); - // Server listener const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/server/models/IMUDataSchema.js b/server/models/IMUDataSchema.js index 5305237..1ca2650 100644 --- a/server/models/IMUDataSchema.js +++ b/server/models/IMUDataSchema.js @@ -3,7 +3,7 @@ const mongoose = require('mongoose'); const IMUDataSchema = new mongoose.Schema( { timestamp: { - type: Date, + type: Number, required: true, index: true, }, diff --git a/server/package.json b/server/package.json index 4a96b60..70ff9ae 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,10 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node index.js", - "dev": "nodemon index.js" + "dev:node": "nodemon index.js", + "dev:flask": "python data_processing.py", + "dev": "concurrently \"npm run dev:node\" \"npm run dev:flask\"", + "prod": "concurrently \"npm start\" \"python flask_app.py\"" }, "keywords": [], "author": "", @@ -25,6 +28,7 @@ "openai": "^4.68.1" }, "devDependencies": { + "concurrently": "^9.1.0", "nodemon": "^3.1.7" } } -- GitLab