feat: Add comprehensive NL-43/NL-53 Communication Guide and command references

- Introduced a new communication guide detailing protocol basics, transport modes, and a quick startup checklist.
- Added a detailed list of commands with their functions and usage for NL-43/NL-53 devices.
- Created a verified quick reference for command formats to prevent common mistakes.
- Implemented an improvements document outlining critical fixes, security enhancements, reliability upgrades, and code quality improvements for the SLMM project.
- Enhanced the frontend with a new button to retrieve all device settings, along with corresponding JavaScript functionality.
- Added a test script for the new settings retrieval API endpoint to demonstrate its usage and validate functionality.
This commit is contained in:
serversdwn
2025-12-25 00:36:46 +00:00
parent c90544a712
commit f9139d6aa3
17 changed files with 1395 additions and 13 deletions

195
FEATURE_SUMMARY.md Normal file
View File

@@ -0,0 +1,195 @@
# Feature Summary: Device Settings Verification
## What Was Added
A new API endpoint that retrieves all current device settings in a single request, allowing users to quickly verify the NL43/NL53 configuration before starting measurements.
## New Endpoint
**`GET /api/nl43/{unit_id}/settings`**
Returns comprehensive device configuration including:
- Measurement state and weighting settings
- Timing and interval configuration
- Battery level and device clock
- Sleep mode and FTP status
## Files Modified
### 1. [app/routers.py](app/routers.py)
**Lines:** 728-761
Added new route handler `get_all_settings()` that:
- Validates device configuration exists
- Checks TCP communication is enabled
- Calls `NL43Client.get_all_settings()`
- Returns formatted JSON response with all settings
- Handles connection errors, timeouts, and exceptions
### 2. [README.md](README.md)
**Updated sections:**
- Line 134: Added new endpoint to Measurement Settings table
- Lines 259-283: Added usage example showing how to verify device settings
## Files Created
### 1. [test_settings_endpoint.py](test_settings_endpoint.py)
Test/demonstration script showing:
- How to use the `get_all_settings()` method
- Example API endpoint usage with curl
- Expected response format
### 2. [SETTINGS_ENDPOINT.md](SETTINGS_ENDPOINT.md)
Comprehensive documentation including:
- Detailed endpoint description
- Complete list of settings retrieved
- Usage examples in bash, Python, and JavaScript
- Performance considerations
- Best practices and troubleshooting
### 3. [FEATURE_SUMMARY.md](FEATURE_SUMMARY.md)
This file - summary of changes for reference
## Existing Functionality Used
The implementation leverages the existing `get_all_settings()` method in [app/services.py](app/services.py#L538) which was already implemented but not exposed via the API. This method queries multiple device settings and handles errors gracefully.
## How It Works
1. **User makes GET request** to `/api/nl43/{unit_id}/settings`
2. **Router validates** device configuration exists and TCP is enabled
3. **NL43Client queries device** for each setting sequentially (with 1-second delays)
4. **Individual errors** are caught and returned as error strings
5. **Response returned** with all settings in JSON format
## Usage Example
```bash
# Quick verification before measurement
curl http://localhost:8100/api/nl43/NL43-001/settings
# Response:
{
"status": "ok",
"unit_id": "NL43-001",
"settings": {
"measurement_state": "Stop",
"frequency_weighting": "A",
"time_weighting": "F",
"measurement_time": "00:01:00",
"leq_interval": "1s",
"lp_interval": "125ms",
"index_number": "0",
"battery_level": "100%",
"clock": "2025/12/24,20:45:30",
"sleep_mode": "Off",
"ftp_status": "On"
}
}
```
## Benefits
1. **Single request** - Get all settings at once instead of multiple API calls
2. **Pre-flight checks** - Verify configuration before starting measurements
3. **Documentation** - Easy to save configuration snapshots for audit trails
4. **Troubleshooting** - Quickly identify misconfigured settings
5. **Multi-device** - Compare settings across multiple devices
## Performance Notes
- **Query time:** ~10-15 seconds (due to required 1-second delays between commands)
- **Rate limiting:** Automatically enforced by NL43Client
- **Error handling:** Partial failures don't prevent other settings from being retrieved
- **Caching recommended:** Settings don't change frequently, cache for 5-10 minutes
## Testing
To test the new endpoint:
1. **Start the server:**
```bash
uvicorn app.main:app --reload --port 8100
```
2. **Configure a device** (if not already configured):
```bash
curl -X PUT http://localhost:8100/api/nl43/test-meter/config \
-H "Content-Type: application/json" \
-d '{"host": "192.168.1.100", "tcp_port": 80, "tcp_enabled": true}'
```
3. **Query settings:**
```bash
curl http://localhost:8100/api/nl43/test-meter/settings
```
4. **Check Swagger UI:**
- Navigate to http://localhost:8100/docs
- Find "GET /api/nl43/{unit_id}/settings" endpoint
- Click "Try it out" and test interactively
## Integration Tips
### Frontend Integration
```javascript
// React/Vue/Angular example
async function verifyDeviceBeforeMeasurement(unitId) {
const response = await fetch(`/api/nl43/${unitId}/settings`);
const { settings } = await response.json();
// Verify critical settings
if (settings.frequency_weighting !== 'A') {
alert('Warning: Frequency weighting not set to A');
}
// Check battery
const batteryPercent = parseInt(settings.battery_level);
if (batteryPercent < 20) {
alert('Low battery! Please charge device.');
}
return settings;
}
```
### Python Automation
```python
def ensure_correct_config(unit_id: str, required_config: dict):
"""Verify device matches required configuration."""
settings = get_device_settings(unit_id)
mismatches = []
for key, expected in required_config.items():
actual = settings.get(key)
if actual != expected:
mismatches.append(f"{key}: expected {expected}, got {actual}")
if mismatches:
raise ValueError(f"Configuration mismatch: {', '.join(mismatches)}")
return True
```
## Future Enhancements
Potential improvements for future versions:
1. **Filtered queries** - Query parameter to select specific settings
2. **Diff mode** - Compare current settings to expected values
3. **Batch queries** - Get settings from multiple devices in one request
4. **Settings profiles** - Save/load common configuration profiles
5. **Change detection** - Track when settings were last modified
## Support
For questions or issues with this feature:
- See [SETTINGS_ENDPOINT.md](SETTINGS_ENDPOINT.md) for detailed documentation
- Check [README.md](README.md) for general API usage
- Review [COMMUNICATION_GUIDE.md](COMMUNICATION_GUIDE.md) for protocol details
## Version Info
- **Added:** December 24, 2025
- **API Version:** Compatible with existing v1 API
- **Breaking Changes:** None - purely additive feature

View File

@@ -131,6 +131,7 @@ Logs are written to:
| Method | Endpoint | Description | | Method | Endpoint | Description |
|--------|----------|-------------| |--------|----------|-------------|
| GET | `/api/nl43/{unit_id}/settings` | Get all current device settings for verification |
| GET | `/api/nl43/{unit_id}/frequency-weighting` | Get frequency weighting (A/C/Z) | | GET | `/api/nl43/{unit_id}/frequency-weighting` | Get frequency weighting (A/C/Z) |
| PUT | `/api/nl43/{unit_id}/frequency-weighting` | Set frequency weighting | | PUT | `/api/nl43/{unit_id}/frequency-weighting` | Set frequency weighting |
| GET | `/api/nl43/{unit_id}/time-weighting` | Get time weighting (F/S/I) | | GET | `/api/nl43/{unit_id}/time-weighting` | Get time weighting (F/S/I) |
@@ -255,6 +256,32 @@ curl -X POST http://localhost:8100/api/nl43/meter-001/start
curl http://localhost:8100/api/nl43/meter-001/live curl http://localhost:8100/api/nl43/meter-001/live
``` ```
### Verify Device Settings
```bash
curl http://localhost:8100/api/nl43/meter-001/settings
```
This returns all current device configuration:
```json
{
"status": "ok",
"unit_id": "meter-001",
"settings": {
"measurement_state": "Stop",
"frequency_weighting": "A",
"time_weighting": "F",
"measurement_time": "00:01:00",
"leq_interval": "1s",
"lp_interval": "125ms",
"index_number": "0",
"battery_level": "100%",
"clock": "2025/12/24,20:45:30",
"sleep_mode": "Off",
"ftp_status": "On"
}
}
```
### Stream Real-time Data (JavaScript) ### Stream Real-time Data (JavaScript)
```javascript ```javascript
const ws = new WebSocket('ws://localhost:8100/api/nl43/meter-001/stream'); const ws = new WebSocket('ws://localhost:8100/api/nl43/meter-001/stream');

258
SETTINGS_ENDPOINT.md Normal file
View File

@@ -0,0 +1,258 @@
# Device Settings Verification Endpoint
## Overview
The new `GET /api/nl43/{unit_id}/settings` endpoint provides a comprehensive view of all current device settings. This allows you to quickly verify the configuration of your NL43/NL53 sound level meter before starting measurements, ensuring the device is configured correctly for your testing requirements.
## Endpoint Details
**URL:** `GET /api/nl43/{unit_id}/settings`
**Description:** Retrieves all queryable settings from the device in a single request.
**Response Time:** Approximately 10-15 seconds (due to required 1-second delay between device commands)
## Settings Retrieved
The endpoint queries the following categories of settings:
### Measurement Configuration
- **measurement_state**: Current state (Measure, Stop, Pause)
- **frequency_weighting**: Frequency weighting (A, C, or Z)
- **time_weighting**: Time weighting (F=Fast, S=Slow, I=Impulse)
### Timing and Intervals
- **measurement_time**: Total measurement duration setting
- **leq_interval**: Leq calculation interval
- **lp_interval**: Lp sampling interval
- **index_number**: Current index/file number for storage
### Device Information
- **battery_level**: Current battery percentage
- **clock**: Device clock time (format: YYYY/MM/DD,HH:MM:SS)
### Operational Status
- **sleep_mode**: Sleep mode status (On/Off)
- **ftp_status**: FTP server status (On/Off)
## Usage Examples
### Basic Request
```bash
curl http://localhost:8100/api/nl43/NL43-001/settings
```
### Response Format
```json
{
"status": "ok",
"unit_id": "NL43-001",
"settings": {
"measurement_state": "Stop",
"frequency_weighting": "A",
"time_weighting": "F",
"measurement_time": "00:01:00",
"leq_interval": "1s",
"lp_interval": "125ms",
"index_number": "0",
"battery_level": "100%",
"clock": "2025/12/24,20:45:30",
"sleep_mode": "Off",
"ftp_status": "On"
}
}
```
### Error Handling
Individual settings that fail to query will show an error message instead of a value:
```json
{
"status": "ok",
"unit_id": "NL43-001",
"settings": {
"measurement_state": "Stop",
"frequency_weighting": "A",
"time_weighting": "Error: Status error - device is in wrong state for this command",
...
}
}
```
This partial error handling ensures you get as much information as possible even if some settings fail to query.
## Common Use Cases
### Pre-Measurement Verification
Before starting a measurement session, verify all critical settings:
```bash
# Get all settings
SETTINGS=$(curl -s http://localhost:8100/api/nl43/meter-001/settings)
# Extract specific values (using jq)
FREQ_WEIGHT=$(echo $SETTINGS | jq -r '.settings.frequency_weighting')
TIME_WEIGHT=$(echo $SETTINGS | jq -r '.settings.time_weighting')
echo "Frequency: $FREQ_WEIGHT, Time: $TIME_WEIGHT"
```
### Configuration Audit
Document device configuration for quality assurance:
```bash
# Save settings snapshot
curl http://localhost:8100/api/nl43/meter-001/settings > config_snapshot_$(date +%Y%m%d_%H%M%S).json
```
### Multi-Device Comparison
Compare settings across multiple devices:
```bash
# Compare two devices
curl http://localhost:8100/api/nl43/meter-001/settings > device1.json
curl http://localhost:8100/api/nl43/meter-002/settings > device2.json
diff device1.json device2.json
```
## Integration Examples
### Python
```python
import requests
def verify_device_settings(unit_id: str) -> dict:
"""Retrieve and verify device settings."""
response = requests.get(f"http://localhost:8100/api/nl43/{unit_id}/settings")
response.raise_for_status()
data = response.json()
settings = data["settings"]
# Verify critical settings
assert settings["frequency_weighting"] == "A", "Wrong frequency weighting!"
assert settings["time_weighting"] == "F", "Wrong time weighting!"
return settings
# Usage
settings = verify_device_settings("NL43-001")
print(f"Battery: {settings['battery_level']}")
print(f"Clock: {settings['clock']}")
```
### JavaScript/TypeScript
```typescript
interface DeviceSettings {
measurement_state: string;
frequency_weighting: string;
time_weighting: string;
measurement_time: string;
leq_interval: string;
lp_interval: string;
index_number: string;
battery_level: string;
clock: string;
sleep_mode: string;
ftp_status: string;
}
async function getDeviceSettings(unitId: string): Promise<DeviceSettings> {
const response = await fetch(`http://localhost:8100/api/nl43/${unitId}/settings`);
const data = await response.json();
if (data.status !== "ok") {
throw new Error("Failed to retrieve settings");
}
return data.settings;
}
// Usage
const settings = await getDeviceSettings("NL43-001");
console.log(`Frequency weighting: ${settings.frequency_weighting}`);
console.log(`Battery level: ${settings.battery_level}`);
```
## Performance Considerations
### Query Duration
The endpoint queries multiple settings sequentially with required 1-second delays between commands. Total query time depends on:
- Number of settings queried (~10-12 settings)
- Network latency
- Device response time
**Expected duration:** 10-15 seconds
### Caching Strategy
For applications that need frequent access to settings:
1. **Cache results** - Settings don't change frequently unless you modify them
2. **Refresh periodically** - Query every 5-10 minutes or on-demand
3. **Track changes** - Re-query after sending configuration commands
### Rate Limiting
The endpoint respects device rate limiting (1-second delay between commands). Concurrent requests to the same device will be serialized automatically.
## Best Practices
1. **Pre-flight check**: Always verify settings before starting critical measurements
2. **Document configuration**: Save settings snapshots for audit trails
3. **Monitor battery**: Check battery level to avoid measurement interruption
4. **Sync clocks**: Verify device clock is accurate for timestamped data
5. **Error handling**: Check for "Error:" prefixes in individual setting values
## Related Endpoints
- `GET /api/nl43/{unit_id}/frequency-weighting` - Get single frequency weighting setting
- `PUT /api/nl43/{unit_id}/frequency-weighting` - Set frequency weighting
- `GET /api/nl43/{unit_id}/time-weighting` - Get single time weighting setting
- `PUT /api/nl43/{unit_id}/time-weighting` - Set time weighting
- `GET /api/nl43/{unit_id}/battery` - Get battery level only
- `GET /api/nl43/{unit_id}/clock` - Get device clock only
## Troubleshooting
### Slow Response
**Problem:** Endpoint takes longer than expected
**Solutions:**
- Normal behavior due to rate limiting (1 second between commands)
- Check network connectivity
- Verify device is not in sleep mode
### Partial Errors
**Problem:** Some settings show "Error:" messages
**Solutions:**
- Device may be in wrong state for certain queries
- Check if measurement is running (some settings require stopped state)
- Verify firmware version supports all queried commands
### Connection Timeout
**Problem:** 504 Gateway Timeout error
**Solutions:**
- Verify device IP address and port in configuration
- Check if device is powered on and connected
- Ensure TCP communication is enabled in device config
## See Also
- [README.md](README.md) - Main documentation
- [API.md](API.md) - Complete API reference
- [COMMUNICATION_GUIDE.md](COMMUNICATION_GUIDE.md) - NL43 protocol details

171
UI_UPDATE.md Normal file
View File

@@ -0,0 +1,171 @@
# Standalone UI Update: Get ALL Settings Button
## What Was Added
A new **"Get ALL Settings"** button has been added to the standalone web UI at [templates/index.html](templates/index.html).
## Location
The button is located in the **Measurement Settings** fieldset, at the top before the individual frequency and time weighting controls.
## Visual Layout
```
┌─────────────────────────────────────────────────────┐
│ Measurement Settings │
├─────────────────────────────────────────────────────┤
│ │
│ [Get ALL Settings] ← NEW BUTTON (bold styling) │
│ │
│ Frequency Weighting: │
│ [Get] [Set A] [Set C] [Set Z] │
│ │
│ Time Weighting: │
│ [Get] [Set Fast] [Set Slow] [Set Impulse] │
│ │
└─────────────────────────────────────────────────────┘
```
## Functionality
When clicked, the button:
1. **Shows loading message**: "Retrieving all device settings (this may take 10-15 seconds)..."
2. **Calls API**: `GET /api/nl43/{unit_id}/settings`
3. **Displays results in two places**:
- **Status area** (top): Shows formatted JSON with all settings
- **Log area** (bottom): Shows each setting on a separate line
## Example Output
### Status Area Display
```json
{
"measurement_state": "Stop",
"frequency_weighting": "A",
"time_weighting": "F",
"measurement_time": "00:01:00",
"leq_interval": "1s",
"lp_interval": "125ms",
"index_number": "0",
"battery_level": "100%",
"clock": "2025/12/24,20:45:30",
"sleep_mode": "Off",
"ftp_status": "On"
}
```
### Log Area Display
```
Retrieving all device settings (this may take 10-15 seconds)...
=== ALL DEVICE SETTINGS ===
measurement_state: Stop
frequency_weighting: A
time_weighting: F
measurement_time: 00:01:00
leq_interval: 1s
lp_interval: 125ms
index_number: 0
battery_level: 100%
clock: 2025/12/24,20:45:30
sleep_mode: Off
ftp_status: On
===========================
```
## Code Changes
### HTML Changes (Line 60)
```html
<button onclick="getAllSettings()" style="margin-bottom: 12px; font-weight: bold;">
Get ALL Settings
</button>
```
### JavaScript Changes (Lines 284-305)
```javascript
async function getAllSettings() {
const unitId = document.getElementById('unitId').value;
log('Retrieving all device settings (this may take 10-15 seconds)...');
const res = await fetch(`/api/nl43/${unitId}/settings`);
const data = await res.json();
if (!res.ok) {
log(`Get All Settings failed: ${res.status} - ${data.detail || JSON.stringify(data)}`);
return;
}
// Display in status area
statusEl.textContent = JSON.stringify(data.settings, null, 2);
// Log summary
log('=== ALL DEVICE SETTINGS ===');
Object.entries(data.settings).forEach(([key, value]) => {
log(`${key}: ${value}`);
});
log('===========================');
}
```
## Usage Flow
1. **Configure device** using the "Unit Config" section
2. **Click "Get ALL Settings"** to retrieve current configuration
3. **Review settings** in the Status and Log areas
4. **Verify** critical settings match requirements before starting measurements
## Benefits
**Quick verification** - One click to see all device settings
**Pre-measurement check** - Ensure device is configured correctly
**Debugging** - Identify misconfigured settings easily
**Documentation** - Copy settings from status area for records
**Comparison** - Compare settings across multiple devices
## Error Handling
If the request fails:
- Error message is displayed in the log
- Status code and details are shown
- Previous status display is preserved
Example error output:
```
Retrieving all device settings (this may take 10-15 seconds)...
Get All Settings failed: 502 - Failed to communicate with device
```
## Testing the Feature
1. **Start the SLMM server**:
```bash
cd /home/serversdown/slmm
uvicorn app.main:app --reload --port 8100
```
2. **Open the standalone UI**:
```
http://localhost:8100
```
3. **Configure a device**:
- Enter Unit ID (e.g., "nl43-1")
- Enter Host IP (e.g., "192.168.1.100")
- Enter Port (e.g., "80")
- Click "Save Config"
4. **Test the new button**:
- Click "Get ALL Settings"
- Wait 10-15 seconds for results
- Review settings in Status and Log areas
## Related Files
- [templates/index.html](templates/index.html) - Standalone UI (updated)
- [app/routers.py](app/routers.py#L728-L761) - Settings endpoint
- [app/services.py](app/services.py#L538-L606) - Client implementation
- [README.md](README.md) - Main documentation
- [SETTINGS_ENDPOINT.md](SETTINGS_ENDPOINT.md) - API documentation

View File

@@ -725,6 +725,42 @@ async def get_ftp_status(unit_id: str, db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail=f"Failed to get FTP status: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get FTP status: {str(e)}")
@router.get("/{unit_id}/settings")
async def get_all_settings(unit_id: str, db: Session = Depends(get_db)):
"""Get all current device settings for verification.
Returns a comprehensive view of all device configuration including:
- Measurement state and weightings
- Timing and interval settings
- Battery level and clock
- Sleep and FTP status
This is useful for verifying device configuration before starting measurements.
"""
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:
settings = await client.get_all_settings()
logger.info(f"Retrieved all settings for unit {unit_id}")
return {"status": "ok", "unit_id": unit_id, "settings": settings}
except ConnectionError as e:
logger.error(f"Failed to get settings for {unit_id}: {e}")
raise HTTPException(status_code=502, detail="Failed to communicate with device")
except TimeoutError:
logger.error(f"Timeout getting settings for {unit_id}")
raise HTTPException(status_code=504, detail="Device communication timeout")
except Exception as e:
logger.error(f"Unexpected error getting settings for {unit_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{unit_id}/ftp/files") @router.get("/{unit_id}/ftp/files")
async def list_ftp_files(unit_id: str, path: str = "/", db: Session = Depends(get_db)): async def list_ftp_files(unit_id: str, path: str = "/", db: Session = Depends(get_db)):
"""List files on the device via FTP. """List files on the device via FTP.
@@ -748,6 +784,14 @@ async def list_ftp_files(unit_id: str, path: str = "/", db: Session = Depends(ge
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
class TimingPayload(BaseModel):
preset: str
class IndexPayload(BaseModel):
index: int
class DownloadRequest(BaseModel): class DownloadRequest(BaseModel):
remote_path: str remote_path: str
@@ -790,3 +834,178 @@ async def download_ftp_file(unit_id: str, payload: DownloadRequest, db: Session
except Exception as e: except Exception as e:
logger.error(f"Unexpected error downloading file from {unit_id}: {e}") logger.error(f"Unexpected error downloading file from {unit_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
# Timing/Interval Configuration Endpoints
@router.get("/{unit_id}/measurement-time")
async def get_measurement_time(unit_id: str, db: Session = Depends(get_db)):
"""Get current measurement time preset."""
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:
preset = await client.get_measurement_time()
return {"status": "ok", "measurement_time": preset}
except Exception as e:
logger.error(f"Failed to get measurement time for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/measurement-time")
async def set_measurement_time(unit_id: str, payload: TimingPayload, db: Session = Depends(get_db)):
"""Set measurement time preset (10s, 1m, 5m, 10m, 15m, 30m, 1h, 8h, 24h, or custom like 00:05:30)."""
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_measurement_time(payload.preset)
return {"status": "ok", "message": f"Measurement time set to {payload.preset}"}
except Exception as e:
logger.error(f"Failed to set measurement time for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/leq-interval")
async def get_leq_interval(unit_id: str, db: Session = Depends(get_db)):
"""Get current Leq calculation interval preset."""
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:
preset = await client.get_leq_interval()
return {"status": "ok", "leq_interval": preset}
except Exception as e:
logger.error(f"Failed to get Leq interval for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/leq-interval")
async def set_leq_interval(unit_id: str, payload: TimingPayload, db: Session = Depends(get_db)):
"""Set Leq calculation interval preset (Off, 10s, 1m, 5m, 10m, 15m, 30m, 1h, 8h, 24h, or custom like 00:05:30)."""
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_leq_interval(payload.preset)
return {"status": "ok", "message": f"Leq interval set to {payload.preset}"}
except Exception as e:
logger.error(f"Failed to set Leq interval for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/lp-interval")
async def get_lp_interval(unit_id: str, db: Session = Depends(get_db)):
"""Get current Lp store interval."""
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:
preset = await client.get_lp_interval()
return {"status": "ok", "lp_interval": preset}
except Exception as e:
logger.error(f"Failed to get Lp interval for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/lp-interval")
async def set_lp_interval(unit_id: str, payload: TimingPayload, db: Session = Depends(get_db)):
"""Set Lp store interval (Off, 10ms, 25ms, 100ms, 200ms, 1s)."""
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_lp_interval(payload.preset)
return {"status": "ok", "message": f"Lp interval set to {payload.preset}"}
except Exception as e:
logger.error(f"Failed to set Lp interval for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/index-number")
async def get_index_number(unit_id: str, db: Session = Depends(get_db)):
"""Get current index number for file numbering."""
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:
index = await client.get_index_number()
return {"status": "ok", "index_number": index}
except Exception as e:
logger.error(f"Failed to get index number for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/index-number")
async def set_index_number(unit_id: str, payload: IndexPayload, db: Session = Depends(get_db)):
"""Set index number for file numbering (0000-9999)."""
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_index_number(payload.index)
return {"status": "ok", "message": f"Index number set to {payload.index:04d}"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Failed to set index number for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/settings/all")
async def get_all_settings(unit_id: str, db: Session = Depends(get_db)):
"""Get all device settings for verification."""
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:
settings = await client.get_all_settings()
return {"status": "ok", "settings": settings}
except Exception as e:
logger.error(f"Failed to get all settings for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))

View File

@@ -465,6 +465,146 @@ class NL43Client:
logger.info(f"DRD stream ended for {self.device_key}") logger.info(f"DRD stream ended for {self.device_key}")
async def set_measurement_time(self, preset: str):
"""Set measurement time preset.
Args:
preset: Time preset (10s, 1m, 5m, 10m, 15m, 30m, 1h, 8h, 24h, or custom like "00:05:30")
"""
await self._send_command(f"Measurement Time Preset Manual,{preset}\r\n")
logger.info(f"Set measurement time to {preset} on {self.device_key}")
async def get_measurement_time(self) -> str:
"""Get current measurement time preset.
Returns: Current time preset setting
"""
resp = await self._send_command("Measurement Time Preset Manual?\r\n")
return resp.strip()
async def set_leq_interval(self, preset: str):
"""Set Leq calculation interval preset.
Args:
preset: Interval preset (Off, 10s, 1m, 5m, 10m, 15m, 30m, 1h, 8h, 24h, or custom like "00:05:30")
"""
await self._send_command(f"Leq Calculation Interval Preset,{preset}\r\n")
logger.info(f"Set Leq interval to {preset} on {self.device_key}")
async def get_leq_interval(self) -> str:
"""Get current Leq calculation interval preset.
Returns: Current interval preset setting
"""
resp = await self._send_command("Leq Calculation Interval Preset?\r\n")
return resp.strip()
async def set_lp_interval(self, preset: str):
"""Set Lp store interval.
Args:
preset: Store interval (Off, 10ms, 25ms, 100ms, 200ms, 1s)
"""
await self._send_command(f"Lp Store Interval,{preset}\r\n")
logger.info(f"Set Lp interval to {preset} on {self.device_key}")
async def get_lp_interval(self) -> str:
"""Get current Lp store interval.
Returns: Current store interval setting
"""
resp = await self._send_command("Lp Store Interval?\r\n")
return resp.strip()
async def set_index_number(self, index: int):
"""Set index number for file numbering.
Args:
index: Index number (0000-9999)
"""
if not 0 <= index <= 9999:
raise ValueError("Index must be between 0000 and 9999")
await self._send_command(f"Index Number,{index:04d}\r\n")
logger.info(f"Set index number to {index:04d} on {self.device_key}")
async def get_index_number(self) -> str:
"""Get current index number.
Returns: Current index number
"""
resp = await self._send_command("Index Number?\r\n")
return resp.strip()
async def get_all_settings(self) -> dict:
"""Query all device settings for verification.
Returns: Dictionary with all current device settings
"""
settings = {}
# Measurement settings
try:
settings["measurement_state"] = await self.get_measurement_state()
except Exception as e:
settings["measurement_state"] = f"Error: {e}"
try:
settings["frequency_weighting"] = await self.get_frequency_weighting()
except Exception as e:
settings["frequency_weighting"] = f"Error: {e}"
try:
settings["time_weighting"] = await self.get_time_weighting()
except Exception as e:
settings["time_weighting"] = f"Error: {e}"
# Timing/interval settings
try:
settings["measurement_time"] = await self.get_measurement_time()
except Exception as e:
settings["measurement_time"] = f"Error: {e}"
try:
settings["leq_interval"] = await self.get_leq_interval()
except Exception as e:
settings["leq_interval"] = f"Error: {e}"
try:
settings["lp_interval"] = await self.get_lp_interval()
except Exception as e:
settings["lp_interval"] = f"Error: {e}"
try:
settings["index_number"] = await self.get_index_number()
except Exception as e:
settings["index_number"] = f"Error: {e}"
# Device info
try:
settings["battery_level"] = await self.get_battery_level()
except Exception as e:
settings["battery_level"] = f"Error: {e}"
try:
settings["clock"] = await self.get_clock()
except Exception as e:
settings["clock"] = f"Error: {e}"
# Sleep mode
try:
settings["sleep_mode"] = await self.get_sleep_status()
except Exception as e:
settings["sleep_mode"] = f"Error: {e}"
# FTP status
try:
settings["ftp_status"] = await self.get_ftp_status()
except Exception as e:
settings["ftp_status"] = f"Error: {e}"
logger.info(f"Retrieved all settings for {self.device_key}")
return settings
async def enable_ftp(self): async def enable_ftp(self):
"""Enable FTP server on the device. """Enable FTP server on the device.

View File

@@ -594,3 +594,18 @@
2025-12-24 06:46:07,062 - app.services - INFO - Sending command to 63.45.161.30:2255: Sleep Mode,On 2025-12-24 06:46:07,062 - app.services - INFO - Sending command to 63.45.161.30:2255: Sleep Mode,On
2025-12-24 06:46:07,221 - app.services - INFO - Device 63.45.161.30:2255 entering sleep mode 2025-12-24 06:46:07,221 - app.services - INFO - Device 63.45.161.30:2255 entering sleep mode
2025-12-24 06:46:07,222 - app.routers - INFO - Put device nl43-1 to sleep 2025-12-24 06:46:07,222 - app.routers - INFO - Put device nl43-1 to sleep
2025-12-24 07:24:24,390 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
2025-12-24 07:24:29,391 - app.services - ERROR - Connection timeout to 63.45.161.30:2255
2025-12-24 07:24:29,391 - app.routers - ERROR - Failed to get live status for nl43-1: Failed to connect to device at 63.45.161.30:2255
2025-12-24 20:37:45,341 - app.main - INFO - Database tables initialized
2025-12-24 20:37:45,342 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 20:38:04,141 - app.main - INFO - Database tables initialized
2025-12-24 20:38:04,142 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 20:38:40,693 - app.main - INFO - Database tables initialized
2025-12-24 20:38:40,693 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 20:39:08,484 - app.main - INFO - Database tables initialized
2025-12-24 20:39:08,484 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 21:22:06,678 - app.main - INFO - Database tables initialized
2025-12-24 21:22:06,679 - app.main - INFO - CORS allowed origins: ['*']
2025-12-24 21:22:38,825 - app.main - INFO - Database tables initialized
2025-12-24 21:22:38,825 - app.main - INFO - CORS allowed origins: ['*']

View File

@@ -240,6 +240,210 @@ Sets the time weighting.
- `S` - Slow (1s) - `S` - Slow (1s)
- `I` - Impulse (35ms) - `I` - Impulse (35ms)
## Timing and Interval Configuration
### Get Measurement Time
```
GET /{unit_id}/measurement-time
```
Gets the current measurement time preset.
**Response:**
```json
{
"status": "ok",
"measurement_time": "1h"
}
```
### Set Measurement Time
```
PUT /{unit_id}/measurement-time
```
Sets the measurement time preset.
**Request Body:**
```json
{
"preset": "1h"
}
```
**Preset Values:**
- `10s`, `1m`, `5m`, `10m`, `15m`, `30m`, `1h`, `8h`, `24h`
- Custom format: `HH:MM:SS` (e.g., `00:05:30` for 5.5 minutes)
### Get Leq Calculation Interval
```
GET /{unit_id}/leq-interval
```
Gets the current Leq calculation interval.
**Response:**
```json
{
"status": "ok",
"leq_interval": "1m"
}
```
### Set Leq Calculation Interval
```
PUT /{unit_id}/leq-interval
```
Sets the Leq calculation interval preset.
**Request Body:**
```json
{
"preset": "1m"
}
```
**Preset Values:**
- `Off`, `10s`, `1m`, `5m`, `10m`, `15m`, `30m`, `1h`, `8h`, `24h`
- Custom format: `HH:MM:SS` (e.g., `00:05:30` for 5.5 minutes)
### Get Lp Store Interval
```
GET /{unit_id}/lp-interval
```
Gets the current Lp store interval.
**Response:**
```json
{
"status": "ok",
"lp_interval": "1s"
}
```
### Set Lp Store Interval
```
PUT /{unit_id}/lp-interval
```
Sets the Lp store interval.
**Request Body:**
```json
{
"preset": "1s"
}
```
**Preset Values:**
- `Off`, `10ms`, `25ms`, `100ms`, `200ms`, `1s`
### Get Index Number
```
GET /{unit_id}/index-number
```
Gets the current index number for file numbering.
**Response:**
```json
{
"status": "ok",
"index_number": "0042"
}
```
### Set Index Number
```
PUT /{unit_id}/index-number
```
Sets the index number for file numbering. This number is incremented with each measurement and used in file names.
**Request Body:**
```json
{
"index": 42
}
```
**Valid Range:** 0000 to 9999
## Device Settings Query
### Get All Settings
```
GET /{unit_id}/settings/all
```
Retrieves all current device settings for verification. This is useful for confirming device configuration before starting measurements.
**Response:**
```json
{
"status": "ok",
"settings": {
"measurement_state": "Stop",
"frequency_weighting": "A",
"time_weighting": "F",
"measurement_time": "1h",
"leq_interval": "1m",
"lp_interval": "1s",
"index_number": "0042",
"battery_level": "80",
"clock": "2025/12/24,02:30:15",
"sleep_mode": "Off",
"ftp_status": "Off"
}
}
```
**Note:** If any setting query fails, the error message will be included in the response for that setting (e.g., `"frequency_weighting": "Error: Connection timeout"`).
## Data Retrieval
### Get Final Results
```
GET /{unit_id}/results
```
Retrieves the final calculation results (DLC) from the last completed measurement.
**Response:**
```json
{
"status": "ok",
"data": {
"leq": "68.4",
"lmax": "82.1",
"lmin": "42.3",
"lpeak": "89.5"
}
}
```
## Power Management
### Sleep Device
```
POST /{unit_id}/sleep
```
Enables Sleep Mode on the device. When enabled, the device will automatically enter sleep mode between Timer Auto measurements.
**Note:** This is a SETTING, not a command to sleep immediately. Sleep Mode only applies when using Timer Auto measurements.
### Wake Device
```
POST /{unit_id}/wake
```
Disables Sleep Mode on the device.
### Get Sleep Status
```
GET /{unit_id}/sleep/status
```
Gets the current Sleep Mode status.
**Response:**
```json
{
"status": "ok",
"sleep_mode": "Off"
}
```
## FTP File Management ## FTP File Management
### Enable FTP ### Enable FTP
@@ -344,8 +548,46 @@ All endpoints return standard HTTP status codes:
### Terra-view Integration Example ### Terra-view Integration Example
```javascript ```javascript
// Get live status from all devices
const devices = ['nl43-1', 'nl43-2', 'nl43-3']; const devices = ['nl43-1', 'nl43-2', 'nl43-3'];
// Configure all devices before measurement
for (const device of devices) {
// Set measurement time to 12 hours
await fetch(`http://localhost:8000/api/nl43/${device}/measurement-time`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preset: '12h' })
});
// Set Leq interval to 1 minute
await fetch(`http://localhost:8000/api/nl43/${device}/leq-interval`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preset: '1m' })
});
// Set index number for daily file organization
const dayNumber = new Date().getDate();
await fetch(`http://localhost:8000/api/nl43/${device}/index-number`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ index: dayNumber })
});
// Verify all settings are correct
const settings = await fetch(`http://localhost:8000/api/nl43/${device}/settings/all`)
.then(r => r.json());
console.log(`Device ${device} settings:`, settings.settings);
}
// Start measurement on all devices at 7pm
await Promise.all(
devices.map(id =>
fetch(`http://localhost:8000/api/nl43/${id}/start`, { method: 'POST' })
)
);
// Get live status from all devices
const statuses = await Promise.all( const statuses = await Promise.all(
devices.map(id => devices.map(id =>
fetch(`http://localhost:8000/api/nl43/${id}/live`) fetch(`http://localhost:8000/api/nl43/${id}/live`)
@@ -353,25 +595,18 @@ const statuses = await Promise.all(
) )
); );
// Start measurement on all devices // Download files from all devices the next morning
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) { for (const device of devices) {
// Enable FTP // Enable FTP
await fetch(`http://localhost:8000/api/nl43/${device}/ftp/enable`, { await fetch(`http://localhost:8000/api/nl43/${device}/ftp/enable`, {
method: 'POST' method: 'POST'
}); });
// List files // List files in device data directory
const res = await fetch(`http://localhost:8000/api/nl43/${device}/ftp/files?path=/NL43_DATA`); const res = await fetch(`http://localhost:8000/api/nl43/${device}/ftp/files?path=/NL43_DATA`);
const { files } = await res.json(); const { files } = await res.json();
// Download latest file // Download latest measurement file
const latestFile = files const latestFile = files
.filter(f => !f.is_dir) .filter(f => !f.is_dir)
.sort((a, b) => b.modified - a.modified)[0]; .sort((a, b) => b.modified - a.modified)[0];
@@ -384,10 +619,10 @@ for (const device of devices) {
}); });
const blob = await download.blob(); const blob = await download.blob();
// Process blob... // Save to local storage or process...
} }
// Disable FTP, re-enable TCP // Disable FTP to re-enable TCP
await fetch(`http://localhost:8000/api/nl43/${device}/ftp/disable`, { await fetch(`http://localhost:8000/api/nl43/${device}/ftp/disable`, {
method: 'POST' method: 'POST'
}); });

View File

@@ -57,6 +57,7 @@
<fieldset> <fieldset>
<legend>Measurement Settings</legend> <legend>Measurement Settings</legend>
<button onclick="getAllSettings()" style="margin-bottom: 12px; font-weight: bold;">Get ALL Settings</button>
<div style="margin-bottom: 8px;"> <div style="margin-bottom: 8px;">
<label style="display: inline; margin-right: 8px;">Frequency Weighting:</label> <label style="display: inline; margin-right: 8px;">Frequency Weighting:</label>
<button onclick="getFreqWeighting()">Get</button> <button onclick="getFreqWeighting()">Get</button>
@@ -280,6 +281,29 @@
} }
// Measurement settings functions // Measurement settings functions
async function getAllSettings() {
const unitId = document.getElementById('unitId').value;
log('Retrieving all device settings (this may take 10-15 seconds)...');
const res = await fetch(`/api/nl43/${unitId}/settings`);
const data = await res.json();
if (!res.ok) {
log(`Get All Settings failed: ${res.status} - ${data.detail || JSON.stringify(data)}`);
return;
}
// Display in status area
statusEl.textContent = JSON.stringify(data.settings, null, 2);
// Log summary
log('=== ALL DEVICE SETTINGS ===');
Object.entries(data.settings).forEach(([key, value]) => {
log(`${key}: ${value}`);
});
log('===========================');
}
async function getFreqWeighting() { async function getFreqWeighting() {
const unitId = document.getElementById('unitId').value; const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/frequency-weighting?channel=Main`); const res = await fetch(`/api/nl43/${unitId}/frequency-weighting?channel=Main`);

98
test_settings_endpoint.py Normal file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
Test script to demonstrate the GET /api/nl43/{unit_id}/settings endpoint.
This endpoint retrieves all current device settings for verification purposes.
"""
import asyncio
import sys
async def test_settings_retrieval():
"""Test the settings retrieval functionality."""
from app.services import NL43Client
# Example configuration - adjust these to match your actual device
host = "192.168.1.100" # Replace with your NL43 device IP
port = 80
unit_id = "NL43-001"
print(f"Connecting to NL43 device at {host}:{port}...")
print(f"Unit ID: {unit_id}\n")
client = NL43Client(host, port)
try:
print("Retrieving all device settings...")
settings = await client.get_all_settings()
print("\n" + "="*60)
print("DEVICE SETTINGS SUMMARY")
print("="*60)
for key, value in settings.items():
print(f"{key:.<30} {value}")
print("="*60)
print(f"\nTotal settings retrieved: {len(settings)}")
print("\n✓ Settings retrieval successful!")
except ConnectionError as e:
print(f"\n✗ Connection Error: {e}")
print("\nTroubleshooting:")
print(" 1. Verify the device IP address and port")
print(" 2. Ensure the device is powered on and connected to the network")
print(" 3. Check firewall settings")
sys.exit(1)
except Exception as e:
print(f"\n✗ Error: {e}")
sys.exit(1)
async def test_api_endpoint():
"""Demonstrate how to call the API endpoint."""
print("\n" + "="*60)
print("API ENDPOINT USAGE")
print("="*60)
print("\nTo retrieve all settings via the API, use:")
print("\n GET /api/nl43/{unit_id}/settings")
print("\nExample with curl:")
print("\n curl http://localhost:8000/api/nl43/NL43-001/settings")
print("\nExample response:")
print("""
{
"status": "ok",
"unit_id": "NL43-001",
"settings": {
"measurement_state": "Stop",
"frequency_weighting": "A",
"time_weighting": "F",
"measurement_time": "00:01:00",
"leq_interval": "1s",
"lp_interval": "125ms",
"index_number": "0",
"battery_level": "100%",
"clock": "2025/12/24,20:45:30",
"sleep_mode": "Off",
"ftp_status": "On"
}
}
""")
print("="*60)
if __name__ == "__main__":
print("NL43 Settings Retrieval Test")
print("="*60)
print("\nThis test demonstrates the new /api/nl43/{unit_id}/settings endpoint")
print("which allows you to view all current device settings for verification.\n")
# Show API usage
asyncio.run(test_api_endpoint())
# Uncomment below to test actual device connection
# asyncio.run(test_settings_retrieval())
print("\n✓ Test completed!")