Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions FES-app/FES/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true
"supportsTablet": true,
"infoPlist": {
"NSMotionUsageDescription": "This app uses motion sensors to detect foot movement for FES device control."
}
},
"android": {
"adaptiveIcon": {
Expand All @@ -19,7 +22,11 @@
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
"predictiveBackGestureEnabled": false,
"permissions": [
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION"
]
},
"web": {
"output": "static",
Expand Down
242 changes: 208 additions & 34 deletions FES-app/FES/app/functional/calibration.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,82 @@
import { StyleSheet, View, TouchableOpacity, ActivityIndicator, ScrollView } from 'react-native';
import { StyleSheet, View, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useState, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { calibrationService, CalibrationProgress, CalibrationResult } from '../../services/backend/calibration';

export default function CalibrationScreen() {
const router = useRouter();
const colorScheme = useColorScheme();
const [currentStep, setCurrentStep] = useState(1);
const [isCalibrating, setIsCalibrating] = useState(false);
const [progress, setProgress] = useState(0);
const [calibrationProgress, setCalibrationProgress] = useState<CalibrationProgress>({
currentStep: 0,
totalSteps: 5,
isValidating: false,
validationMessage: ''
});
const [calibrationResults, setCalibrationResults] = useState<CalibrationResult[]>([]);
const [calibrationComplete, setCalibrationComplete] = useState(false);

useEffect(() => {
let interval: ReturnType<typeof setInterval>;

if (isCalibrating) {
interval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setCurrentStep(2);
setIsCalibrating(false);
return 100;
}
return prev + 10;
});
}, 300);
}

return () => clearInterval(interval);
}, [isCalibrating]);
// Dynamic colors based on color scheme
const colors = {
instructionCard: colorScheme === 'dark' ? '#2C2C2E' : '#F2F2F7',
warningCard: colorScheme === 'dark' ? '#1C3A5C' : '#E3F2FD',
resultsContainer: colorScheme === 'dark' ? '#2C2C2E' : '#F2F2F7',
finalResultsContainer: colorScheme === 'dark' ? '#2C2C2E' : '#F2F2F7',
progressBar: colorScheme === 'dark' ? '#3A3A3C' : '#E0E0E0',
borderColor: colorScheme === 'dark' ? '#3A3A3C' : '#E5E5EA',
headerBorder: colorScheme === 'dark' ? '#3A3A3C' : '#E5E5EA',
};

const handleStartCalibration = () => {
setProgress(0);
setIsCalibrating(true);
const handleStartCalibration = async () => {
try {
setIsCalibrating(true);
setCalibrationComplete(false);
setCalibrationResults([]);

await calibrationService.startCalibration(
(progress) => {
setCalibrationProgress(progress);
setProgress((progress.currentStep / progress.totalSteps) * 100);
},
(results) => {
setCalibrationResults(results);
setCalibrationComplete(true);
setIsCalibrating(false);
setCurrentStep(2);

Alert.alert(
'Calibration Complete',
`Calibration successful! All 5 foot angles captured.`,
[{ text: 'OK' }]
);
}
);
} catch (error) {
console.error('Calibration failed:', error);
Alert.alert(
'Calibration Failed',
'An error occurred during calibration. Please try again.',
[{ text: 'OK' }]
);
setIsCalibrating(false);
}
};


const handleComplete = () => {
// Navigate back to home after calibration
router.back();
};

return (
<ThemedView style={styles.container}>
<View style={styles.header}>
<View style={[styles.header, { borderBottomColor: colors.headerBorder }]}>
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
<Ionicons name="arrow-back" size={24} color="#007AFF" />
</TouchableOpacity>
Expand All @@ -64,27 +97,61 @@ export default function CalibrationScreen() {
</View>
</View>

<ThemedText type="subtitle" style={styles.stepTitle}>Prepare for Calibration</ThemedText>
<ThemedText type="subtitle" style={styles.stepTitle}>Foot Drop Calibration</ThemedText>
<ThemedText style={styles.stepDescription}>
Please make sure the device is properly attached and you're in a comfortable position.
We'll capture the angle when your foot gets "stuck" during walking to detect future foot drop episodes.
</ThemedText>

<View style={styles.instructionCard}>
<Ionicons name="alert-circle" size={24} color="#FF9500" style={styles.instructionIcon} />
<View style={[styles.instructionCard, { backgroundColor: colors.instructionCard }]}>
<Ionicons name="walk" size={24} color="#FF9500" style={styles.instructionIcon} />
<ThemedText style={styles.instructionText}>
Walk normally and let your foot get stuck at its natural "drop" position. We'll capture that angle 5 times for 5 seconds each.
</ThemedText>
</View>

<View style={[styles.warningCard, { backgroundColor: colors.warningCard }]}>
<Ionicons name="information-circle" size={24} color="#007AFF" style={styles.instructionIcon} />
<ThemedText style={styles.instructionText}>
Ensure the electrodes are properly placed on your skin and the device is turned on.
The phone should be attached to your leg/foot to measure the angle when foot drop occurs.
</ThemedText>
</View>

{isCalibrating ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
<ThemedText style={styles.loadingText}>
Calibrating... {progress}%
{calibrationProgress.validationMessage}
</ThemedText>
<View style={styles.progressBar}>
{calibrationProgress.currentAngle !== undefined && (
<ThemedText style={styles.angleText}>
Foot Angle: {calibrationProgress.currentAngle.toFixed(2)}°
</ThemedText>
)}
<ThemedText style={styles.progressText}>
Capturing foot angle {calibrationProgress.currentStep} of {calibrationProgress.totalSteps} ({progress.toFixed(0)}%)
</ThemedText>
<View style={[styles.progressBar, { backgroundColor: colors.progressBar }]}>
<View style={[styles.progressFill, { width: `${progress}%` }]} />
</View>

{/* Show results as they come in */}
{calibrationResults.length > 0 && (
<View style={[styles.resultsContainer, { backgroundColor: colors.resultsContainer }]}>
<ThemedText style={styles.resultsTitle}>Foot Angles Captured:</ThemedText>
{calibrationResults.map((result, index) => (
<View key={index} style={styles.resultRow}>
<ThemedText style={styles.resultText}>
Capture {index + 1}: {result.angle.toFixed(2)}°
</ThemedText>
</View>
))}
{calibrationResults.length > 0 && (
<ThemedText style={[styles.averageText, { borderTopColor: colors.borderColor }]}>
Target Foot Drop Angle: {calibrationService.getAverageCalibrationAngle().toFixed(2)}°
</ThemedText>
)}
</View>
)}
</View>
) : (
<TouchableOpacity
Expand Down Expand Up @@ -124,6 +191,24 @@ export default function CalibrationScreen() {
<ThemedText style={styles.successText}>Calibration Successful!</ThemedText>
</View>

{calibrationResults.length > 0 && (
<View style={[styles.finalResultsContainer, { backgroundColor: colors.finalResultsContainer }]}>
<ThemedText style={styles.finalResultsTitle}>Final Calibration Results</ThemedText>
<ThemedText style={styles.finalResultsText}>
Average Foot Drop Angle: {calibrationService.getAverageCalibrationAngle().toFixed(2)}°
</ThemedText>
<View style={styles.finalResultsList}>
{calibrationResults.map((result, index) => (
<View key={index} style={styles.finalResultRow}>
<ThemedText style={styles.finalResultText}>
Capture {index + 1}: {result.angle.toFixed(2)}°
</ThemedText>
</View>
))}
</View>
</View>
)}

<TouchableOpacity
style={styles.primaryButton}
onPress={handleComplete}
Expand All @@ -147,7 +232,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#E5E5EA',
},
backButton: {
padding: 8,
Expand Down Expand Up @@ -209,7 +293,13 @@ const styles = StyleSheet.create({
},
instructionCard: {
flexDirection: 'row',
backgroundColor: '#F2F2F7',
borderRadius: 12,
padding: 16,
marginBottom: 15,
width: '100%',
},
warningCard: {
flexDirection: 'row',
borderRadius: 12,
padding: 16,
marginBottom: 30,
Expand Down Expand Up @@ -269,7 +359,6 @@ const styles = StyleSheet.create({
progressBar: {
height: 10,
width: '100%',
backgroundColor: '#E0E0E0',
borderRadius: 5,
marginTop: 20,
overflow: 'hidden',
Expand All @@ -279,4 +368,89 @@ const styles = StyleSheet.create({
backgroundColor: '#007AFF',
borderRadius: 5,
},
angleText: {
fontSize: 18,
fontWeight: '600',
color: '#007AFF',
marginTop: 8,
},
progressText: {
fontSize: 14,
color: '#8E8E93',
marginTop: 8,
},
resultsContainer: {
marginTop: 20,
width: '100%',
borderRadius: 8,
padding: 12,
},
resultsTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
textAlign: 'center',
},
resultRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
resultText: {
fontSize: 14,
flex: 1,
},
statusIndicator: {
width: 8,
height: 8,
borderRadius: 4,
marginLeft: 8,
},
averageText: {
fontSize: 16,
fontWeight: '600',
color: '#007AFF',
textAlign: 'center',
marginTop: 8,
paddingTop: 8,
borderTopWidth: 1,
},
finalResultsContainer: {
borderRadius: 12,
padding: 16,
marginVertical: 20,
width: '100%',
},
finalResultsTitle: {
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
marginBottom: 12,
},
finalResultsText: {
fontSize: 16,
textAlign: 'center',
marginBottom: 8,
fontWeight: '500',
},
finalResultsList: {
marginTop: 12,
},
finalResultRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6,
},
finalResultText: {
fontSize: 14,
flex: 1,
},
finalStatusIndicator: {
width: 10,
height: 10,
borderRadius: 5,
marginLeft: 8,
},
});
2 changes: 1 addition & 1 deletion FES-app/FES/app/functional/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default function HomeScreen() {
<ActionButton
icon="settings"
label="Calibrate"
onPress={() => router.push('/RealTimeData' as any)}
onPress={() => router.push('/functional/calibration' as any)}
color="#FF9500"
/>
</View>
Expand Down
Loading