API built for most common commands

This commit is contained in:
serversdwn
2025-12-24 06:18:42 +00:00
parent 316cfa84f8
commit 60c95e825d
8 changed files with 1172 additions and 38 deletions

406
API.md Normal file
View File

@@ -0,0 +1,406 @@
# SLMM API Documentation
REST API for controlling Rion NL-43/NL-53 Sound Level Meters via TCP and FTP.
Base URL: `http://localhost:8000/api/nl43`
All endpoints require a `unit_id` parameter identifying the device.
## Device Configuration
### Get Device Config
```
GET /{unit_id}/config
```
Returns the device configuration including host, port, and enabled protocols.
**Response:**
```json
{
"status": "ok",
"data": {
"unit_id": "nl43-1",
"host": "192.168.1.100",
"tcp_port": 2255,
"tcp_enabled": true,
"ftp_enabled": false,
"web_enabled": false
}
}
```
### Update Device Config
```
PUT /{unit_id}/config
```
Update device configuration.
**Request Body:**
```json
{
"host": "192.168.1.100",
"tcp_port": 2255,
"tcp_enabled": true,
"ftp_enabled": false,
"ftp_username": "admin",
"ftp_password": "password",
"web_enabled": false
}
```
## Device Status
### Get Cached Status
```
GET /{unit_id}/status
```
Returns the last cached measurement snapshot from the database.
### Get Live Status
```
GET /{unit_id}/live
```
Requests fresh DOD (Display On Demand) data from the device and returns current measurements.
**Response:**
```json
{
"status": "ok",
"data": {
"unit_id": "nl43-1",
"measurement_state": "Measure",
"lp": "65.2",
"leq": "68.4",
"lmax": "82.1",
"lmin": "42.3",
"lpeak": "89.5",
"battery_level": "80",
"power_source": "Battery",
"sd_remaining_mb": "2048",
"sd_free_ratio": "50"
}
}
```
### Stream Live Data (WebSocket)
```
WS /{unit_id}/live
```
Opens a WebSocket connection and streams continuous DRD (Display Real-time Data) from the device.
## Measurement Control
### Start Measurement
```
POST /{unit_id}/start
```
Starts measurement on the device.
### Stop Measurement
```
POST /{unit_id}/stop
```
Stops measurement on the device.
### Pause Measurement
```
POST /{unit_id}/pause
```
Pauses the current measurement.
### Resume Measurement
```
POST /{unit_id}/resume
```
Resumes a paused measurement.
### Reset Measurement
```
POST /{unit_id}/reset
```
Resets the measurement data.
### Manual Store
```
POST /{unit_id}/store
```
Manually stores the current measurement data.
## Device Information
### Get Battery Level
```
GET /{unit_id}/battery
```
Returns the battery level.
**Response:**
```json
{
"status": "ok",
"battery_level": "80"
}
```
### Get Clock
```
GET /{unit_id}/clock
```
Returns the device clock time.
**Response:**
```json
{
"status": "ok",
"clock": "2025/12/24,02:30:15"
}
```
### Set Clock
```
PUT /{unit_id}/clock
```
Sets the device clock time.
**Request Body:**
```json
{
"datetime": "2025/12/24,02:30:15"
}
```
## Measurement Settings
### Get Frequency Weighting
```
GET /{unit_id}/frequency-weighting?channel=Main
```
Gets the frequency weighting (A, C, or Z) for a channel.
**Query Parameters:**
- `channel` (optional): Main, Sub1, Sub2, or Sub3 (default: Main)
**Response:**
```json
{
"status": "ok",
"frequency_weighting": "A",
"channel": "Main"
}
```
### Set Frequency Weighting
```
PUT /{unit_id}/frequency-weighting
```
Sets the frequency weighting.
**Request Body:**
```json
{
"weighting": "A",
"channel": "Main"
}
```
### Get Time Weighting
```
GET /{unit_id}/time-weighting?channel=Main
```
Gets the time weighting (F, S, or I) for a channel.
**Query Parameters:**
- `channel` (optional): Main, Sub1, Sub2, or Sub3 (default: Main)
**Response:**
```json
{
"status": "ok",
"time_weighting": "F",
"channel": "Main"
}
```
### Set Time Weighting
```
PUT /{unit_id}/time-weighting
```
Sets the time weighting.
**Request Body:**
```json
{
"weighting": "F",
"channel": "Main"
}
```
**Values:**
- `F` - Fast (125ms)
- `S` - Slow (1s)
- `I` - Impulse (35ms)
## FTP File Management
### Enable FTP
```
POST /{unit_id}/ftp/enable
```
Enables FTP server on the device.
**Note:** FTP and TCP are mutually exclusive. Enabling FTP will temporarily disable TCP control.
### Disable FTP
```
POST /{unit_id}/ftp/disable
```
Disables FTP server on the device.
### Get FTP Status
```
GET /{unit_id}/ftp/status
```
Checks if FTP is enabled on the device.
**Response:**
```json
{
"status": "ok",
"ftp_status": "On",
"ftp_enabled": true
}
```
### List Files
```
GET /{unit_id}/ftp/files?path=/
```
Lists files and directories at the specified path.
**Query Parameters:**
- `path` (optional): Directory path to list (default: /)
**Response:**
```json
{
"status": "ok",
"path": "/NL43_DATA/",
"count": 3,
"files": [
{
"name": "measurement_001.wav",
"path": "/NL43_DATA/measurement_001.wav",
"size": 102400,
"modified": "Dec 24 2025",
"is_dir": false
},
{
"name": "folder1",
"path": "/NL43_DATA/folder1",
"size": 0,
"modified": "Dec 23 2025",
"is_dir": true
}
]
}
```
### Download File
```
POST /{unit_id}/ftp/download
```
Downloads a file from the device via FTP.
**Request Body:**
```json
{
"remote_path": "/NL43_DATA/measurement_001.wav"
}
```
**Response:**
Returns the file as a binary download with appropriate `Content-Disposition` header.
## Error Responses
All endpoints return standard HTTP status codes:
- `200` - Success
- `404` - Device config not found
- `403` - TCP communication is disabled
- `502` - Failed to communicate with device
- `504` - Device communication timeout
- `500` - Internal server error
**Error Response Format:**
```json
{
"detail": "Error message"
}
```
## Common Patterns
### Terra-view Integration Example
```javascript
// Get live status from all devices
const devices = ['nl43-1', 'nl43-2', 'nl43-3'];
const statuses = await Promise.all(
devices.map(id =>
fetch(`http://localhost:8000/api/nl43/${id}/live`)
.then(r => r.json())
)
);
// Start measurement on all devices
await Promise.all(
devices.map(id =>
fetch(`http://localhost:8000/api/nl43/${id}/start`, { method: 'POST' })
)
);
// Download latest files from all devices
for (const device of devices) {
// Enable FTP
await fetch(`http://localhost:8000/api/nl43/${device}/ftp/enable`, {
method: 'POST'
});
// List files
const res = await fetch(`http://localhost:8000/api/nl43/${device}/ftp/files?path=/NL43_DATA`);
const { files } = await res.json();
// Download latest file
const latestFile = files
.filter(f => !f.is_dir)
.sort((a, b) => b.modified - a.modified)[0];
if (latestFile) {
const download = await fetch(`http://localhost:8000/api/nl43/${device}/ftp/download`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ remote_path: latestFile.path })
});
const blob = await download.blob();
// Process blob...
}
// Disable FTP, re-enable TCP
await fetch(`http://localhost:8000/api/nl43/${device}/ftp/disable`, {
method: 'POST'
});
}
```
## Rate Limiting
The NL43 protocol requires ≥1 second between commands to the same device. The API automatically enforces this rate limit.
## Notes
- TCP and FTP protocols are mutually exclusive on the device
- FTP uses active mode (requires device to connect back to server)
- WebSocket streaming keeps a persistent connection - limit concurrent streams
- All measurements are stored in the database for quick access via `/status` endpoint

View File

@@ -258,6 +258,208 @@ async def manual_store(unit_id: str, db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/{unit_id}/pause")
async def pause_measurement(unit_id: str, db: Session = Depends(get_db)):
"""Pause the current measurement."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.pause()
logger.info(f"Paused measurement on unit {unit_id}")
return {"status": "ok", "message": "Measurement paused"}
except Exception as e:
logger.error(f"Failed to pause measurement on {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.post("/{unit_id}/resume")
async def resume_measurement(unit_id: str, db: Session = Depends(get_db)):
"""Resume a paused measurement."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.resume()
logger.info(f"Resumed measurement on unit {unit_id}")
return {"status": "ok", "message": "Measurement resumed"}
except Exception as e:
logger.error(f"Failed to resume measurement on {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.post("/{unit_id}/reset")
async def reset_measurement(unit_id: str, db: Session = Depends(get_db)):
"""Reset the measurement data."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.reset()
logger.info(f"Reset measurement data on unit {unit_id}")
return {"status": "ok", "message": "Measurement data reset"}
except Exception as e:
logger.error(f"Failed to reset measurement on {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/battery")
async def get_battery(unit_id: str, db: Session = Depends(get_db)):
"""Get battery level."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
level = await client.get_battery_level()
return {"status": "ok", "battery_level": level}
except Exception as e:
logger.error(f"Failed to get battery level for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/clock")
async def get_clock(unit_id: str, db: Session = Depends(get_db)):
"""Get device clock time."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
clock = await client.get_clock()
return {"status": "ok", "clock": clock}
except Exception as e:
logger.error(f"Failed to get clock for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
class ClockPayload(BaseModel):
datetime: str # Format: YYYY/MM/DD,HH:MM:SS
@router.put("/{unit_id}/clock")
async def set_clock(unit_id: str, payload: ClockPayload, db: Session = Depends(get_db)):
"""Set device clock time."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_clock(payload.datetime)
return {"status": "ok", "message": f"Clock set to {payload.datetime}"}
except Exception as e:
logger.error(f"Failed to set clock for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
class WeightingPayload(BaseModel):
weighting: str
channel: str = "Main"
@router.get("/{unit_id}/frequency-weighting")
async def get_frequency_weighting(unit_id: str, channel: str = "Main", db: Session = Depends(get_db)):
"""Get frequency weighting (A, C, Z)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
weighting = await client.get_frequency_weighting(channel)
return {"status": "ok", "frequency_weighting": weighting, "channel": channel}
except Exception as e:
logger.error(f"Failed to get frequency weighting for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/frequency-weighting")
async def set_frequency_weighting(unit_id: str, payload: WeightingPayload, db: Session = Depends(get_db)):
"""Set frequency weighting (A, C, Z)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_frequency_weighting(payload.weighting, payload.channel)
return {"status": "ok", "message": f"Frequency weighting set to {payload.weighting} on {payload.channel}"}
except Exception as e:
logger.error(f"Failed to set frequency weighting for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/time-weighting")
async def get_time_weighting(unit_id: str, channel: str = "Main", db: Session = Depends(get_db)):
"""Get time weighting (F, S, I)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
weighting = await client.get_time_weighting(channel)
return {"status": "ok", "time_weighting": weighting, "channel": channel}
except Exception as e:
logger.error(f"Failed to get time weighting for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/time-weighting")
async def set_time_weighting(unit_id: str, payload: WeightingPayload, db: Session = Depends(get_db)):
"""Set time weighting (F, S, I)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_time_weighting(payload.weighting, payload.channel)
return {"status": "ok", "message": f"Time weighting set to {payload.weighting} on {payload.channel}"}
except Exception as e:
logger.error(f"Failed to set time weighting for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/live")
async def live_status(unit_id: str, db: Session = Depends(get_db)):
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
@@ -292,6 +494,33 @@ async def live_status(unit_id: str, db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{unit_id}/results")
async def get_results(unit_id: str, db: Session = Depends(get_db)):
"""Get final calculation results (DLC) from the last measurement."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
results = await client.request_dlc()
logger.info(f"Retrieved measurement results for unit {unit_id}")
return {"status": "ok", "data": results}
except ConnectionError as e:
logger.error(f"Failed to get results for {unit_id}: {e}")
raise HTTPException(status_code=502, detail="Failed to communicate with device")
except TimeoutError:
logger.error(f"Timeout getting results for {unit_id}")
raise HTTPException(status_code=504, detail="Device communication timeout")
except Exception as e:
logger.error(f"Unexpected error getting results for {unit_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.websocket("/{unit_id}/stream")
async def stream_live(websocket: WebSocket, unit_id: str):
"""WebSocket endpoint for real-time DRD streaming from NL43 device.

View File

@@ -13,7 +13,8 @@ from dataclasses import dataclass
from datetime import datetime
from typing import Optional, List
from sqlalchemy.orm import Session
import aioftp
from ftplib import FTP
from pathlib import Path
from app.models import NL43Status
@@ -237,6 +238,102 @@ class NL43Client:
await self._send_command("Manual Store,Start\r\n")
logger.info(f"Manual store executed on {self.device_key}")
async def pause(self):
"""Pause the current measurement."""
await self._send_command("Pause,On\r\n")
logger.info(f"Measurement paused on {self.device_key}")
async def resume(self):
"""Resume a paused measurement."""
await self._send_command("Pause,Off\r\n")
logger.info(f"Measurement resumed on {self.device_key}")
async def reset(self):
"""Reset the measurement data."""
await self._send_command("Reset\r\n")
logger.info(f"Measurement data reset on {self.device_key}")
async def get_battery_level(self) -> str:
"""Get the battery level."""
resp = await self._send_command("Battery Level?\r\n")
logger.info(f"Battery level on {self.device_key}: {resp}")
return resp.strip()
async def get_clock(self) -> str:
"""Get the device clock time."""
resp = await self._send_command("Clock?\r\n")
logger.info(f"Clock on {self.device_key}: {resp}")
return resp.strip()
async def set_clock(self, datetime_str: str):
"""Set the device clock time.
Args:
datetime_str: Time in format YYYY/MM/DD,HH:MM:SS
"""
await self._send_command(f"Clock,{datetime_str}\r\n")
logger.info(f"Clock set on {self.device_key} to {datetime_str}")
async def get_frequency_weighting(self, channel: str = "Main") -> str:
"""Get frequency weighting (A, C, Z, etc.).
Args:
channel: Main, Sub1, Sub2, or Sub3
"""
resp = await self._send_command(f"Frequency Weighting ({channel})?\r\n")
logger.info(f"Frequency weighting ({channel}) on {self.device_key}: {resp}")
return resp.strip()
async def set_frequency_weighting(self, weighting: str, channel: str = "Main"):
"""Set frequency weighting.
Args:
weighting: A, C, or Z
channel: Main, Sub1, Sub2, or Sub3
"""
await self._send_command(f"Frequency Weighting ({channel}),{weighting}\r\n")
logger.info(f"Frequency weighting ({channel}) set to {weighting} on {self.device_key}")
async def get_time_weighting(self, channel: str = "Main") -> str:
"""Get time weighting (F, S, I).
Args:
channel: Main, Sub1, Sub2, or Sub3
"""
resp = await self._send_command(f"Time Weighting ({channel})?\r\n")
logger.info(f"Time weighting ({channel}) on {self.device_key}: {resp}")
return resp.strip()
async def set_time_weighting(self, weighting: str, channel: str = "Main"):
"""Set time weighting.
Args:
weighting: F (Fast), S (Slow), or I (Impulse)
channel: Main, Sub1, Sub2, or Sub3
"""
await self._send_command(f"Time Weighting ({channel}),{weighting}\r\n")
logger.info(f"Time weighting ({channel}) set to {weighting} on {self.device_key}")
async def request_dlc(self) -> dict:
"""Request DLC (Data Last Calculation) - final stored measurement results.
This retrieves the complete calculation results from the last/current measurement,
including all statistical data. Similar to DOD but for final results.
Returns:
Dict with parsed DLC data
"""
resp = await self._send_command("DLC?\r\n")
logger.info(f"DLC data received from {self.device_key}: {resp[:100]}...")
# Parse DLC response - similar format to DOD
# The exact format depends on device configuration
# For now, return raw data - can be enhanced based on actual response format
return {
"raw_data": resp.strip(),
"device_key": self.device_key,
}
async def stream_drd(self, callback):
"""Stream continuous DRD output from the device.
@@ -380,23 +477,45 @@ class NL43Client:
"""
logger.info(f"Listing FTP files on {self.device_key} at {remote_path}")
def _list_ftp_sync():
"""Synchronous FTP listing using ftplib (supports active mode)."""
ftp = FTP()
ftp.set_debuglevel(0)
try:
# FTP uses standard port 21, not the TCP control port
async with aioftp.Client.context(
self.host,
port=21,
user=self.ftp_username,
password=self.ftp_password,
socket_timeout=10
) as client:
# Connect and login
ftp.connect(self.host, 21, timeout=10)
ftp.login(self.ftp_username, self.ftp_password)
ftp.set_pasv(False) # Force active mode
# Change to target directory
if remote_path != "/":
ftp.cwd(remote_path)
# Get directory listing with details
files = []
async for path, info in client.list(remote_path):
lines = []
ftp.retrlines('LIST', lines.append)
for line in lines:
# Parse Unix-style ls output
parts = line.split(None, 8)
if len(parts) < 9:
continue
is_dir = parts[0].startswith('d')
size = int(parts[4]) if not is_dir else 0
name = parts[8]
# Skip . and ..
if name in ('.', '..'):
continue
file_info = {
"name": path.name,
"path": str(path),
"size": info.get("size", 0),
"modified": info.get("modify", ""),
"is_dir": info["type"] == "dir",
"name": name,
"path": f"{remote_path.rstrip('/')}/{name}",
"size": size,
"modified": f"{parts[5]} {parts[6]} {parts[7]}",
"is_dir": is_dir,
}
files.append(file_info)
logger.debug(f"Found file: {file_info}")
@@ -404,6 +523,15 @@ class NL43Client:
logger.info(f"Found {len(files)} files/directories on {self.device_key}")
return files
finally:
try:
ftp.quit()
except:
pass
try:
# Run synchronous FTP in thread pool
return await asyncio.to_thread(_list_ftp_sync)
except Exception as e:
logger.error(f"Failed to list FTP files on {self.device_key}: {e}")
raise ConnectionError(f"FTP connection failed: {str(e)}")
@@ -417,18 +545,31 @@ class NL43Client:
"""
logger.info(f"Downloading {remote_path} from {self.device_key} to {local_path}")
def _download_ftp_sync():
"""Synchronous FTP download using ftplib (supports active mode)."""
ftp = FTP()
ftp.set_debuglevel(0)
try:
# FTP uses standard port 21, not the TCP control port
async with aioftp.Client.context(
self.host,
port=21,
user=self.ftp_username,
password=self.ftp_password,
socket_timeout=10
) as client:
await client.download(remote_path, local_path, write_into=True)
# Connect and login
ftp.connect(self.host, 21, timeout=10)
ftp.login(self.ftp_username, self.ftp_password)
ftp.set_pasv(False) # Force active mode
# Download file
with open(local_path, 'wb') as f:
ftp.retrbinary(f'RETR {remote_path}', f.write)
logger.info(f"Successfully downloaded {remote_path} to {local_path}")
finally:
try:
ftp.quit()
except:
pass
try:
# Run synchronous FTP in thread pool
await asyncio.to_thread(_download_ftp_sync)
except Exception as e:
logger.error(f"Failed to download {remote_path} from {self.device_key}: {e}")
raise ConnectionError(f"FTP download failed: {str(e)}")

Binary file not shown.

View File

@@ -379,3 +379,149 @@
2025-12-24 02:02:08,074 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:02:13,115 - app.main - INFO - Database tables initialized
2025-12-24 02:02:13,115 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:03:20,909 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:03:21,218 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('230', '33x') but got 530 [' Login Fail']
2025-12-24 02:03:21,218 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('230', '33x') but got 530 [' Login Fail']
2025-12-24 02:03:26,148 - app.main - INFO - Database tables initialized
2025-12-24 02:03:26,149 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:04:55,026 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:04:55,339 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('230', '33x') but got 530 [' Login Fail']
2025-12-24 02:04:55,339 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('230', '33x') but got 530 [' Login Fail']
2025-12-24 02:12:30,900 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:12:31,978 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('227',) but got 502 [' Not Implemented']
2025-12-24 02:12:31,978 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('227',) but got 502 [' Not Implemented']
2025-12-24 02:12:42,647 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
2025-12-24 02:12:42,819 - app.services - INFO - FTP enabled on 63.45.161.30:2255
2025-12-24 02:12:42,819 - app.routers - INFO - Enabled FTP on unit nl43-1
2025-12-24 02:12:46,890 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:12:47,779 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: Waiting for ('227',) but got 502 [' Not Implemented']
2025-12-24 02:12:47,779 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: Waiting for ('227',) but got 502 [' Not Implemented']
2025-12-24 02:14:28,289 - app.main - INFO - Database tables initialized
2025-12-24 02:14:28,289 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:14:29,306 - app.main - INFO - Database tables initialized
2025-12-24 02:14:29,306 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:14:58,933 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
2025-12-24 02:14:59,099 - app.services - INFO - FTP enabled on 63.45.161.30:2255
2025-12-24 02:14:59,099 - app.routers - INFO - Enabled FTP on unit nl43-1
2025-12-24 02:14:59,921 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:15:00,339 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: No passive commands provided
2025-12-24 02:15:00,339 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: No passive commands provided
2025-12-24 02:15:32,844 - app.main - INFO - Database tables initialized
2025-12-24 02:15:32,844 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:15:34,474 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:15:34,859 - app.services - ERROR - Failed to list FTP files on 63.45.161.30:2255: No passive commands provided
2025-12-24 02:15:34,859 - app.routers - ERROR - Failed to list FTP files on nl43-1: FTP connection failed: No passive commands provided
2025-12-24 02:16:31,671 - app.main - INFO - Database tables initialized
2025-12-24 02:16:31,671 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:17:13,560 - app.main - INFO - Database tables initialized
2025-12-24 02:17:13,560 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:17:29,976 - app.main - INFO - Database tables initialized
2025-12-24 02:17:29,976 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:30:23,927 - app.main - INFO - Database tables initialized
2025-12-24 02:30:23,928 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:30:43,933 - app.routers - INFO - WebSocket connection accepted for unit nl43-1
2025-12-24 02:30:43,934 - app.routers - INFO - Starting DRD stream for unit nl43-1
2025-12-24 02:30:43,934 - app.services - INFO - Starting DRD stream for 63.45.161.30:2255
2025-12-24 02:30:44,099 - app.services - INFO - DRD stream started successfully for 63.45.161.30:2255
2025-12-24 02:30:47,915 - app.routers - ERROR - Failed to send snapshot via WebSocket: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
2025-12-24 02:30:47,916 - app.services - INFO - DRD stream ended for 63.45.161.30:2255
2025-12-24 02:30:47,916 - app.routers - ERROR - Unexpected error in WebSocket stream for nl43-1: received 1005 (no status received [internal]); then sent 1005 (no status received [internal])
2025-12-24 02:30:47,916 - app.routers - INFO - WebSocket stream closed for unit nl43-1
2025-12-24 02:30:50,949 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
2025-12-24 02:30:51,149 - app.services - INFO - Parsed 64 data points from DOD response
2025-12-24 02:30:51,159 - app.routers - INFO - Retrieved live status for unit nl43-1
2025-12-24 02:30:54,330 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:30:55,059 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255
2025-12-24 02:31:12,298 - app.services - INFO - Downloading /NIKON001.DSC from 63.45.161.30:2255 to data/downloads/nl43-1/NIKON001.DSC
2025-12-24 02:31:13,140 - app.services - INFO - Successfully downloaded /NIKON001.DSC to data/downloads/nl43-1/NIKON001.DSC
2025-12-24 02:31:13,220 - app.routers - INFO - Downloaded /NIKON001.DSC from nl43-1 to data/downloads/nl43-1/NIKON001.DSC
2025-12-24 02:34:49,457 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:34:50,419 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255
2025-12-24 02:34:52,824 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43
2025-12-24 02:34:53,709 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255
2025-12-24 02:35:00,136 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Screenshot
2025-12-24 02:35:00,942 - app.services - INFO - Found 1 files/directories on 63.45.161.30:2255
2025-12-24 02:35:03,332 - app.services - INFO - Downloading /NL-43/Screenshot/0001_20251223_193950.bmp from 63.45.161.30:2255 to data/downloads/nl43-1/0001_20251223_193950.bmp
2025-12-24 02:35:04,939 - app.services - INFO - Successfully downloaded /NL-43/Screenshot/0001_20251223_193950.bmp to data/downloads/nl43-1/0001_20251223_193950.bmp
2025-12-24 02:35:05,019 - app.routers - INFO - Downloaded /NL-43/Screenshot/0001_20251223_193950.bmp from nl43-1 to data/downloads/nl43-1/0001_20251223_193950.bmp
2025-12-24 02:35:20,669 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/
2025-12-24 02:35:21,539 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255
2025-12-24 02:35:24,452 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019
2025-12-24 02:35:25,339 - app.services - INFO - Found 3 files/directories on 63.45.161.30:2255
2025-12-24 02:35:30,134 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019/Auto_Lp_01
2025-12-24 02:35:30,939 - app.services - INFO - Found 1 files/directories on 63.45.161.30:2255
2025-12-24 02:35:34,225 - app.services - INFO - Downloading /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd from 63.45.161.30:2255 to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd
2025-12-24 02:35:36,139 - app.services - INFO - Successfully downloaded /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd
2025-12-24 02:35:36,219 - app.routers - INFO - Downloaded /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd from nl43-1 to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd
2025-12-24 02:36:30,080 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:36:30,779 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255
2025-12-24 02:36:34,289 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43
2025-12-24 02:36:35,109 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255
2025-12-24 02:36:40,284 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019
2025-12-24 02:36:41,220 - app.services - INFO - Found 3 files/directories on 63.45.161.30:2255
2025-12-24 02:36:43,370 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019/Auto_Lp_01
2025-12-24 02:36:44,428 - app.services - INFO - Found 1 files/directories on 63.45.161.30:2255
2025-12-24 02:36:46,101 - app.services - INFO - Downloading /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd from 63.45.161.30:2255 to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd
2025-12-24 02:36:47,380 - app.services - INFO - Successfully downloaded /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd
2025-12-24 02:36:47,476 - app.routers - INFO - Downloaded /NL-43/Auto_0019/Auto_Lp_01/NL_0001_SLM_Lp _0019_0001.rnd from nl43-1 to data/downloads/nl43-1/NL_0001_SLM_Lp _0019_0001.rnd
2025-12-24 02:37:18,786 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Auto_0019/
2025-12-24 02:37:19,580 - app.services - INFO - Found 3 files/directories on 63.45.161.30:2255
2025-12-24 02:37:25,812 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/
2025-12-24 02:37:26,619 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255
2025-12-24 02:37:28,988 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/Manual_0019
2025-12-24 02:37:29,859 - app.services - INFO - Found 1 files/directories on 63.45.161.30:2255
2025-12-24 02:37:31,775 - app.services - INFO - Downloading /NL-43/Manual_0019/NL_0001_SLM_MAN_0019_0000.rnd from 63.45.161.30:2255 to data/downloads/nl43-1/NL_0001_SLM_MAN_0019_0000.rnd
2025-12-24 02:37:32,659 - app.services - INFO - Successfully downloaded /NL-43/Manual_0019/NL_0001_SLM_MAN_0019_0000.rnd to data/downloads/nl43-1/NL_0001_SLM_MAN_0019_0000.rnd
2025-12-24 02:37:32,739 - app.routers - INFO - Downloaded /NL-43/Manual_0019/NL_0001_SLM_MAN_0019_0000.rnd from nl43-1 to data/downloads/nl43-1/NL_0001_SLM_MAN_0019_0000.rnd
2025-12-24 02:38:02,603 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:38:03,379 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255
2025-12-24 02:38:19,338 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
2025-12-24 02:38:19,499 - app.services - INFO - FTP enabled on 63.45.161.30:2255
2025-12-24 02:38:19,499 - app.routers - INFO - Enabled FTP on unit nl43-1
2025-12-24 02:38:20,339 - app.services - INFO - Sending command to 63.45.161.30:2255: FTP,On
2025-12-24 02:38:20,500 - app.services - INFO - FTP enabled on 63.45.161.30:2255
2025-12-24 02:38:20,500 - app.routers - INFO - Enabled FTP on unit nl43-1
2025-12-24 02:38:23,612 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:38:24,339 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255
2025-12-24 02:38:31,856 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /NL-43/
2025-12-24 02:38:32,660 - app.services - INFO - Found 8 files/directories on 63.45.161.30:2255
2025-12-24 02:38:35,781 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:38:36,500 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255
2025-12-24 02:38:39,724 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /DCIM
2025-12-24 02:38:40,499 - app.services - INFO - Found 0 files/directories on 63.45.161.30:2255
2025-12-24 02:38:45,065 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 02:38:45,939 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255
2025-12-24 02:41:25,498 - app.main - INFO - Database tables initialized
2025-12-24 02:41:25,498 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 02:42:08,371 - app.main - INFO - Database tables initialized
2025-12-24 02:42:08,372 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 05:44:06,113 - app.main - INFO - Database tables initialized
2025-12-24 05:44:06,113 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 05:44:19,375 - app.main - INFO - Database tables initialized
2025-12-24 05:44:19,376 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 06:11:27,237 - app.main - INFO - Database tables initialized
2025-12-24 06:11:27,237 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 06:11:31,752 - app.services - INFO - Sending command to 63.45.161.30:2255: Battery Level?
2025-12-24 06:11:31,901 - app.services - INFO - Battery level on 63.45.161.30:2255: Full
2025-12-24 06:11:34,598 - app.services - INFO - Sending command to 63.45.161.30:2255: Clock?
2025-12-24 06:11:34,742 - app.services - INFO - Clock on 63.45.161.30:2255: 2025/12/24 02:10:55
2025-12-24 06:11:39,102 - app.services - INFO - Sending command to 63.45.161.30:2255: Clock,2025/12/24,01:11:41
2025-12-24 06:11:39,262 - app.services - ERROR - Communication error with 63.45.161.30:2255: Parameter error - invalid parameter value
2025-12-24 06:11:39,262 - app.routers - ERROR - Failed to set clock for nl43-1: Parameter error - invalid parameter value
2025-12-24 06:12:00,090 - app.services - INFO - Listing FTP files on 63.45.161.30:2255 at /
2025-12-24 06:12:00,789 - app.services - INFO - Found 4 files/directories on 63.45.161.30:2255
2025-12-24 06:12:12,263 - app.services - INFO - Sending command to 63.45.161.30:2255: Frequency Weighting (Main)?
2025-12-24 06:12:12,432 - app.services - INFO - Frequency weighting (Main) on 63.45.161.30:2255: A
2025-12-24 06:12:23,787 - app.services - INFO - Sending command to 63.45.161.30:2255: Time Weighting (Main)?
2025-12-24 06:12:23,942 - app.services - INFO - Time weighting (Main) on 63.45.161.30:2255: S
2025-12-24 06:12:41,070 - app.services - INFO - Sending command to 63.45.161.30:2255: Clock,2025/12/24,01:12:43
2025-12-24 06:12:41,232 - app.services - ERROR - Communication error with 63.45.161.30:2255: Parameter error - invalid parameter value
2025-12-24 06:12:41,232 - app.routers - ERROR - Failed to set clock for nl43-1: Parameter error - invalid parameter value
2025-12-24 06:13:04,056 - app.services - INFO - Sending command to 63.45.161.30:2255: DLC?
2025-12-24 06:13:04,232 - app.services - INFO - DLC data received from 63.45.161.30:2255: -.-, 43.7, 53.7, 44.1, 43.4, 44.2, 44.2, 43.7, 43.4, 43.3, 59.0, -.-, -.-, -.-,0,0, -.-, -.-, ...
2025-12-24 06:13:04,232 - app.routers - INFO - Retrieved measurement results for unit nl43-1
2025-12-24 06:13:29,235 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
2025-12-24 06:13:29,422 - app.routers - INFO - Started measurement on unit nl43-1
2025-12-24 06:13:55,260 - app.services - INFO - Sending command to 63.45.161.30:2255: Clock,2025/12/24,01:13:57
2025-12-24 06:13:55,422 - app.services - ERROR - Communication error with 63.45.161.30:2255: Parameter error - invalid parameter value
2025-12-24 06:13:55,422 - app.routers - ERROR - Failed to set clock for nl43-1: Parameter error - invalid parameter value

View File

@@ -30,11 +30,40 @@
</fieldset>
<fieldset>
<legend>Controls</legend>
<legend>Measurement Controls</legend>
<button onclick="start()">Start</button>
<button onclick="stop()">Stop</button>
<button onclick="pause()">Pause</button>
<button onclick="resume()">Resume</button>
<button onclick="reset()">Reset</button>
<button onclick="store()">Store Data</button>
<button onclick="live()">Fetch Live (DOD?)</button>
<button onclick="live()">Fetch Live (DOD)</button>
<button onclick="getResults()">Get Results (DLC)</button>
</fieldset>
<fieldset>
<legend>Device Info</legend>
<button onclick="getBattery()">Get Battery</button>
<button onclick="getClock()">Get Clock</button>
<button onclick="syncClock()">Sync Clock to PC</button>
</fieldset>
<fieldset>
<legend>Measurement Settings</legend>
<div style="margin-bottom: 8px;">
<label style="display: inline; margin-right: 8px;">Frequency Weighting:</label>
<button onclick="getFreqWeighting()">Get</button>
<button onclick="setFreqWeighting('A')">Set A</button>
<button onclick="setFreqWeighting('C')">Set C</button>
<button onclick="setFreqWeighting('Z')">Set Z</button>
</div>
<div>
<label style="display: inline; margin-right: 8px;">Time Weighting:</label>
<button onclick="getTimeWeighting()">Get</button>
<button onclick="setTimeWeighting('F')">Set Fast</button>
<button onclick="setTimeWeighting('S')">Set Slow</button>
<button onclick="setTimeWeighting('I')">Set Impulse</button>
</div>
</fieldset>
<fieldset>
@@ -134,6 +163,134 @@
log(`Live: ${JSON.stringify(data.data)}`);
}
async function getResults() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/results`);
const data = await res.json();
if (!res.ok) {
log(`Get Results failed: ${res.status} ${JSON.stringify(data)}`);
return;
}
statusEl.textContent = JSON.stringify(data.data, null, 2);
log(`Results (DLC): Retrieved final calculation data`);
}
// New measurement control functions
async function pause() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/pause`, { method: 'POST' });
const data = await res.json();
log(`Pause: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
async function resume() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/resume`, { method: 'POST' });
const data = await res.json();
log(`Resume: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
async function reset() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/reset`, { method: 'POST' });
const data = await res.json();
log(`Reset: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
// Device info functions
async function getBattery() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/battery`);
const data = await res.json();
if (res.ok) {
log(`Battery Level: ${data.battery_level}%`);
} else {
log(`Get Battery failed: ${res.status} - ${data.detail}`);
}
}
async function getClock() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/clock`);
const data = await res.json();
if (res.ok) {
log(`Device Clock: ${data.clock}`);
} else {
log(`Get Clock failed: ${res.status} - ${data.detail}`);
}
}
async function syncClock() {
const unitId = document.getElementById('unitId').value;
const now = new Date();
const datetime = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')},${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
const res = await fetch(`/api/nl43/${unitId}/clock`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ datetime })
});
const data = await res.json();
if (res.ok) {
log(`Clock synced to: ${datetime}`);
} else {
log(`Sync Clock failed: ${res.status} - ${data.detail}`);
}
}
// Measurement settings functions
async function getFreqWeighting() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/frequency-weighting?channel=Main`);
const data = await res.json();
if (res.ok) {
log(`Frequency Weighting (Main): ${data.frequency_weighting}`);
} else {
log(`Get Freq Weighting failed: ${res.status} - ${data.detail}`);
}
}
async function setFreqWeighting(weighting) {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/frequency-weighting`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ weighting, channel: 'Main' })
});
const data = await res.json();
if (res.ok) {
log(`Frequency Weighting set to: ${weighting}`);
} else {
log(`Set Freq Weighting failed: ${res.status} - ${data.detail}`);
}
}
async function getTimeWeighting() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/time-weighting?channel=Main`);
const data = await res.json();
if (res.ok) {
log(`Time Weighting (Main): ${data.time_weighting}`);
} else {
log(`Get Time Weighting failed: ${res.status} - ${data.detail}`);
}
}
async function setTimeWeighting(weighting) {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/time-weighting`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ weighting, channel: 'Main' })
});
const data = await res.json();
if (res.ok) {
log(`Time Weighting set to: ${weighting}`);
} else {
log(`Set Time Weighting failed: ${res.status} - ${data.detail}`);
}
}
function toggleStream() {
if (ws && ws.readyState === WebSocket.OPEN) {
stopStream();
@@ -239,9 +396,12 @@
}
}
async function listFiles() {
let currentPath = '/';
async function listFiles(path = '/') {
currentPath = path;
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/ftp/files?path=/`);
const res = await fetch(`/api/nl43/${unitId}/ftp/files?path=${encodeURIComponent(path)}`);
const data = await res.json();
if (!res.ok) {
@@ -252,13 +412,58 @@
const fileListEl = document.getElementById('fileList');
fileListEl.innerHTML = '';
// Add breadcrumb navigation
const breadcrumb = document.createElement('div');
breadcrumb.style.marginBottom = '8px';
breadcrumb.style.padding = '4px';
breadcrumb.style.background = '#e1e4e8';
breadcrumb.style.borderRadius = '3px';
breadcrumb.innerHTML = '<strong>Path:</strong> ';
const pathParts = path.split('/').filter(p => p);
let builtPath = '/';
// Root link
const rootLink = document.createElement('a');
rootLink.href = '#';
rootLink.textContent = '/';
rootLink.style.marginRight = '4px';
rootLink.onclick = (e) => { e.preventDefault(); listFiles('/'); };
breadcrumb.appendChild(rootLink);
// Path component links
pathParts.forEach((part, idx) => {
builtPath += part + '/';
const linkPath = builtPath;
const separator = document.createElement('span');
separator.textContent = ' / ';
breadcrumb.appendChild(separator);
const link = document.createElement('a');
link.href = '#';
link.textContent = part;
link.style.marginRight = '4px';
if (idx === pathParts.length - 1) {
link.style.fontWeight = 'bold';
link.style.color = '#000';
}
link.onclick = (e) => { e.preventDefault(); listFiles(linkPath); };
breadcrumb.appendChild(link);
});
fileListEl.appendChild(breadcrumb);
if (data.files.length === 0) {
fileListEl.textContent = 'No files found';
log(`No files found on device`);
const emptyDiv = document.createElement('div');
emptyDiv.textContent = 'No files found';
emptyDiv.style.padding = '8px';
fileListEl.appendChild(emptyDiv);
log(`No files found in ${path}`);
return;
}
log(`Found ${data.count} files on device`);
log(`Found ${data.count} files in ${path}`);
data.files.forEach(file => {
const fileDiv = document.createElement('div');
@@ -269,11 +474,18 @@
const icon = file.is_dir ? '📁' : '📄';
const size = file.is_dir ? '' : ` (${(file.size / 1024).toFixed(1)} KB)`;
if (file.is_dir) {
fileDiv.innerHTML = `
${icon} <strong>${file.name}</strong>${size}
${!file.is_dir ? `<button onclick="downloadFile('${file.path}')" style="margin-left: 8px; padding: 2px 6px; font-size: 0.9em;">Download</button>` : ''}
${icon} <a href="#" onclick="event.preventDefault(); listFiles('${file.path}');" style="font-weight: bold;">${file.name}</a>
<br><small style="color: #666;">${file.path}</small>
`;
} else {
fileDiv.innerHTML = `
${icon} <strong>${file.name}</strong>${size}
<button onclick="downloadFile('${file.path}')" style="margin-left: 8px; padding: 2px 6px; font-size: 0.9em;">Download</button>
<br><small style="color: #666;">${file.path}</small>
`;
}
fileListEl.appendChild(fileDiv);
});