Feat: rnd file viewer built

This commit is contained in:
serversdwn
2026-01-19 21:49:10 +00:00
parent ff38b74548
commit 4c213c96ee
3 changed files with 634 additions and 2 deletions

View File

@@ -644,6 +644,8 @@ async def ftp_download_to_server(
'.flac': 'audio',
'.m4a': 'audio',
'.aac': 'audio',
# Sound level meter measurement files
'.rnd': 'measurement',
# Data files
'.csv': 'data',
'.txt': 'data',
@@ -1023,6 +1025,176 @@ async def download_project_file(
)
@router.get("/{project_id}/files/{file_id}/view-rnd", response_class=HTMLResponse)
async def view_rnd_file(
request: Request,
project_id: str,
file_id: str,
db: Session = Depends(get_db),
):
"""
View an RND (sound level meter measurement) file.
Returns a dedicated page with data table and charts.
"""
from backend.models import DataFile
from pathlib import Path
# Get the file record
file_record = db.query(DataFile).filter_by(id=file_id).first()
if not file_record:
raise HTTPException(status_code=404, detail="File not found")
# Verify file belongs to this project
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
if not session or session.project_id != project_id:
raise HTTPException(status_code=403, detail="File does not belong to this project")
# Build full file path
file_path = Path("data") / file_record.file_path
if not file_path.exists():
raise HTTPException(status_code=404, detail="File not found on disk")
# Get project info
project = db.query(Project).filter_by(id=project_id).first()
# Get location info if available
location = None
if session.location_id:
location = db.query(MonitoringLocation).filter_by(id=session.location_id).first()
# Get unit info if available
unit = None
if session.unit_id:
unit = db.query(RosterUnit).filter_by(id=session.unit_id).first()
# Parse file metadata
metadata = {}
if file_record.file_metadata:
try:
metadata = json.loads(file_record.file_metadata)
except json.JSONDecodeError:
pass
return templates.TemplateResponse("rnd_viewer.html", {
"request": request,
"project": project,
"project_id": project_id,
"file": file_record,
"file_id": file_id,
"session": session,
"location": location,
"unit": unit,
"metadata": metadata,
"filename": file_path.name,
})
@router.get("/{project_id}/files/{file_id}/rnd-data")
async def get_rnd_data(
project_id: str,
file_id: str,
db: Session = Depends(get_db),
):
"""
Get parsed RND file data as JSON.
Returns the measurement data for charts and tables.
"""
from backend.models import DataFile
from pathlib import Path
import csv
import io
# Get the file record
file_record = db.query(DataFile).filter_by(id=file_id).first()
if not file_record:
raise HTTPException(status_code=404, detail="File not found")
# Verify file belongs to this project
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
if not session or session.project_id != project_id:
raise HTTPException(status_code=403, detail="File does not belong to this project")
# Build full file path
file_path = Path("data") / file_record.file_path
if not file_path.exists():
raise HTTPException(status_code=404, detail="File not found on disk")
# Read and parse the RND file
try:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
# Parse as CSV
reader = csv.DictReader(io.StringIO(content))
rows = []
headers = []
for row in reader:
if not headers:
headers = list(row.keys())
# Clean up values - strip whitespace and handle special values
cleaned_row = {}
for key, value in row.items():
if key: # Skip empty keys
cleaned_key = key.strip()
cleaned_value = value.strip() if value else ''
# Convert numeric values
if cleaned_value and cleaned_value not in ['-.-', '-', '']:
try:
cleaned_value = float(cleaned_value)
except ValueError:
pass
elif cleaned_value in ['-.-', '-']:
cleaned_value = None
cleaned_row[cleaned_key] = cleaned_value
rows.append(cleaned_row)
# Detect file type (Leq vs Lp) based on columns
file_type = 'unknown'
if headers:
header_str = ','.join(headers).lower()
if 'leq' in header_str:
file_type = 'leq' # Time-averaged data
elif 'lp(main)' in header_str or 'lp (main)' in header_str:
file_type = 'lp' # Instantaneous data
# Get summary statistics
summary = {
"total_rows": len(rows),
"file_type": file_type,
"headers": [h.strip() for h in headers if h.strip()],
}
# Calculate min/max/avg for key metrics if available
metrics_to_summarize = ['Leq(Main)', 'Lmax(Main)', 'Lmin(Main)', 'Lpeak(Main)', 'Lp(Main)']
for metric in metrics_to_summarize:
values = [row.get(metric) for row in rows if isinstance(row.get(metric), (int, float))]
if values:
summary[f"{metric}_min"] = min(values)
summary[f"{metric}_max"] = max(values)
summary[f"{metric}_avg"] = sum(values) / len(values)
# Get time range
if rows:
first_time = rows[0].get('Start Time', '')
last_time = rows[-1].get('Start Time', '')
summary['time_start'] = first_time
summary['time_end'] = last_time
return {
"success": True,
"summary": summary,
"headers": summary["headers"],
"data": rows,
}
except Exception as e:
logger.error(f"Error parsing RND file: {e}")
raise HTTPException(status_code=500, detail=f"Error parsing file: {str(e)}")
@router.get("/types/list", response_class=HTMLResponse)
async def get_project_types(request: Request, db: Session = Depends(get_db)):
"""