diff --git a/API.md b/API.md new file mode 100644 index 0000000..ff0d276 --- /dev/null +++ b/API.md @@ -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 diff --git a/app/__pycache__/routers.cpython-310.pyc b/app/__pycache__/routers.cpython-310.pyc index 97e9712..c455f0e 100644 Binary files a/app/__pycache__/routers.cpython-310.pyc and b/app/__pycache__/routers.cpython-310.pyc differ diff --git a/app/__pycache__/services.cpython-310.pyc b/app/__pycache__/services.cpython-310.pyc index 3c8764f..acd25a2 100644 Binary files a/app/__pycache__/services.cpython-310.pyc and b/app/__pycache__/services.cpython-310.pyc differ diff --git a/app/routers.py b/app/routers.py index 3e5dff5..bf030f7 100644 --- a/app/routers.py +++ b/app/routers.py @@ -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. diff --git a/app/services.py b/app/services.py index 3462a2f..5558151 100644 --- a/app/services.py +++ b/app/services.py @@ -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}") - 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: + def _list_ftp_sync(): + """Synchronous FTP listing using ftplib (supports active mode).""" + ftp = FTP() + ftp.set_debuglevel(0) + try: + # 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}") - 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) + def _download_ftp_sync(): + """Synchronous FTP download using ftplib (supports active mode).""" + ftp = FTP() + ftp.set_debuglevel(0) + try: + # 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)}") diff --git a/data/slmm.db b/data/slmm.db index 24c7a61..c8a4bfd 100644 Binary files a/data/slmm.db and b/data/slmm.db differ diff --git a/data/slmm.log b/data/slmm.log index b365629..3bec438 100644 --- a/data/slmm.log +++ b/data/slmm.log @@ -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 diff --git a/templates/index.html b/templates/index.html index 3b5a088..e1090b1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -30,11 +30,40 @@
+ + + +