API built for most common commands
This commit is contained in:
406
API.md
Normal file
406
API.md
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
229
app/routers.py
229
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.
|
||||
|
||||
193
app/services.py
193
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)}")
|
||||
|
||||
BIN
data/slmm.db
BIN
data/slmm.db
Binary file not shown.
146
data/slmm.log
146
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
|
||||
|
||||
@@ -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)`;
|
||||
|
||||
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>` : ''}
|
||||
<br><small style="color: #666;">${file.path}</small>
|
||||
`;
|
||||
if (file.is_dir) {
|
||||
fileDiv.innerHTML = `
|
||||
${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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user