327 lines
12 KiB
Python
327 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
from flask import Flask, jsonify, request
|
|
from flask_cors import CORS
|
|
import json
|
|
import h5py
|
|
import numpy as np
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
|
|
# Load index once at startup
|
|
with open('/data/index.json', 'r') as f:
|
|
INDEX = json.load(f)
|
|
|
|
|
|
def to_python_type(val):
|
|
"""Convert numpy types to Python types for JSON serialization"""
|
|
if hasattr(val, 'item'):
|
|
return val.item()
|
|
return val
|
|
|
|
H5_DIR = Path('/data/h5')
|
|
|
|
@app.route('/api/nodes', methods=['GET'])
|
|
def get_nodes():
|
|
nodes_list = []
|
|
for node_id, node_info in INDEX['nodes'].items():
|
|
file_count = len(node_info.get('files', []))
|
|
nodes_list.append({
|
|
'id': node_id,
|
|
'position': node_info.get('position', {}),
|
|
'file_count': file_count,
|
|
'hasDates': file_count > 0
|
|
})
|
|
return jsonify({
|
|
'nodes': nodes_list,
|
|
'sampleRateHz': 500
|
|
})
|
|
|
|
@app.route('/api/dates', methods=['GET'])
|
|
def get_dates():
|
|
return jsonify({'dates': INDEX['dates']})
|
|
|
|
@app.route('/api/migration-status', methods=['GET'])
|
|
def get_migration_status():
|
|
"""Status de la conversion RAW -> H5"""
|
|
total_files = 345 # Connu du projet
|
|
h5_files = list(H5_DIR.glob('*.h5'))
|
|
converted = len(h5_files)
|
|
|
|
return jsonify({
|
|
'summary': {
|
|
'total_files': total_files,
|
|
'converted_files': converted,
|
|
'percentage': round(converted / total_files * 100, 1),
|
|
'status': 'in_progress' if converted < total_files else 'complete'
|
|
},
|
|
'h5_files_available': converted
|
|
})
|
|
|
|
@app.route('/api/rms-timeline', methods=['GET'])
|
|
def get_rms_timeline():
|
|
"""Timeline RMS optimisé - utilise l'INDEX pour filtrer par date"""
|
|
date = request.args.get('date')
|
|
channel = request.args.get('channel', 'ch0')
|
|
window_sec = int(request.args.get('window', 60))
|
|
|
|
if not date:
|
|
return jsonify({'error': 'date parameter required'}), 400
|
|
|
|
channel_num = int(channel.replace('ch', '').replace('CH', ''))
|
|
nodes_data = {}
|
|
|
|
# Utiliser l'INDEX pour trouver les fichiers de cette date
|
|
for node_id, node_info in INDEX['nodes'].items():
|
|
for file_info in node_info.get('files', []):
|
|
if file_info.get('date') != date:
|
|
continue
|
|
|
|
filename = file_info.get('filename')
|
|
h5_path = H5_DIR / filename
|
|
if not h5_path.exists():
|
|
continue
|
|
|
|
try:
|
|
with h5py.File(h5_path, 'r') as f:
|
|
sample_rate = to_python_type(f['metadata'].attrs.get('sample_rate_hz', 500))
|
|
start_ts = file_info.get('timestamp', 0)
|
|
|
|
dataset_name = f'calibrated_data/channel_{channel_num + 1}'
|
|
if dataset_name not in f:
|
|
continue
|
|
|
|
dataset = f[dataset_name]
|
|
total_samples = dataset.shape[0]
|
|
window_samples = int(window_sec * sample_rate)
|
|
|
|
# Calculer RMS par fenêtre (max 100 points par fichier)
|
|
timeline = []
|
|
step = max(window_samples, total_samples // 100)
|
|
for i in range(0, min(total_samples, step * 100), step):
|
|
end_idx = min(i + window_samples, total_samples)
|
|
chunk = dataset[i:end_idx]
|
|
rms = float(np.sqrt(np.mean(chunk ** 2)))
|
|
ts = start_ts + (i / sample_rate)
|
|
timeline.append({'ts': ts, 'rms': rms})
|
|
|
|
if node_id not in nodes_data:
|
|
nodes_data[node_id] = []
|
|
nodes_data[node_id].extend(timeline)
|
|
except Exception as e:
|
|
continue
|
|
|
|
return jsonify({
|
|
'date': date,
|
|
'channel': channel,
|
|
'nodes': nodes_data
|
|
})
|
|
|
|
@app.route('/api/h5/data', methods=['GET'])
|
|
@app.route('/api/data', methods=['GET'])
|
|
def get_waveform_data():
|
|
"""Données waveform - accepte soit file= soit node=+date="""
|
|
filename = request.args.get('file')
|
|
node_id = request.args.get('node')
|
|
date = request.args.get('date')
|
|
channel = request.args.get('channel', 'ch0')
|
|
start = float(request.args.get('start', 0))
|
|
duration = float(request.args.get('duration', 10))
|
|
|
|
# Trouver le fichier H5
|
|
h5_file = None
|
|
if filename:
|
|
# Mode direct: fichier spécifié
|
|
h5_file = H5_DIR / filename
|
|
elif node_id and date:
|
|
# Mode lookup: chercher via node_id et date
|
|
node_info = INDEX['nodes'].get(node_id)
|
|
if node_info:
|
|
for file_info in node_info.get('files', []):
|
|
if file_info.get('date') == date:
|
|
h5_file = H5_DIR / file_info.get('filename', '')
|
|
break
|
|
else:
|
|
return jsonify({'error': 'file or (node and date) required'}), 400
|
|
|
|
if not h5_file or not h5_file.exists():
|
|
return jsonify({'error': 'H5 file not found', 'path': str(h5_file)}), 404
|
|
|
|
try:
|
|
with h5py.File(h5_file, 'r') as f:
|
|
sample_rate = f['metadata'].attrs['sample_rate_hz']
|
|
channel_num = int(channel.replace('ch', '').replace('CH', ''))
|
|
|
|
# Lire les données calibrées
|
|
dataset = f[f'calibrated_data/channel_{channel_num + 1}']
|
|
start_sample = int(start * sample_rate)
|
|
end_sample = int((start + duration) * sample_rate)
|
|
|
|
# Limiter à la taille du dataset
|
|
end_sample = min(end_sample, dataset.shape[0])
|
|
|
|
if start_sample >= dataset.shape[0]:
|
|
return jsonify({'error': 'start time out of range'}), 400
|
|
|
|
data = dataset[start_sample:end_sample]
|
|
|
|
# Calculer stats
|
|
rms = float(np.sqrt(np.mean(data ** 2)))
|
|
peak = float(np.max(np.abs(data)))
|
|
|
|
return jsonify({
|
|
'node_id': node_id,
|
|
'date': date,
|
|
'channel': channel,
|
|
'start': start,
|
|
'duration': duration,
|
|
'sample_rate': to_python_type(sample_rate),
|
|
'data': data.tolist(),
|
|
'stats': {
|
|
'rms': rms,
|
|
'peak': peak,
|
|
'samples': len(data)
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/h5/files', methods=['GET'])
|
|
def get_h5_files():
|
|
"""Liste des fichiers H5 disponibles avec métadonnées"""
|
|
files = []
|
|
for h5_path in H5_DIR.glob('*.h5'):
|
|
try:
|
|
with h5py.File(h5_path, 'r') as f:
|
|
filename = h5_path.name
|
|
# Parse: auto_228_064210_b50_rsn51848_seq1_1597256250.h5
|
|
parts = filename.replace('.h5', '').split('_')
|
|
node_id = parts[1] if len(parts) > 1 else 'unknown'
|
|
|
|
# Find date from INDEX
|
|
date = 'unknown'
|
|
node_info = INDEX['nodes'].get(node_id)
|
|
if node_info:
|
|
for file_info in node_info.get('files', []):
|
|
if file_info.get('filename') == filename:
|
|
date = file_info.get('date', 'unknown')
|
|
break
|
|
|
|
files.append({
|
|
'filename': filename,
|
|
'nodeId': node_id,
|
|
'date': date,
|
|
'size_mb': round(h5_path.stat().st_size / 1024 / 1024, 2),
|
|
'duration_sec': to_python_type(f['metadata'].attrs.get('duration_sec', 0)),
|
|
'sample_rate': to_python_type(f['metadata'].attrs.get('sample_rate_hz', 500)),
|
|
'channels': to_python_type(f['metadata'].attrs.get('n_channels', 4))
|
|
})
|
|
except Exception as e:
|
|
pass
|
|
|
|
return jsonify({'files': files, 'count': len(files)})
|
|
|
|
@app.route('/api/h5/coverage', methods=['GET'])
|
|
def get_h5_coverage():
|
|
"""Matrice de couverture nodes x dates"""
|
|
coverage = {}
|
|
for node_id, node_info in INDEX['nodes'].items():
|
|
node_dates = [f['date'] for f in node_info.get('files', []) if 'date' in f]
|
|
coverage[node_id] = node_dates
|
|
|
|
return jsonify({
|
|
'coverage': coverage,
|
|
'total_nodes': len(coverage),
|
|
'total_dates': len(INDEX['dates'])
|
|
})
|
|
|
|
@app.route('/api/h5/gaps', methods=['GET'])
|
|
def get_h5_gaps():
|
|
"""Identify gaps in H5 data coverage per node"""
|
|
gaps = []
|
|
|
|
for node_id, node_info in INDEX['nodes'].items():
|
|
files = sorted(node_info.get('files', []), key=lambda x: x.get('timestamp', 0))
|
|
|
|
for i in range(len(files) - 1):
|
|
current = files[i]
|
|
next_file = files[i + 1]
|
|
|
|
current_end = current.get('timestamp', 0) + current.get('duration_sec', 0)
|
|
next_start = next_file.get('timestamp', 0)
|
|
|
|
gap_seconds = next_start - current_end
|
|
|
|
# Report gaps > 1 hour
|
|
if gap_seconds > 3600:
|
|
gaps.append({
|
|
'node_id': node_id,
|
|
'gap_start': current_end,
|
|
'gap_end': next_start,
|
|
'gap_hours': round(gap_seconds / 3600, 2),
|
|
'before_file': current.get('filename'),
|
|
'after_file': next_file.get('filename')
|
|
})
|
|
|
|
return jsonify({
|
|
'gaps': gaps,
|
|
'total_gaps': len(gaps)
|
|
})
|
|
|
|
@app.route('/api/chat', methods=['POST'])
|
|
def chat():
|
|
"""Endpoint chat assistant (mock)"""
|
|
data = request.json
|
|
message = data.get('message', '')
|
|
|
|
return jsonify({
|
|
'response': f"[Mock] Vous avez dit: {message}",
|
|
'timestamp': datetime.now().isoformat()
|
|
})
|
|
|
|
|
|
@app.route('/api/docs/manifest', methods=['GET'])
|
|
def get_docs_manifest():
|
|
"""Manifest de la documentation de campagne"""
|
|
return jsonify({
|
|
'campaign': {
|
|
'name': 'SeaKESP Sète 2020',
|
|
'start_date': '2020-07-11',
|
|
'end_date': '2020-09-22',
|
|
'duration_days': 46,
|
|
'location': 'Sète, Méditerranée, France',
|
|
'coordinates': {'lat': 43.40, 'lon': 3.70}
|
|
},
|
|
'timeline': [
|
|
{'date': '2020-07-11', 'event': 'Début du déploiement', 'type': 'deployment'},
|
|
{'date': '2020-08-08', 'event': 'Premiers enregistrements', 'type': 'recording'},
|
|
{'date': '2020-08-12', 'event': 'Campagne principale', 'type': 'recording'},
|
|
{'date': '2020-08-16', 'event': 'Fin campagne principale', 'type': 'recording'},
|
|
{'date': '2020-09-22', 'event': 'Récupération équipements', 'type': 'recovery'}
|
|
],
|
|
'documents': [
|
|
{'name': 'Rapport Gandalf', 'type': 'report', 'available': False},
|
|
{'name': 'SPS Preplots', 'type': 'technical', 'available': False},
|
|
{'name': 'Paramètres acquisition', 'type': 'technical', 'available': False}
|
|
],
|
|
'stats': {
|
|
'total_nodes': len(INDEX['nodes']),
|
|
'total_dates': len(INDEX['dates']),
|
|
'total_files': INDEX.get('total_files', 0),
|
|
'sample_rate_hz': 500,
|
|
'channels': 4
|
|
}
|
|
})
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
def health():
|
|
return jsonify({'status': 'ok', 'nodes': len(INDEX['nodes']), 'dates': len(INDEX['dates'])})
|
|
|
|
if __name__ == '__main__':
|
|
print(f"Loaded {len(INDEX['nodes'])} nodes, {len(INDEX['dates'])} dates")
|
|
print(f"H5 directory: {H5_DIR}")
|
|
app.run(host='0.0.0.0', port=3004)
|