diff --git a/client/api/api_client.js b/client/api/api_client.js index 9ea8f79fde3a4961527a454651ea0b29b7a7aabf..d5c08671105be2e51614045df412233be91697a3 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 9d167698d9dca4d49d14c7f080d6e19b33382819..0129ddd0781d56d4c2bbf68eaf5b0ae10c8a6f26 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 210c63e1c68b1ce2662379f8f59312ef1c0c1d83..ba94341d2020eb27ae0b5155e4a820e74b484909 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 557bc0f9f0aa74515c834ad82955abdb0a82b099..3d104134ab07da86b361d31299620164a8112267 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 2a5a4c78c11998d73501195ce4bded5c4067b969..fc098dd30c917fb3463185beeec1bbec863fd0db 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 43f2f506ca450de832dbacd73b0bc36a4da9a610..3b5419c4121c1f6f8558e2676b625339cedbcdc4 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 b14e82d473eecc9fee061b5bca8c1d76aa3153cb..da9eb6d760eda0abbd83320e929d4b0b78bae158 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 0000000000000000000000000000000000000000..237a35fe002c428919b1b828809065498d7f1e8c --- /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 2d28cfb1d87217cad859659ed2d378bd0051e8c0..c6271a94c184865db416b6d641366513cbf07c41 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 acb9f4286348b82e636bff53ae4872eca69f7b8b..a9b47426029ca90773e43f76d76ac094606fd8ff 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 5305237bcef3948bdbdf431a4ff117f77b183e46..1ca2650a0f9326e039923b0f1498a5995b150730 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 4a96b60b25c065daad9792b9d0c7a40308572af5..70ff9ae1425c4f03793c39601ca332d785e8b2b5 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" } }