diff --git a/FEATURE_SUMMARY.md b/FEATURE_SUMMARY.md new file mode 100644 index 0000000..f311f0d --- /dev/null +++ b/FEATURE_SUMMARY.md @@ -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 diff --git a/README.md b/README.md index edc286e..acc5c50 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ Logs are written to: | 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) | | PUT | `/api/nl43/{unit_id}/frequency-weighting` | Set frequency weighting | | 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 ``` +### 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) ```javascript const ws = new WebSocket('ws://localhost:8100/api/nl43/meter-001/stream'); diff --git a/SETTINGS_ENDPOINT.md b/SETTINGS_ENDPOINT.md new file mode 100644 index 0000000..faa856a --- /dev/null +++ b/SETTINGS_ENDPOINT.md @@ -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 { + 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 diff --git a/UI_UPDATE.md b/UI_UPDATE.md new file mode 100644 index 0000000..6550b57 --- /dev/null +++ b/UI_UPDATE.md @@ -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 + +``` + +### 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 diff --git a/app/__pycache__/routers.cpython-310.pyc b/app/__pycache__/routers.cpython-310.pyc index 833ee93..9101142 100644 Binary files a/app/__pycache__/routers.cpython-310.pyc and b/app/__pycache__/routers.cpython-310.pyc differ diff --git a/app/__pycache__/services.cpython-310.pyc b/app/__pycache__/services.cpython-310.pyc index 79b077c..7972133 100644 Binary files a/app/__pycache__/services.cpython-310.pyc and b/app/__pycache__/services.cpython-310.pyc differ diff --git a/app/routers.py b/app/routers.py index 3b1509e..c9f17db 100644 --- a/app/routers.py +++ b/app/routers.py @@ -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)}") +@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") async def list_ftp_files(unit_id: str, path: str = "/", db: Session = Depends(get_db)): """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") +class TimingPayload(BaseModel): + preset: str + + +class IndexPayload(BaseModel): + index: int + + class DownloadRequest(BaseModel): remote_path: str @@ -790,3 +834,178 @@ async def download_ftp_file(unit_id: str, payload: DownloadRequest, db: Session except Exception as e: logger.error(f"Unexpected error downloading file from {unit_id}: {e}") 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)) diff --git a/app/services.py b/app/services.py index fcb0cc5..8c9ad80 100644 --- a/app/services.py +++ b/app/services.py @@ -465,6 +465,146 @@ class NL43Client: 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): """Enable FTP server on the device. diff --git a/data/slmm.log b/data/slmm.log index 8068ceb..50bfbb8 100644 --- a/data/slmm.log +++ b/data/slmm.log @@ -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,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 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: ['*'] diff --git a/API.md b/docs/API.md similarity index 59% rename from API.md rename to docs/API.md index ff0d276..0ed5ca1 100644 --- a/API.md +++ b/docs/API.md @@ -240,6 +240,210 @@ Sets the time weighting. - `S` - Slow (1s) - `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 ### Enable FTP @@ -344,8 +548,46 @@ All endpoints return standard HTTP status codes: ### Terra-view Integration Example ```javascript -// Get live status from all devices 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( devices.map(id => fetch(`http://localhost:8000/api/nl43/${id}/live`) @@ -353,25 +595,18 @@ const statuses = await Promise.all( ) ); -// 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 +// Download files from all devices the next morning for (const device of devices) { // Enable FTP await fetch(`http://localhost:8000/api/nl43/${device}/ftp/enable`, { 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 { files } = await res.json(); - // Download latest file + // Download latest measurement file const latestFile = files .filter(f => !f.is_dir) .sort((a, b) => b.modified - a.modified)[0]; @@ -384,10 +619,10 @@ for (const device of devices) { }); 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`, { method: 'POST' }); diff --git a/COMMUNICATION_GUIDE.md b/docs/COMMUNICATION_GUIDE.md similarity index 100% rename from COMMUNICATION_GUIDE.md rename to docs/COMMUNICATION_GUIDE.md diff --git a/IMPROVEMENTS.md b/docs/IMPROVEMENTS.md similarity index 100% rename from IMPROVEMENTS.md rename to docs/IMPROVEMENTS.md diff --git a/NL43_COMMANDS.md b/docs/NL43_COMMANDS.md similarity index 100% rename from NL43_COMMANDS.md rename to docs/NL43_COMMANDS.md diff --git a/NL43_quickref.md b/docs/NL43_quickref.md similarity index 100% rename from NL43_quickref.md rename to docs/NL43_quickref.md diff --git a/nl43_Command_ref.md b/docs/nl43_Command_ref.md similarity index 100% rename from nl43_Command_ref.md rename to docs/nl43_Command_ref.md diff --git a/templates/index.html b/templates/index.html index 3c8e979..0b94b2b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -57,6 +57,7 @@
Measurement Settings +
@@ -280,6 +281,29 @@ } // 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() { const unitId = document.getElementById('unitId').value; const res = await fetch(`/api/nl43/${unitId}/frequency-weighting?channel=Main`); diff --git a/test_settings_endpoint.py b/test_settings_endpoint.py new file mode 100644 index 0000000..94923a9 --- /dev/null +++ b/test_settings_endpoint.py @@ -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!")