Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import boto3 | |
| import json | |
| import time | |
| import threading | |
| import queue | |
| from datetime import datetime | |
| import pandas as pd | |
| import os | |
| from botocore.exceptions import NoCredentialsError, ClientError | |
| import tempfile | |
| import platform | |
| class AccelerometerS3App: | |
| def __init__(self): | |
| self.s3_client = None | |
| self.is_recording = False | |
| self.data_queue = queue.Queue() | |
| self.recording_thread = None | |
| self.recorded_data = [] | |
| self.motion_data_buffer = [] | |
| def setup_s3_client(self, aws_access_key, aws_secret_key, region_name): | |
| """Initialize S3 client with credentials""" | |
| try: | |
| self.s3_client = boto3.client( | |
| 's3', | |
| aws_access_key_id=aws_access_key, | |
| aws_secret_access_key=aws_secret_key, | |
| region_name=region_name | |
| ) | |
| # Test connection | |
| self.s3_client.list_buckets() | |
| return "β AWS credentials configured successfully!" | |
| except NoCredentialsError: | |
| return "β Invalid AWS credentials" | |
| except Exception as e: | |
| return f"β Error setting up S3 client: {str(e)}" | |
| def process_motion_data_from_browser(self, motion_data_json): | |
| """Process motion data received from iPad browser""" | |
| try: | |
| if not motion_data_json or motion_data_json.strip() == "": | |
| return "β No motion data received" | |
| # Parse the JSON data from the browser | |
| motion_data = json.loads(motion_data_json) | |
| # Add server timestamp and process data | |
| for reading in motion_data: | |
| processed_reading = { | |
| 'timestamp': reading.get('timestamp', datetime.now().isoformat()), | |
| 'x': round(float(reading.get('x', 0)), 6), | |
| 'y': round(float(reading.get('y', 0)), 6), | |
| 'z': round(float(reading.get('z', 0)), 6), | |
| 'device_id': reading.get('device_id', 'ipad_unknown'), | |
| 'data_source': 'device_motion_api', | |
| 'user_agent': reading.get('user_agent', 'unknown'), | |
| 'orientation': reading.get('orientation', 'unknown') | |
| } | |
| self.motion_data_buffer.append(processed_reading) | |
| return f"β Processed {len(motion_data)} motion readings" | |
| except json.JSONDecodeError as e: | |
| return f"β Invalid JSON data: {str(e)}" | |
| except Exception as e: | |
| return f"β Error processing motion data: {str(e)}" | |
| def start_recording_session(self, duration, sample_rate): | |
| """Start a recording session for specified duration""" | |
| self.motion_data_buffer = [] | |
| self.is_recording = True | |
| return f""" | |
| π΄ Recording session started for {duration} seconds at {sample_rate} Hz | |
| π± **iPad Instructions:** | |
| 1. Make sure your iPad is in landscape orientation | |
| 2. The motion sensor should now be active | |
| 3. Move your iPad around to generate motion data | |
| 4. Data will be collected automatically | |
| β±οΈ Recording will automatically stop after {duration} seconds. | |
| """ | |
| def stop_recording_session(self): | |
| """Stop recording and transfer data""" | |
| if not self.is_recording: | |
| return "β No recording session in progress" | |
| self.is_recording = False | |
| self.recorded_data = self.motion_data_buffer.copy() | |
| return f"βΉοΈ Recording session stopped. {len(self.recorded_data)} samples collected from iPad sensors" | |
| def get_device_id(self): | |
| """Generate a device identifier for iPad""" | |
| return f"ipad_{int(time.time())}" | |
| def upload_to_s3(self, bucket_name, file_prefix=""): | |
| """Upload recorded data to S3 bucket""" | |
| if not self.s3_client: | |
| return "β AWS credentials not configured" | |
| if not self.recorded_data: | |
| return "β No data to upload. Record some data first." | |
| try: | |
| # Create filename with timestamp | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| filename = f"{file_prefix}ipad_accelerometer_data_{timestamp}.json" | |
| # Prepare data for upload | |
| upload_data = { | |
| 'device_type': 'iPad', | |
| 'device_id': self.recorded_data[0]['device_id'] if self.recorded_data else 'unknown', | |
| 'recording_info': { | |
| 'start_time': self.recorded_data[0]['timestamp'] if self.recorded_data else None, | |
| 'end_time': self.recorded_data[-1]['timestamp'] if self.recorded_data else None, | |
| 'sample_count': len(self.recorded_data), | |
| 'data_source': 'device_motion_api' | |
| }, | |
| 'data': self.recorded_data | |
| } | |
| # Convert to JSON and upload | |
| json_data = json.dumps(upload_data, indent=2) | |
| self.s3_client.put_object( | |
| Bucket=bucket_name, | |
| Key=filename, | |
| Body=json_data, | |
| ContentType='application/json' | |
| ) | |
| return f"β Successfully uploaded {filename} to s3://{bucket_name}/{filename}" | |
| except ClientError as e: | |
| return f"β AWS Error: {str(e)}" | |
| except Exception as e: | |
| return f"β Upload error: {str(e)}" | |
| def get_data_preview(self): | |
| """Get a preview of recorded data""" | |
| if not self.recorded_data: | |
| return "No data recorded yet from iPad sensors" | |
| # Create a DataFrame for better display | |
| df = pd.DataFrame(self.recorded_data) | |
| # Determine data source | |
| data_source = self.recorded_data[0].get('data_source', 'unknown') | |
| # Show basic statistics | |
| preview = f""" | |
| π **iPad Motion Data Summary:** | |
| - π± Data Source: Device Motion API | |
| - π² Device: {self.recorded_data[0].get('user_agent', 'iPad')} | |
| - Total samples: {len(self.recorded_data)} | |
| - Device ID: {self.recorded_data[0]['device_id']} | |
| π **Acceleration Statistics (m/sΒ²):** | |
| ``` | |
| {df[['x', 'y', 'z']].describe().round(6)} | |
| ``` | |
| π **Latest 5 samples:** | |
| ``` | |
| {df[['timestamp', 'x', 'y', 'z']].tail().to_string(index=False)} | |
| ``` | |
| π§ **Device Orientation:** {self.recorded_data[-1].get('orientation', 'Unknown')} | |
| """ | |
| return preview | |
| # Initialize the app | |
| app = AccelerometerS3App() | |
| # Custom CSS for mobile-friendly interface | |
| custom_css = """ | |
| .gradio-container { | |
| max-width: 100% !important; | |
| padding: 10px !important; | |
| } | |
| .motion-display { | |
| font-family: 'Courier New', monospace; | |
| background: #1e1e1e; | |
| color: #00ff00; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin: 10px 0; | |
| } | |
| .ipad-instructions { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 12px; | |
| margin: 15px 0; | |
| } | |
| #motion-sensor-area { | |
| background: #f0f0f0; | |
| border: 2px dashed #007AFF; | |
| border-radius: 12px; | |
| padding: 20px; | |
| text-align: center; | |
| margin: 10px 0; | |
| min-height: 200px; | |
| } | |
| """ | |
| # JavaScript for iPad motion sensor access | |
| motion_sensor_js = """ | |
| function startMotionSensor(duration, sampleRate) { | |
| if (!window.DeviceMotionEvent) { | |
| return "β Device Motion API not supported on this device"; | |
| } | |
| let motionData = []; | |
| let isRecording = true; | |
| let sampleInterval = 1000 / sampleRate; // Convert Hz to milliseconds | |
| let lastSample = 0; | |
| // Get device information | |
| const deviceInfo = { | |
| userAgent: navigator.userAgent, | |
| orientation: screen.orientation ? screen.orientation.type : 'unknown' | |
| }; | |
| function handleMotion(event) { | |
| const now = Date.now(); | |
| // Throttle samples based on sample rate | |
| if (now - lastSample < sampleInterval) return; | |
| lastSample = now; | |
| if (!isRecording) return; | |
| const acceleration = event.accelerationIncludingGravity; | |
| if (acceleration) { | |
| motionData.push({ | |
| timestamp: new Date().toISOString(), | |
| x: acceleration.x || 0, | |
| y: acceleration.y || 0, | |
| z: acceleration.z || 0, | |
| device_id: 'ipad_' + Date.now(), | |
| ...deviceInfo | |
| }); | |
| } | |
| } | |
| // Request permission for iOS 13+ | |
| if (typeof DeviceMotionEvent.requestPermission === 'function') { | |
| DeviceMotionEvent.requestPermission() | |
| .then(permissionState => { | |
| if (permissionState === 'granted') { | |
| window.addEventListener('devicemotion', handleMotion); | |
| } else { | |
| alert('Motion permission denied. Please enable in Safari settings.'); | |
| } | |
| }) | |
| .catch(console.error); | |
| } else { | |
| // For older devices or non-iOS | |
| window.addEventListener('devicemotion', handleMotion); | |
| } | |
| // Stop recording after duration | |
| setTimeout(() => { | |
| isRecording = false; | |
| window.removeEventListener('devicemotion', handleMotion); | |
| // Return data to Gradio | |
| const motionDataJson = JSON.stringify(motionData); | |
| return motionDataJson; | |
| }, duration * 1000); | |
| return `Recording started for ${duration} seconds at ${sampleRate} Hz`; | |
| } | |
| """ | |
| # Create Gradio interface | |
| with gr.Blocks(title="iPad Accelerometer to S3", theme=gr.themes.Soft(), css=custom_css) as demo: | |
| gr.Markdown(""" | |
| # π± iPad Accelerometer Data Logger | |
| This app collects **real motion sensor data** from your iPad and uploads it to AWS S3. | |
| **Requirements:** | |
| - Safari browser on iPad (Motion API support) | |
| - iPad with built-in accelerometer (all modern iPads) | |
| - Network connection for data upload | |
| """) | |
| with gr.Tab("π§ Setup"): | |
| gr.Markdown("### iPad Compatibility Check", elem_classes="ipad-instructions") | |
| gr.HTML(""" | |
| <div id="compatibility-check" style="padding: 15px; background: #f8f9fa; border-radius: 8px; margin: 10px 0;"> | |
| <p><strong>π± Device Compatibility:</strong></p> | |
| <p id="user-agent">Loading device info...</p> | |
| <p id="motion-support">Checking motion sensor support...</p> | |
| <p id="permission-status">Checking motion permissions...</p> | |
| <p id="orientation-info">Checking orientation...</p> | |
| <button id="permission-btn" onclick="requestMotionPermission()" style="background: #007AFF; color: white; border: none; padding: 10px 20px; border-radius: 8px; margin: 10px 0; font-size: 16px;"> | |
| π Request Motion Permission | |
| </button> | |
| <div id="permission-help" style="background: #fff3cd; padding: 10px; border-radius: 5px; margin: 10px 0; display: none;"> | |
| <strong>If permission button doesn't work:</strong><br> | |
| 1. Open iPad Settings β Safari β Privacy & Security<br> | |
| 2. Enable "Motion & Orientation Access"<br> | |
| 3. Refresh this page and try again | |
| </div> | |
| </div> | |
| <script> | |
| // Check device compatibility | |
| document.getElementById('user-agent').innerHTML = 'π Device: ' + navigator.userAgent; | |
| // Check if we're on iOS | |
| const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); | |
| const isIPad = /iPad/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); | |
| if (window.DeviceMotionEvent) { | |
| document.getElementById('motion-support').innerHTML = 'β Motion sensors supported'; | |
| // Check if permission API exists (iOS 13+) | |
| if (typeof DeviceMotionEvent.requestPermission === 'function') { | |
| document.getElementById('permission-status').innerHTML = 'π Motion permission required (iOS 13+)'; | |
| } else if (isIOS || isIPad) { | |
| document.getElementById('permission-status').innerHTML = 'β οΈ Older iOS - check Safari settings manually'; | |
| document.getElementById('permission-help').style.display = 'block'; | |
| } else { | |
| document.getElementById('permission-status').innerHTML = 'β No permission required'; | |
| document.getElementById('permission-btn').style.display = 'none'; | |
| } | |
| } else { | |
| document.getElementById('motion-support').innerHTML = 'β Motion sensors not supported'; | |
| document.getElementById('permission-btn').style.display = 'none'; | |
| } | |
| if (screen.orientation) { | |
| document.getElementById('orientation-info').innerHTML = 'π§ Orientation: ' + screen.orientation.type; | |
| } else { | |
| document.getElementById('orientation-info').innerHTML = 'π§ Orientation API not available'; | |
| } | |
| // Function to request motion permission | |
| async function requestMotionPermission() { | |
| const button = document.getElementById('permission-btn'); | |
| const statusEl = document.getElementById('permission-status'); | |
| button.disabled = true; | |
| button.innerHTML = 'β³ Requesting permission...'; | |
| try { | |
| if (typeof DeviceMotionEvent.requestPermission === 'function') { | |
| const permission = await DeviceMotionEvent.requestPermission(); | |
| if (permission === 'granted') { | |
| statusEl.innerHTML = 'β Motion permission granted!'; | |
| button.innerHTML = 'β Permission Granted'; | |
| button.style.background = '#28a745'; | |
| // Test motion sensors | |
| testMotionSensors(); | |
| } else { | |
| statusEl.innerHTML = 'β Motion permission denied'; | |
| button.innerHTML = 'β Permission Denied'; | |
| button.style.background = '#dc3545'; | |
| document.getElementById('permission-help').style.display = 'block'; | |
| } | |
| } else { | |
| // For older iOS or non-iOS devices | |
| statusEl.innerHTML = 'β οΈ Direct permission API not available'; | |
| button.innerHTML = 'β οΈ Check Safari Settings'; | |
| document.getElementById('permission-help').style.display = 'block'; | |
| // Try to test sensors anyway | |
| testMotionSensors(); | |
| } | |
| } catch (error) { | |
| console.error('Permission request failed:', error); | |
| statusEl.innerHTML = 'β Permission request failed: ' + error.message; | |
| button.innerHTML = 'β Request Failed'; | |
| button.style.background = '#dc3545'; | |
| document.getElementById('permission-help').style.display = 'block'; | |
| } | |
| button.disabled = false; | |
| } | |
| // Test motion sensors | |
| function testMotionSensors() { | |
| let testCount = 0; | |
| const maxTests = 10; | |
| function handleTestMotion(event) { | |
| testCount++; | |
| const acc = event.accelerationIncludingGravity; | |
| if (acc && (acc.x !== null || acc.y !== null || acc.z !== null)) { | |
| document.getElementById('permission-status').innerHTML += '<br>π― Motion sensors working! X=' + (acc.x || 0).toFixed(2) + ', Y=' + (acc.y || 0).toFixed(2) + ', Z=' + (acc.z || 0).toFixed(2); | |
| window.removeEventListener('devicemotion', handleTestMotion); | |
| } else if (testCount >= maxTests) { | |
| document.getElementById('permission-status').innerHTML += '<br>β οΈ No motion data received - check Safari settings'; | |
| window.removeEventListener('devicemotion', handleTestMotion); | |
| } | |
| } | |
| window.addEventListener('devicemotion', handleTestMotion); | |
| // Remove listener after 3 seconds | |
| setTimeout(() => { | |
| window.removeEventListener('devicemotion', handleTestMotion); | |
| }, 3000); | |
| } | |
| </script> | |
| """) | |
| gr.Markdown("### AWS S3 Configuration") | |
| with gr.Row(): | |
| aws_key = gr.Textbox(label="AWS Access Key ID", type="password") | |
| aws_secret = gr.Textbox(label="AWS Secret Access Key", type="password") | |
| with gr.Row(): | |
| aws_region = gr.Dropdown( | |
| label="AWS Region", | |
| choices=["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1", "ap-northeast-1"], | |
| value="us-east-1" | |
| ) | |
| setup_btn = gr.Button("Configure AWS", variant="primary") | |
| setup_status = gr.Textbox(label="Setup Status", interactive=False) | |
| setup_btn.click( | |
| app.setup_s3_client, | |
| inputs=[aws_key, aws_secret, aws_region], | |
| outputs=[setup_status] | |
| ) | |
| with gr.Tab("π― Motion Recording"): | |
| gr.Markdown("### Record iPad Motion Data", elem_classes="ipad-instructions") | |
| gr.HTML(""" | |
| <div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 15px; margin: 10px 0;"> | |
| <h4>π± iPad Recording Instructions:</h4> | |
| <div style="background: #e7f3ff; padding: 10px; border-radius: 5px; margin: 10px 0;"> | |
| <strong>π IMPORTANT: Motion Permission Required</strong><br> | |
| <strong>If you haven't seen a permission prompt:</strong><br> | |
| 1. First try the "Request Motion Permission" button in the Setup tab<br> | |
| 2. If that doesn't work, manually enable in Settings β Safari β Privacy & Security β Motion & Orientation Access<br> | |
| 3. Refresh this page after changing settings | |
| </div> | |
| <ol> | |
| <li><strong>Hold your iPad securely</strong> - You'll be moving it around</li> | |
| <li><strong>Grant motion permission</strong> when prompted by Safari</li> | |
| <li><strong>Start recording</strong> and move your iPad in different directions</li> | |
| <li><strong>Try these motions:</strong> Tilt forward/back, left/right, rotate, gentle shaking</li> | |
| <li><strong>Keep Safari active</strong> during recording (don't switch apps)</li> | |
| </ol> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| duration = gr.Slider( | |
| label="Recording Duration (seconds)", | |
| minimum=5, | |
| maximum=60, | |
| value=15, | |
| step=1 | |
| ) | |
| sample_rate = gr.Slider( | |
| label="Sample Rate (Hz)", | |
| minimum=5, | |
| maximum=60, | |
| value=20, | |
| step=5 | |
| ) | |
| with gr.Row(): | |
| start_btn = gr.Button("π΄ Start iPad Recording", variant="primary", scale=2) | |
| stop_btn = gr.Button("βΉοΈ Stop Recording", variant="secondary", scale=1) | |
| recording_status = gr.Textbox(label="Recording Status", interactive=False) | |
| # Motion sensor area | |
| gr.HTML(""" | |
| <div id="motion-sensor-area"> | |
| <h3>π± Motion Sensor Active Area</h3> | |
| <p>Real-time acceleration values will appear here during recording:</p> | |
| <div class="motion-display"> | |
| <div id="accel-x">X: 0.000</div> | |
| <div id="accel-y">Y: 0.000</div> | |
| <div id="accel-z">Z: 0.000</div> | |
| <div id="motion-status">Status: Ready</div> | |
| </div> | |
| <button id="test-motion-btn" onclick="testMotionAccess()" style="background: #17a2b8; color: white; border: none; padding: 8px 16px; border-radius: 5px; margin: 5px;"> | |
| π§ͺ Test Motion Access | |
| </button> | |
| <p><small>Grant permission when prompted and move your iPad to see values change</small></p> | |
| </div> | |
| <script> | |
| let motionDataBuffer = []; | |
| let isRecording = false; | |
| let recordingDuration = 15; | |
| let sampleRate = 20; | |
| let hasMotionPermission = false; | |
| // Test motion access function | |
| async function testMotionAccess() { | |
| const statusEl = document.getElementById('motion-status'); | |
| const button = document.getElementById('test-motion-btn'); | |
| button.disabled = true; | |
| button.innerHTML = 'β³ Testing...'; | |
| statusEl.textContent = 'Status: Requesting permission...'; | |
| try { | |
| // Try to request permission first | |
| if (typeof DeviceMotionEvent.requestPermission === 'function') { | |
| const permission = await DeviceMotionEvent.requestPermission(); | |
| if (permission === 'granted') { | |
| statusEl.textContent = 'Status: Permission granted, testing sensors...'; | |
| hasMotionPermission = true; | |
| testSensors(); | |
| } else { | |
| statusEl.textContent = 'Status: Permission denied - check Safari settings'; | |
| button.innerHTML = 'β Permission Denied'; | |
| showManualInstructions(); | |
| } | |
| } else { | |
| // For older iOS, just try to access sensors | |
| statusEl.textContent = 'Status: Testing sensors (no permission API)...'; | |
| testSensors(); | |
| } | |
| } catch (error) { | |
| console.error('Motion access test failed:', error); | |
| statusEl.textContent = 'Status: Error - ' + error.message; | |
| showManualInstructions(); | |
| } | |
| button.disabled = false; | |
| button.innerHTML = 'π§ͺ Test Motion Access'; | |
| } | |
| function testSensors() { | |
| let testData = []; | |
| let testCount = 0; | |
| const maxTests = 20; | |
| function handleTestMotion(event) { | |
| testCount++; | |
| const acc = event.accelerationIncludingGravity; | |
| if (acc && (acc.x !== null || acc.y !== null || acc.z !== null)) { | |
| const x = acc.x || 0; | |
| const y = acc.y || 0; | |
| const z = acc.z || 0; | |
| // Update display | |
| document.getElementById('accel-x').textContent = `X: ${x.toFixed(3)}`; | |
| document.getElementById('accel-y').textContent = `Y: ${y.toFixed(3)}`; | |
| document.getElementById('accel-z').textContent = `Z: ${z.toFixed(3)}`; | |
| document.getElementById('motion-status').textContent = `Status: β Motion sensors working! (${testCount} samples)`; | |
| testData.push({x, y, z}); | |
| if (testData.length >= 5) { | |
| window.removeEventListener('devicemotion', handleTestMotion); | |
| document.getElementById('motion-status').textContent = `Status: β Test complete - sensors working properly!`; | |
| } | |
| } else if (testCount >= maxTests) { | |
| window.removeEventListener('devicemotion', handleTestMotion); | |
| document.getElementById('motion-status').textContent = 'Status: β No motion data - check Safari settings'; | |
| showManualInstructions(); | |
| } | |
| } | |
| window.addEventListener('devicemotion', handleTestMotion); | |
| // Remove listener after 5 seconds if no data | |
| setTimeout(() => { | |
| if (testData.length === 0) { | |
| window.removeEventListener('devicemotion', handleTestMotion); | |
| document.getElementById('motion-status').textContent = 'Status: β No motion data received'; | |
| showManualInstructions(); | |
| } | |
| }, 5000); | |
| } | |
| function showManualInstructions() { | |
| document.getElementById('motion-status').innerHTML = ` | |
| Status: β Manual setup required<br> | |
| <small style="color: #dc3545;"> | |
| Go to Settings β Safari β Privacy & Security β Enable "Motion & Orientation Access"<br> | |
| Then refresh this page and try again. | |
| </small> | |
| `; | |
| } | |
| async function startIPadRecording(duration, rate) { | |
| motionDataBuffer = []; | |
| isRecording = true; | |
| recordingDuration = duration; | |
| sampleRate = rate; | |
| // Clear previous display | |
| document.getElementById('accel-x').textContent = 'X: 0.000'; | |
| document.getElementById('accel-y').textContent = 'Y: 0.000'; | |
| document.getElementById('accel-z').textContent = 'Z: 0.000'; | |
| document.getElementById('motion-status').textContent = 'Status: Starting recording...'; | |
| // Request permission if needed | |
| if (typeof DeviceMotionEvent.requestPermission === 'function' && !hasMotionPermission) { | |
| try { | |
| const permission = await DeviceMotionEvent.requestPermission(); | |
| if (permission === 'granted') { | |
| hasMotionPermission = true; | |
| startMotionCollection(); | |
| } else { | |
| document.getElementById('motion-status').textContent = 'Status: β Permission denied'; | |
| isRecording = false; | |
| return 'β Motion permission denied. Please enable in Safari settings.'; | |
| } | |
| } catch (error) { | |
| document.getElementById('motion-status').textContent = 'Status: β Permission error'; | |
| isRecording = false; | |
| return 'β Permission request failed: ' + error.message; | |
| } | |
| } else { | |
| startMotionCollection(); | |
| } | |
| return `π΄ Recording started for ${duration} seconds at ${rate} Hz`; | |
| } | |
| function startMotionCollection() { | |
| let lastSample = 0; | |
| let sampleInterval = 1000 / sampleRate; | |
| let sampleCount = 0; | |
| function handleMotion(event) { | |
| const now = Date.now(); | |
| if (now - lastSample < sampleInterval) return; | |
| lastSample = now; | |
| const acceleration = event.accelerationIncludingGravity; | |
| if (acceleration && isRecording) { | |
| const x = acceleration.x || 0; | |
| const y = acceleration.y || 0; | |
| const z = acceleration.z || 0; | |
| sampleCount++; | |
| // Update display | |
| document.getElementById('accel-x').textContent = `X: ${x.toFixed(3)}`; | |
| document.getElementById('accel-y').textContent = `Y: ${y.toFixed(3)}`; | |
| document.getElementById('accel-z').textContent = `Z: ${z.toFixed(3)}`; | |
| document.getElementById('motion-status').textContent = `Status: π΄ Recording... (${sampleCount} samples)`; | |
| // Store data | |
| motionDataBuffer.push({ | |
| timestamp: new Date().toISOString(), | |
| x: x, | |
| y: y, | |
| z: z, | |
| device_id: 'ipad_' + Date.now(), | |
| user_agent: navigator.userAgent, | |
| orientation: screen.orientation ? screen.orientation.type : 'unknown' | |
| }); | |
| } | |
| } | |
| window.addEventListener('devicemotion', handleMotion); | |
| // Auto-stop after duration | |
| setTimeout(() => { | |
| isRecording = false; | |
| window.removeEventListener('devicemotion', handleMotion); | |
| document.getElementById('motion-status').textContent = `Status: βΉοΈ Recording complete (${motionDataBuffer.length} samples)`; | |
| // Process the collected data | |
| if (motionDataBuffer.length > 0) { | |
| console.log('Collected', motionDataBuffer.length, 'samples'); | |
| // Here you would send the data to the Gradio backend | |
| } else { | |
| document.getElementById('motion-status').textContent = 'Status: β No data collected'; | |
| } | |
| }, recordingDuration * 1000); | |
| } | |
| function stopIPadRecording() { | |
| isRecording = false; | |
| document.getElementById('motion-status').textContent = `Status: βΉοΈ Manually stopped (${motionDataBuffer.length} samples)`; | |
| return `βΉοΈ Recording stopped. ${motionDataBuffer.length} samples collected`; | |
| } | |
| </script> | |
| """) | |
| start_btn.click( | |
| app.start_recording_session, | |
| inputs=[duration, sample_rate], | |
| outputs=[recording_status] | |
| ) | |
| stop_btn.click( | |
| app.stop_recording_session, | |
| outputs=[recording_status] | |
| ) | |
| with gr.Tab("π Data Preview"): | |
| with gr.Row(): | |
| refresh_btn = gr.Button("π Refresh Data Preview", variant="secondary") | |
| data_preview = gr.Markdown("No data recorded yet from iPad") | |
| refresh_btn.click( | |
| app.get_data_preview, | |
| outputs=[data_preview] | |
| ) | |
| with gr.Tab("βοΈ Upload to S3"): | |
| gr.Markdown("### Upload iPad Motion Data to S3") | |
| with gr.Row(): | |
| bucket_name = gr.Textbox( | |
| label="S3 Bucket Name", | |
| placeholder="my-ipad-motion-bucket" | |
| ) | |
| file_prefix = gr.Textbox( | |
| label="File Prefix (optional)", | |
| placeholder="ipad_sensors/", | |
| value="" | |
| ) | |
| upload_btn = gr.Button("βοΈ Upload to S3", variant="primary") | |
| upload_status = gr.Textbox(label="Upload Status", interactive=False) | |
| upload_btn.click( | |
| app.upload_to_s3, | |
| inputs=[bucket_name, file_prefix], | |
| outputs=[upload_status] | |
| ) | |
| with gr.Tab("π Instructions"): | |
| gr.Markdown(""" | |
| ## iPad Motion Sensor Setup | |
| ### 1. System Requirements | |
| - **iPad**: Any modern iPad with built-in accelerometer | |
| - **Browser**: Safari (recommended) or Chrome on iOS | |
| - **iOS Version**: iOS 13+ for permission prompts | |
| - **Network**: Internet connection for S3 upload | |
| ### 2. Motion Sensor Access | |
| The app uses the **Device Motion API** which provides: | |
| - **Acceleration**: Device movement in 3D space | |
| - **Real-time data**: Up to 60Hz sample rate | |
| - **Cross-platform**: Works on any modern iPad | |
| ### 3. Data Collection Tips | |
| - **Secure grip**: Hold your iPad firmly while recording | |
| - **Varied motions**: Try tilting, rotating, and gentle shaking | |
| - **Stay in Safari**: Don't switch apps during recording | |
| - **Grant permissions**: Allow motion access when prompted | |
| ### 4. Understanding iPad Accelerometer Data | |
| - **X-axis**: Left (-) to Right (+) when holding in landscape | |
| - **Y-axis**: Bottom (-) to Top (+) of the screen | |
| - **Z-axis**: Back (-) to Front (+) of the device | |
| - **Units**: Measured in m/sΒ² (meters per second squared) | |
| - **Gravity effect**: ~9.8 m/sΒ² when stationary | |
| ### 5. Privacy & Security | |
| - **Motion data**: Only collected when recording is active | |
| - **Local processing**: Data stays in your browser until upload | |
| - **AWS S3**: Secure cloud storage with your credentials | |
| - **No tracking**: App doesn't store personal information | |
| ### 6. Troubleshooting | |
| **"Motion permission not prompted":** | |
| - **iOS 13+**: The permission prompt only appears when triggered by user interaction | |
| - **Solution 1**: Use the "Request Motion Permission" button in Setup tab | |
| - **Solution 2**: Go to iPad Settings β Safari β Privacy & Security β Enable "Motion & Orientation Access" | |
| - **Solution 3**: Try the "Test Motion Access" button in the recording section | |
| - **Important**: Permission prompts only work on HTTPS websites or localhost | |
| **"Permission button doesn't work":** | |
| - Clear Safari cache and cookies for this site | |
| - Make sure you're using Safari (not Chrome or other browsers) | |
| - Check that the website is served over HTTPS | |
| - Try refreshing the page and clicking the button again | |
| **"Motion sensors not supported":** | |
| - Use Safari browser instead of Chrome | |
| - Check iPad model (very old iPads might not be supported) | |
| - Restart Safari and try again | |
| **"Permission denied":** | |
| - Go to Safari Settings > Privacy & Security | |
| - Enable Motion & Orientation Access | |
| - Refresh the page and try again | |
| **"No data collected":** | |
| - Make sure to move the iPad during recording | |
| - Check that Safari is the active app | |
| - Try longer recording duration (15+ seconds) | |
| ### 7. Data Format | |
| Each uploaded JSON file contains: | |
| ```json | |
| { | |
| "device_type": "iPad", | |
| "device_id": "ipad_timestamp", | |
| "recording_info": { | |
| "start_time": "2025-01-01T12:00:00Z", | |
| "sample_count": 300, | |
| "data_source": "device_motion_api" | |
| }, | |
| "data": [ | |
| { | |
| "timestamp": "2025-01-01T12:00:00.123Z", | |
| "x": 0.123456, | |
| "y": -0.654321, | |
| "z": 9.234567, | |
| "device_id": "ipad_1234567890", | |
| "user_agent": "iPad Safari info", | |
| "orientation": "landscape-primary" | |
| } | |
| ] | |
| } | |
| ``` | |
| ### 8. Use Cases | |
| - **Gesture recognition**: Train ML models on iPad movements | |
| - **Activity tracking**: Monitor device usage patterns | |
| - **Game development**: Capture motion for game controls | |
| - **Research**: Study human-computer interaction | |
| - **Accessibility**: Develop motion-based interfaces | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| share=True, | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| show_api=False, | |
| favicon_path=None | |
| ) |