Compare commits
10 Commits
db6fd56673
...
a297e6c5fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a297e6c5fe | ||
|
|
6ac60eb380 | ||
|
|
b1267f47b2 | ||
|
|
8f93c345fe | ||
|
|
92b6173f5f | ||
|
|
f9139d6aa3 | ||
|
|
c90544a712 | ||
|
|
12d512a515 | ||
|
|
60c95e825d | ||
|
|
316cfa84f8 |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1 +1,14 @@
|
||||
/manuals/
|
||||
/data/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
*.egg
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
195
FEATURE_SUMMARY.md
Normal file
195
FEATURE_SUMMARY.md
Normal 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
|
||||
398
README.md
398
README.md
@@ -1,17 +1,389 @@
|
||||
# slmm
|
||||
Standalone NL43 addon module (keep separate from the SFM/terra-view codebase).
|
||||
# SLMM - Sound Level Meter Manager
|
||||
|
||||
Run the addon API:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload --port 8100
|
||||
Backend API service for controlling and monitoring Rion NL-43/NL-53 Sound Level Meters via TCP and FTP protocols.
|
||||
|
||||
## Overview
|
||||
|
||||
SLMM is a standalone backend module that provides REST API routing and command translation for NL43/NL53 sound level meters. This service acts as a bridge between the hardware devices and frontend applications, handling all device communication, data persistence, and protocol management.
|
||||
|
||||
**Note:** This is a backend-only service. Actual user interfacing is done via [SFM/Terra-View](https://github.com/your-org/terra-view) frontend applications.
|
||||
|
||||
## Features
|
||||
|
||||
- **Device Management**: Configure and manage multiple NL43/NL53 devices
|
||||
- **Real-time Monitoring**: Stream live measurement data via WebSocket
|
||||
- **Measurement Control**: Start, stop, pause, resume, and reset measurements
|
||||
- **Data Retrieval**: Access current and historical measurement snapshots
|
||||
- **FTP Integration**: Download measurement files directly from devices
|
||||
- **Device Configuration**: Manage frequency/time weighting, clock sync, and more
|
||||
- **Rate Limiting**: Automatic 1-second delay enforcement between device commands
|
||||
- **Persistent Storage**: SQLite database for device configs and measurement cache
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
|
||||
│ Terra-View UI │◄───────►│ SLMM API │◄───────►│ NL43/NL53 │
|
||||
│ (Frontend) │ HTTP │ (Backend) │ TCP │ Sound Meters │
|
||||
└─────────────────┘ └──────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ SQLite DB │
|
||||
│ (Cache) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
- `GET /health`
|
||||
- `GET /api/nl43/{unit_id}/config`
|
||||
- `PUT /api/nl43/{unit_id}/config`
|
||||
- `GET /api/nl43/{unit_id}/status`
|
||||
- `POST /api/nl43/{unit_id}/status`
|
||||
## Quick Start
|
||||
|
||||
Use `app/services.py` to wire in the TCP connector and call `persist_snapshot` with parsed DOD/DRD data.
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- pip package manager
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd slmm
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Running the Server
|
||||
|
||||
```bash
|
||||
# Development mode with auto-reload
|
||||
uvicorn app.main:app --reload --port 8100
|
||||
|
||||
# Production mode
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8100
|
||||
```
|
||||
|
||||
The API will be available at `http://localhost:8100`
|
||||
|
||||
### API Documentation
|
||||
|
||||
Once running, visit:
|
||||
- Swagger UI: `http://localhost:8100/docs`
|
||||
- ReDoc: `http://localhost:8100/redoc`
|
||||
- Health Check: `http://localhost:8100/health`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `PORT`: Server port (default: 8100)
|
||||
- `CORS_ORIGINS`: Comma-separated list of allowed origins (default: "*")
|
||||
|
||||
### Database
|
||||
|
||||
The SQLite database is automatically created at [data/slmm.db](data/slmm.db) on first run.
|
||||
|
||||
### Logging
|
||||
|
||||
Logs are written to:
|
||||
- Console output (stdout)
|
||||
- [data/slmm.log](data/slmm.log) file
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Device Configuration
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/nl43/{unit_id}/config` | Get device configuration |
|
||||
| PUT | `/api/nl43/{unit_id}/config` | Update device configuration |
|
||||
|
||||
### Device Status
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/nl43/{unit_id}/status` | Get cached measurement snapshot |
|
||||
| GET | `/api/nl43/{unit_id}/live` | Request fresh DOD data from device |
|
||||
| WS | `/api/nl43/{unit_id}/stream` | WebSocket stream for real-time DRD data |
|
||||
|
||||
### Measurement Control
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/nl43/{unit_id}/start` | Start measurement |
|
||||
| POST | `/api/nl43/{unit_id}/stop` | Stop measurement |
|
||||
| POST | `/api/nl43/{unit_id}/pause` | Pause measurement |
|
||||
| POST | `/api/nl43/{unit_id}/resume` | Resume paused measurement |
|
||||
| POST | `/api/nl43/{unit_id}/reset` | Reset measurement data |
|
||||
| POST | `/api/nl43/{unit_id}/store` | Manually store data to SD card |
|
||||
|
||||
### Device Information
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/nl43/{unit_id}/battery` | Get battery level |
|
||||
| GET | `/api/nl43/{unit_id}/clock` | Get device clock time |
|
||||
| PUT | `/api/nl43/{unit_id}/clock` | Set device clock time |
|
||||
| GET | `/api/nl43/{unit_id}/results` | Get final calculation results (DLC) |
|
||||
|
||||
### Measurement Settings
|
||||
|
||||
| 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) |
|
||||
| PUT | `/api/nl43/{unit_id}/time-weighting` | Set time weighting |
|
||||
|
||||
### Sleep Mode
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/nl43/{unit_id}/sleep` | Put device into sleep mode |
|
||||
| POST | `/api/nl43/{unit_id}/wake` | Wake device from sleep |
|
||||
| GET | `/api/nl43/{unit_id}/sleep/status` | Get sleep mode status |
|
||||
|
||||
### FTP File Management
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/nl43/{unit_id}/ftp/enable` | Enable FTP server on device |
|
||||
| POST | `/api/nl43/{unit_id}/ftp/disable` | Disable FTP server on device |
|
||||
| GET | `/api/nl43/{unit_id}/ftp/status` | Get FTP server status |
|
||||
| GET | `/api/nl43/{unit_id}/ftp/files` | List files on device |
|
||||
| POST | `/api/nl43/{unit_id}/ftp/download` | Download file from device |
|
||||
|
||||
For detailed API documentation and examples, see [API.md](API.md).
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
slmm/
|
||||
├── app/
|
||||
│ ├── __init__.py # Package initialization
|
||||
│ ├── main.py # FastAPI application and startup
|
||||
│ ├── routers.py # API route definitions
|
||||
│ ├── models.py # SQLAlchemy database models
|
||||
│ ├── services.py # NL43Client and business logic
|
||||
│ └── database.py # Database configuration
|
||||
├── data/
|
||||
│ ├── slmm.db # SQLite database (auto-created)
|
||||
│ ├── slmm.log # Application logs
|
||||
│ └── downloads/ # Downloaded files from devices
|
||||
├── templates/
|
||||
│ └── index.html # Simple web interface (optional)
|
||||
├── manuals/ # Device documentation
|
||||
├── API.md # Detailed API documentation
|
||||
├── COMMUNICATION_GUIDE.md # NL43 protocol documentation
|
||||
├── NL43_COMMANDS.md # Command reference
|
||||
├── requirements.txt # Python dependencies
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### NL43Config Table
|
||||
Stores device connection configuration:
|
||||
- `unit_id` (PK): Unique device identifier
|
||||
- `host`: Device IP address or hostname
|
||||
- `tcp_port`: TCP control port (default: 80)
|
||||
- `tcp_enabled`: Enable/disable TCP communication
|
||||
- `ftp_enabled`: Enable/disable FTP functionality
|
||||
- `ftp_username`: FTP authentication username
|
||||
- `ftp_password`: FTP authentication password
|
||||
- `web_enabled`: Enable/disable web interface access
|
||||
|
||||
### NL43Status Table
|
||||
Caches latest measurement snapshot:
|
||||
- `unit_id` (PK): Unique device identifier
|
||||
- `last_seen`: Timestamp of last update
|
||||
- `measurement_state`: Current state (Measure/Stop)
|
||||
- `lp`: Instantaneous sound pressure level
|
||||
- `leq`: Equivalent continuous sound level
|
||||
- `lmax`: Maximum sound level
|
||||
- `lmin`: Minimum sound level
|
||||
- `lpeak`: Peak sound level
|
||||
- `battery_level`: Battery percentage
|
||||
- `power_source`: Current power source
|
||||
- `sd_remaining_mb`: Free SD card space (MB)
|
||||
- `sd_free_ratio`: SD card free space ratio
|
||||
- `raw_payload`: Raw device response data
|
||||
|
||||
## Protocol Details
|
||||
|
||||
### TCP Communication
|
||||
- Uses ASCII command protocol over TCP
|
||||
- Enforces ≥1 second delay between commands to same device
|
||||
- Two-line response format:
|
||||
- Line 1: Result code (R+0000 for success)
|
||||
- Line 2: Data payload (for query commands)
|
||||
|
||||
### FTP Communication
|
||||
- Uses active mode FTP (requires device to connect back)
|
||||
- TCP and FTP are mutually exclusive on the device
|
||||
- Credentials configurable per device
|
||||
|
||||
### Data Formats
|
||||
|
||||
**DOD (Data Output Display)**: Snapshot of current display values
|
||||
**DRD (Data Real-time Display)**: Continuous streaming data
|
||||
**DLC (Data Last Calculation)**: Final stored measurement results
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Configure a Device
|
||||
```bash
|
||||
curl -X PUT http://localhost:8100/api/nl43/meter-001/config \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"host": "192.168.1.100",
|
||||
"tcp_port": 2255,
|
||||
"tcp_enabled": true,
|
||||
"ftp_username": "admin",
|
||||
"ftp_password": "password"
|
||||
}'
|
||||
```
|
||||
|
||||
### Start Measurement
|
||||
```bash
|
||||
curl -X POST http://localhost:8100/api/nl43/meter-001/start
|
||||
```
|
||||
|
||||
### Get Live Status
|
||||
```bash
|
||||
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');
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('Live measurement:', data);
|
||||
};
|
||||
```
|
||||
|
||||
### Download Files via FTP
|
||||
```bash
|
||||
# Enable FTP
|
||||
curl -X POST http://localhost:8100/api/nl43/meter-001/ftp/enable
|
||||
|
||||
# List files
|
||||
curl http://localhost:8100/api/nl43/meter-001/ftp/files?path=/NL43_DATA
|
||||
|
||||
# Download file
|
||||
curl -X POST http://localhost:8100/api/nl43/meter-001/ftp/download \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"remote_path": "/NL43_DATA/measurement.wav"}' \
|
||||
--output measurement.wav
|
||||
|
||||
# Disable FTP
|
||||
curl -X POST http://localhost:8100/api/nl43/meter-001/ftp/disable
|
||||
```
|
||||
|
||||
## Integration with Terra-View
|
||||
|
||||
This backend is designed to be consumed by the Terra-View frontend application. The frontend should:
|
||||
|
||||
1. Use the config endpoints to register and configure devices
|
||||
2. Poll or stream live status for real-time monitoring
|
||||
3. Use control endpoints to manage measurements
|
||||
4. Download files via FTP endpoints for analysis
|
||||
|
||||
See [API.md](API.md) for detailed integration examples.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
- Verify device IP address and port in configuration
|
||||
- Ensure device is on the same network
|
||||
- Check firewall rules allow TCP/FTP connections
|
||||
- Verify RX55 network adapter is properly configured on device
|
||||
|
||||
### Rate Limiting
|
||||
- API automatically enforces 1-second delay between commands
|
||||
- If experiencing delays, this is normal device behavior
|
||||
- Multiple devices can be controlled in parallel
|
||||
|
||||
### FTP Active Mode
|
||||
- Ensure server can accept incoming connections from device
|
||||
- FTP uses active mode (device connects back to server)
|
||||
- May require firewall configuration for data channel
|
||||
|
||||
### WebSocket Disconnects
|
||||
- WebSocket streams maintain persistent connection
|
||||
- Limit concurrent streams to avoid device overload
|
||||
- Connection will auto-close if device stops responding
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# Add test commands when implemented
|
||||
pytest
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
```bash
|
||||
# Migrate existing database to add FTP credentials
|
||||
python migrate_add_ftp_credentials.py
|
||||
|
||||
# Set FTP credentials for a device
|
||||
python set_ftp_credentials.py <unit_id> <username> <password>
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a standalone module kept separate from the SFM/Terra-View codebase. When contributing:
|
||||
|
||||
1. Maintain separation from frontend code
|
||||
2. Follow existing API patterns and error handling
|
||||
3. Update API documentation for new endpoints
|
||||
4. Ensure rate limiting is enforced for device commands
|
||||
|
||||
## License
|
||||
|
||||
[Specify license here]
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [API.md](API.md) - Complete API reference with examples
|
||||
- [COMMUNICATION_GUIDE.md](COMMUNICATION_GUIDE.md) - NL43 protocol details
|
||||
- [NL43_COMMANDS.md](NL43_COMMANDS.md) - Device command reference
|
||||
- [manuals/](manuals/) - Device manufacturer documentation
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- Backend API issues: This repository
|
||||
- Frontend/UI issues: Terra-View repository
|
||||
- Device protocol questions: See COMMUNICATION_GUIDE.md
|
||||
|
||||
258
SETTINGS_ENDPOINT.md
Normal file
258
SETTINGS_ENDPOINT.md
Normal 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
171
UI_UPDATE.md
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -73,7 +73,7 @@ async def health_devices():
|
||||
configs = db.query(NL43Config).filter_by(tcp_enabled=True).all()
|
||||
|
||||
for cfg in configs:
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, timeout=2.0)
|
||||
client = NL43Client(cfg.host, cfg.tcp_port, timeout=2.0, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
|
||||
status = {
|
||||
"unit_id": cfg.unit_id,
|
||||
"host": cfg.host,
|
||||
|
||||
@@ -14,6 +14,8 @@ class NL43Config(Base):
|
||||
tcp_port = Column(Integer, default=80) # NL43 TCP control port (via RX55)
|
||||
tcp_enabled = Column(Boolean, default=True)
|
||||
ftp_enabled = Column(Boolean, default=False)
|
||||
ftp_username = Column(String, nullable=True) # FTP login username
|
||||
ftp_password = Column(String, nullable=True) # FTP login password
|
||||
web_enabled = Column(Boolean, default=False)
|
||||
|
||||
|
||||
|
||||
956
app/routers.py
956
app/routers.py
File diff suppressed because it is too large
Load Diff
539
app/services.py
539
app/services.py
@@ -11,8 +11,10 @@ import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.orm import Session
|
||||
from ftplib import FTP
|
||||
from pathlib import Path
|
||||
|
||||
from app.models import NL43Status
|
||||
|
||||
@@ -69,10 +71,12 @@ _rate_limit_lock = asyncio.Lock()
|
||||
|
||||
|
||||
class NL43Client:
|
||||
def __init__(self, host: str, port: int, timeout: float = 5.0):
|
||||
def __init__(self, host: str, port: int, timeout: float = 5.0, ftp_username: str = None, ftp_password: str = None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.ftp_username = ftp_username or "anonymous"
|
||||
self.ftp_password = ftp_password or ""
|
||||
self.device_key = f"{host}:{port}"
|
||||
|
||||
async def _enforce_rate_limit(self):
|
||||
@@ -215,3 +219,534 @@ class NL43Client:
|
||||
According to NL43 protocol: Measure,Stop (no $ prefix, capitalized param)
|
||||
"""
|
||||
await self._send_command("Measure,Stop\r\n")
|
||||
|
||||
async def set_store_mode_manual(self):
|
||||
"""Set the device to Manual Store mode.
|
||||
|
||||
According to NL43 protocol: Store Mode,Manual sets manual storage mode
|
||||
"""
|
||||
await self._send_command("Store Mode,Manual\r\n")
|
||||
logger.info(f"Store mode set to Manual on {self.device_key}")
|
||||
|
||||
async def manual_store(self):
|
||||
"""Manually store the current measurement data.
|
||||
|
||||
According to NL43 protocol: Manual Store,Start executes storing
|
||||
Parameter p1="Start" executes the storage operation
|
||||
Device must be in Manual Store mode first
|
||||
"""
|
||||
await self._send_command("Manual Store,Start\r\n")
|
||||
logger.info(f"Manual store executed on {self.device_key}")
|
||||
|
||||
async def pause(self):
|
||||
"""Pause the current measurement."""
|
||||
await self._send_command("Pause,On\r\n")
|
||||
logger.info(f"Measurement paused on {self.device_key}")
|
||||
|
||||
async def resume(self):
|
||||
"""Resume a paused measurement."""
|
||||
await self._send_command("Pause,Off\r\n")
|
||||
logger.info(f"Measurement resumed on {self.device_key}")
|
||||
|
||||
async def reset(self):
|
||||
"""Reset the measurement data."""
|
||||
await self._send_command("Reset\r\n")
|
||||
logger.info(f"Measurement data reset on {self.device_key}")
|
||||
|
||||
async def get_measurement_state(self) -> str:
|
||||
"""Get the current measurement state.
|
||||
|
||||
Returns: "Start" if measuring, "Stop" if stopped
|
||||
"""
|
||||
resp = await self._send_command("Measure?\r\n")
|
||||
state = resp.strip()
|
||||
logger.info(f"Measurement state on {self.device_key}: {state}")
|
||||
return state
|
||||
|
||||
async def get_battery_level(self) -> str:
|
||||
"""Get the battery level."""
|
||||
resp = await self._send_command("Battery Level?\r\n")
|
||||
logger.info(f"Battery level on {self.device_key}: {resp}")
|
||||
return resp.strip()
|
||||
|
||||
async def get_clock(self) -> str:
|
||||
"""Get the device clock time."""
|
||||
resp = await self._send_command("Clock?\r\n")
|
||||
logger.info(f"Clock on {self.device_key}: {resp}")
|
||||
return resp.strip()
|
||||
|
||||
async def set_clock(self, datetime_str: str):
|
||||
"""Set the device clock time.
|
||||
|
||||
Args:
|
||||
datetime_str: Time in format YYYY/MM/DD,HH:MM:SS or YYYY/MM/DD HH:MM:SS
|
||||
"""
|
||||
# Device expects format: Clock,YYYY/MM/DD HH:MM:SS (space between date and time)
|
||||
# Replace comma with space if present to normalize format
|
||||
normalized = datetime_str.replace(',', ' ', 1)
|
||||
await self._send_command(f"Clock,{normalized}\r\n")
|
||||
logger.info(f"Clock set on {self.device_key} to {normalized}")
|
||||
|
||||
async def get_frequency_weighting(self, channel: str = "Main") -> str:
|
||||
"""Get frequency weighting (A, C, Z, etc.).
|
||||
|
||||
Args:
|
||||
channel: Main, Sub1, Sub2, or Sub3
|
||||
"""
|
||||
resp = await self._send_command(f"Frequency Weighting ({channel})?\r\n")
|
||||
logger.info(f"Frequency weighting ({channel}) on {self.device_key}: {resp}")
|
||||
return resp.strip()
|
||||
|
||||
async def set_frequency_weighting(self, weighting: str, channel: str = "Main"):
|
||||
"""Set frequency weighting.
|
||||
|
||||
Args:
|
||||
weighting: A, C, or Z
|
||||
channel: Main, Sub1, Sub2, or Sub3
|
||||
"""
|
||||
await self._send_command(f"Frequency Weighting ({channel}),{weighting}\r\n")
|
||||
logger.info(f"Frequency weighting ({channel}) set to {weighting} on {self.device_key}")
|
||||
|
||||
async def get_time_weighting(self, channel: str = "Main") -> str:
|
||||
"""Get time weighting (F, S, I).
|
||||
|
||||
Args:
|
||||
channel: Main, Sub1, Sub2, or Sub3
|
||||
"""
|
||||
resp = await self._send_command(f"Time Weighting ({channel})?\r\n")
|
||||
logger.info(f"Time weighting ({channel}) on {self.device_key}: {resp}")
|
||||
return resp.strip()
|
||||
|
||||
async def set_time_weighting(self, weighting: str, channel: str = "Main"):
|
||||
"""Set time weighting.
|
||||
|
||||
Args:
|
||||
weighting: F (Fast), S (Slow), or I (Impulse)
|
||||
channel: Main, Sub1, Sub2, or Sub3
|
||||
"""
|
||||
await self._send_command(f"Time Weighting ({channel}),{weighting}\r\n")
|
||||
logger.info(f"Time weighting ({channel}) set to {weighting} on {self.device_key}")
|
||||
|
||||
async def request_dlc(self) -> dict:
|
||||
"""Request DLC (Data Last Calculation) - final stored measurement results.
|
||||
|
||||
This retrieves the complete calculation results from the last/current measurement,
|
||||
including all statistical data. Similar to DOD but for final results.
|
||||
|
||||
Returns:
|
||||
Dict with parsed DLC data
|
||||
"""
|
||||
resp = await self._send_command("DLC?\r\n")
|
||||
logger.info(f"DLC data received from {self.device_key}: {resp[:100]}...")
|
||||
|
||||
# Parse DLC response - similar format to DOD
|
||||
# The exact format depends on device configuration
|
||||
# For now, return raw data - can be enhanced based on actual response format
|
||||
return {
|
||||
"raw_data": resp.strip(),
|
||||
"device_key": self.device_key,
|
||||
}
|
||||
|
||||
async def sleep(self):
|
||||
"""Put the device into sleep mode to conserve battery.
|
||||
|
||||
Sleep mode is useful for battery conservation between scheduled measurements.
|
||||
Device can be woken up remotely via TCP command or by pressing a button.
|
||||
"""
|
||||
await self._send_command("Sleep Mode,On\r\n")
|
||||
logger.info(f"Device {self.device_key} entering sleep mode")
|
||||
|
||||
async def wake(self):
|
||||
"""Wake the device from sleep mode.
|
||||
|
||||
Note: This may not work if the device is in deep sleep.
|
||||
Physical button press might be required in some cases.
|
||||
"""
|
||||
await self._send_command("Sleep Mode,Off\r\n")
|
||||
logger.info(f"Device {self.device_key} waking from sleep mode")
|
||||
|
||||
async def get_sleep_status(self) -> str:
|
||||
"""Get the current sleep mode status."""
|
||||
resp = await self._send_command("Sleep Mode?\r\n")
|
||||
logger.info(f"Sleep mode status on {self.device_key}: {resp}")
|
||||
return resp.strip()
|
||||
|
||||
async def stream_drd(self, callback):
|
||||
"""Stream continuous DRD output from the device.
|
||||
|
||||
Opens a persistent connection and streams DRD data lines.
|
||||
Calls the provided callback function with each parsed snapshot.
|
||||
|
||||
Args:
|
||||
callback: Async function that receives NL43Snapshot objects
|
||||
|
||||
The stream continues until an exception occurs or the connection is closed.
|
||||
Send SUB character (0x1A) to stop the stream.
|
||||
"""
|
||||
await self._enforce_rate_limit()
|
||||
|
||||
logger.info(f"Starting DRD stream for {self.device_key}")
|
||||
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(self.host, self.port), timeout=self.timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"DRD stream connection timeout to {self.device_key}")
|
||||
raise ConnectionError(f"Failed to connect to device at {self.host}:{self.port}")
|
||||
except Exception as e:
|
||||
logger.error(f"DRD stream connection failed to {self.device_key}: {e}")
|
||||
raise ConnectionError(f"Failed to connect to device: {str(e)}")
|
||||
|
||||
try:
|
||||
# Start DRD streaming
|
||||
writer.write(b"DRD?\r\n")
|
||||
await writer.drain()
|
||||
|
||||
# Read initial result code
|
||||
first_line_data = await asyncio.wait_for(reader.readuntil(b"\n"), timeout=self.timeout)
|
||||
result_code = first_line_data.decode(errors="ignore").strip()
|
||||
|
||||
if result_code.startswith("$"):
|
||||
result_code = result_code[1:].strip()
|
||||
|
||||
logger.debug(f"DRD stream result code from {self.device_key}: {result_code}")
|
||||
|
||||
if result_code != "R+0000":
|
||||
raise ValueError(f"DRD stream failed to start: {result_code}")
|
||||
|
||||
logger.info(f"DRD stream started successfully for {self.device_key}")
|
||||
|
||||
# Continuously read data lines
|
||||
while True:
|
||||
try:
|
||||
line_data = await asyncio.wait_for(reader.readuntil(b"\n"), timeout=30.0)
|
||||
line = line_data.decode(errors="ignore").strip()
|
||||
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Remove leading $ if present
|
||||
if line.startswith("$"):
|
||||
line = line[1:].strip()
|
||||
|
||||
# Parse the DRD data (same format as DOD)
|
||||
parts = [p.strip() for p in line.split(",") if p.strip() != ""]
|
||||
|
||||
if len(parts) < 2:
|
||||
logger.warning(f"Malformed DRD data from {self.device_key}: {line}")
|
||||
continue
|
||||
|
||||
snap = NL43Snapshot(unit_id="", raw_payload=line, measurement_state="Measure")
|
||||
|
||||
# Parse known positions
|
||||
try:
|
||||
if len(parts) >= 1:
|
||||
snap.lp = parts[0]
|
||||
if len(parts) >= 2:
|
||||
snap.leq = parts[1]
|
||||
if len(parts) >= 4:
|
||||
snap.lmax = parts[3]
|
||||
if len(parts) >= 5:
|
||||
snap.lmin = parts[4]
|
||||
if len(parts) >= 11:
|
||||
snap.lpeak = parts[10]
|
||||
except (IndexError, ValueError) as e:
|
||||
logger.warning(f"Error parsing DRD data points: {e}")
|
||||
|
||||
# Call the callback with the snapshot
|
||||
await callback(snap)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"DRD stream timeout (no data for 30s) from {self.device_key}")
|
||||
break
|
||||
except asyncio.IncompleteReadError:
|
||||
logger.info(f"DRD stream closed by device {self.device_key}")
|
||||
break
|
||||
|
||||
finally:
|
||||
# Send SUB character to stop streaming
|
||||
try:
|
||||
writer.write(b"\x1A")
|
||||
await writer.drain()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
writer.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await writer.wait_closed()
|
||||
|
||||
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.
|
||||
|
||||
According to NL43 protocol: FTP,On enables the FTP server
|
||||
"""
|
||||
await self._send_command("FTP,On\r\n")
|
||||
logger.info(f"FTP enabled on {self.device_key}")
|
||||
|
||||
async def disable_ftp(self):
|
||||
"""Disable FTP server on the device.
|
||||
|
||||
According to NL43 protocol: FTP,Off disables the FTP server
|
||||
"""
|
||||
await self._send_command("FTP,Off\r\n")
|
||||
logger.info(f"FTP disabled on {self.device_key}")
|
||||
|
||||
async def get_ftp_status(self) -> str:
|
||||
"""Query FTP server status on the device.
|
||||
|
||||
Returns: "On" or "Off"
|
||||
"""
|
||||
resp = await self._send_command("FTP?\r\n")
|
||||
logger.info(f"FTP status on {self.device_key}: {resp}")
|
||||
return resp.strip()
|
||||
|
||||
async def list_ftp_files(self, remote_path: str = "/") -> List[dict]:
|
||||
"""List files on the device via FTP.
|
||||
|
||||
Args:
|
||||
remote_path: Directory path on the device (default: root)
|
||||
|
||||
Returns:
|
||||
List of file info dicts with 'name', 'size', 'modified', 'is_dir'
|
||||
"""
|
||||
logger.info(f"Listing FTP files on {self.device_key} at {remote_path}")
|
||||
|
||||
def _list_ftp_sync():
|
||||
"""Synchronous FTP listing using ftplib (supports active mode)."""
|
||||
ftp = FTP()
|
||||
ftp.set_debuglevel(0)
|
||||
try:
|
||||
# Connect and login
|
||||
ftp.connect(self.host, 21, timeout=10)
|
||||
ftp.login(self.ftp_username, self.ftp_password)
|
||||
ftp.set_pasv(False) # Force active mode
|
||||
|
||||
# Change to target directory
|
||||
if remote_path != "/":
|
||||
ftp.cwd(remote_path)
|
||||
|
||||
# Get directory listing with details
|
||||
files = []
|
||||
lines = []
|
||||
ftp.retrlines('LIST', lines.append)
|
||||
|
||||
for line in lines:
|
||||
# Parse Unix-style ls output
|
||||
parts = line.split(None, 8)
|
||||
if len(parts) < 9:
|
||||
continue
|
||||
|
||||
is_dir = parts[0].startswith('d')
|
||||
size = int(parts[4]) if not is_dir else 0
|
||||
name = parts[8]
|
||||
|
||||
# Skip . and ..
|
||||
if name in ('.', '..'):
|
||||
continue
|
||||
|
||||
file_info = {
|
||||
"name": name,
|
||||
"path": f"{remote_path.rstrip('/')}/{name}",
|
||||
"size": size,
|
||||
"modified": f"{parts[5]} {parts[6]} {parts[7]}",
|
||||
"is_dir": is_dir,
|
||||
}
|
||||
files.append(file_info)
|
||||
logger.debug(f"Found file: {file_info}")
|
||||
|
||||
logger.info(f"Found {len(files)} files/directories on {self.device_key}")
|
||||
return files
|
||||
|
||||
finally:
|
||||
try:
|
||||
ftp.quit()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Run synchronous FTP in thread pool
|
||||
return await asyncio.to_thread(_list_ftp_sync)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list FTP files on {self.device_key}: {e}")
|
||||
raise ConnectionError(f"FTP connection failed: {str(e)}")
|
||||
|
||||
async def download_ftp_file(self, remote_path: str, local_path: str):
|
||||
"""Download a file from the device via FTP.
|
||||
|
||||
Args:
|
||||
remote_path: Full path to file on the device
|
||||
local_path: Local path where file will be saved
|
||||
"""
|
||||
logger.info(f"Downloading {remote_path} from {self.device_key} to {local_path}")
|
||||
|
||||
def _download_ftp_sync():
|
||||
"""Synchronous FTP download using ftplib (supports active mode)."""
|
||||
ftp = FTP()
|
||||
ftp.set_debuglevel(0)
|
||||
try:
|
||||
# Connect and login
|
||||
ftp.connect(self.host, 21, timeout=10)
|
||||
ftp.login(self.ftp_username, self.ftp_password)
|
||||
ftp.set_pasv(False) # Force active mode
|
||||
|
||||
# Download file
|
||||
with open(local_path, 'wb') as f:
|
||||
ftp.retrbinary(f'RETR {remote_path}', f.write)
|
||||
|
||||
logger.info(f"Successfully downloaded {remote_path} to {local_path}")
|
||||
|
||||
finally:
|
||||
try:
|
||||
ftp.quit()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Run synchronous FTP in thread pool
|
||||
await asyncio.to_thread(_download_ftp_sync)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download {remote_path} from {self.device_key}: {e}")
|
||||
raise ConnectionError(f"FTP download failed: {str(e)}")
|
||||
|
||||
BIN
data/slmm.db
BIN
data/slmm.db
Binary file not shown.
@@ -1,93 +0,0 @@
|
||||
2025-12-23 19:02:07,047 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 19:02:07,048 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 19:02:19,874 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 19:02:19,874 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 19:25:54,057 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 19:25:54,057 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 19:26:08,782 - app.services - INFO - Sending command to 63.45.161.130:2255: DOD?
|
||||
2025-12-23 19:26:13,783 - app.services - ERROR - Connection timeout to 63.45.161.130:2255
|
||||
2025-12-23 19:26:13,783 - app.routers - ERROR - Failed to get live status for nl43-1: Failed to connect to device at 63.45.161.130:2255
|
||||
2025-12-23 19:32:21,893 - app.routers - INFO - Updated config for unit nl43-1
|
||||
2025-12-23 19:32:24,189 - app.services - INFO - Sending command to 63.45.161.130:52255: DOD?
|
||||
2025-12-23 19:32:29,191 - app.services - ERROR - Connection timeout to 63.45.161.130:52255
|
||||
2025-12-23 19:32:29,191 - app.routers - ERROR - Failed to get live status for nl43-1: Failed to connect to device at 63.45.161.130:52255
|
||||
2025-12-23 19:33:01,847 - app.services - INFO - Sending command to 63.45.161.130:52255: DOD?
|
||||
2025-12-23 19:33:06,848 - app.services - ERROR - Connection timeout to 63.45.161.130:52255
|
||||
2025-12-23 19:33:06,848 - app.routers - ERROR - Failed to get live status for nl43-1: Failed to connect to device at 63.45.161.130:52255
|
||||
2025-12-23 19:33:07,508 - app.routers - INFO - Updated config for unit nl43-1
|
||||
2025-12-23 19:33:09,056 - app.services - INFO - Sending command to 63.45.161.130:5225: DOD?
|
||||
2025-12-23 19:33:14,057 - app.services - ERROR - Connection timeout to 63.45.161.130:5225
|
||||
2025-12-23 19:33:14,057 - app.routers - ERROR - Failed to get live status for nl43-1: Failed to connect to device at 63.45.161.130:5225
|
||||
2025-12-23 19:33:22,649 - app.services - INFO - Sending command to 63.45.161.130:5225: $Measure, Start
|
||||
2025-12-23 19:33:27,650 - app.services - ERROR - Connection timeout to 63.45.161.130:5225
|
||||
2025-12-23 19:33:27,650 - app.routers - ERROR - Failed to start measurement on nl43-1: Failed to connect to device at 63.45.161.130:5225
|
||||
2025-12-23 19:39:10,370 - app.routers - INFO - Updated config for unit nl43-1
|
||||
2025-12-23 19:39:12,948 - app.services - INFO - Sending command to 63.45.161.30:5225: DOD?
|
||||
2025-12-23 19:39:17,948 - app.services - ERROR - Connection timeout to 63.45.161.30:5225
|
||||
2025-12-23 19:39:17,948 - app.routers - ERROR - Failed to get live status for nl43-1: Failed to connect to device at 63.45.161.30:5225
|
||||
2025-12-23 19:39:40,472 - app.services - INFO - Sending command to 63.45.161.30:5225: $Measure, Start
|
||||
2025-12-23 19:39:45,472 - app.services - ERROR - Connection timeout to 63.45.161.30:5225
|
||||
2025-12-23 19:39:45,472 - app.routers - ERROR - Failed to start measurement on nl43-1: Failed to connect to device at 63.45.161.30:5225
|
||||
2025-12-23 19:39:52,929 - app.services - INFO - Sending command to 63.45.161.30:5225: $Measure, Stop
|
||||
2025-12-23 19:39:57,929 - app.services - ERROR - Connection timeout to 63.45.161.30:5225
|
||||
2025-12-23 19:39:57,929 - app.routers - ERROR - Failed to stop measurement on nl43-1: Failed to connect to device at 63.45.161.30:5225
|
||||
2025-12-23 19:40:15,626 - app.services - INFO - Sending command to 63.45.161.30:5225: DOD?
|
||||
2025-12-23 19:40:20,626 - app.services - ERROR - Connection timeout to 63.45.161.30:5225
|
||||
2025-12-23 19:40:20,626 - app.routers - ERROR - Failed to get live status for nl43-1: Failed to connect to device at 63.45.161.30:5225
|
||||
2025-12-23 19:41:48,870 - app.routers - INFO - Updated config for unit nl43-1
|
||||
2025-12-23 19:41:51,383 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 19:41:51,551 - app.services - ERROR - Malformed DOD response from 63.45.161.30:2255: R+0000
|
||||
2025-12-23 19:41:51,551 - app.routers - ERROR - Invalid response from device nl43-1: Malformed DOD response: expected comma-separated values, got: R+0000
|
||||
2025-12-23 19:41:59,639 - app.services - INFO - Sending command to 63.45.161.30:2255: $Measure, Start
|
||||
2025-12-23 19:41:59,774 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 19:42:12,493 - app.services - INFO - Sending command to 63.45.161.30:2255: $Measure, Stop
|
||||
2025-12-23 19:42:12,654 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
2025-12-23 19:42:21,145 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 19:42:21,304 - app.services - ERROR - Malformed DOD response from 63.45.161.30:2255: R+0000
|
||||
2025-12-23 19:42:21,304 - app.routers - ERROR - Invalid response from device nl43-1: Malformed DOD response: expected comma-separated values, got: R+0000
|
||||
2025-12-23 19:43:43,946 - app.services - INFO - Sending command to 63.45.161.30:2255: $Measure, Start
|
||||
2025-12-23 19:43:44,096 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 19:57:13,076 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 19:57:13,077 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 19:57:31,950 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 19:57:31,950 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 19:58:52,676 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 19:58:52,824 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 19:58:52,833 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 19:59:05,367 - app.services - INFO - Sending command to 63.45.161.30:2255: $Measure, Start
|
||||
2025-12-23 19:59:05,534 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 19:59:05,534 - app.routers - ERROR - Unexpected error starting measurement on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 19:59:25,765 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 19:59:26,024 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 19:59:26,044 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 19:59:29,829 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 19:59:29,983 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 19:59:29,991 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 20:01:30,447 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 20:01:30,624 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 20:01:30,633 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 20:01:31,448 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 20:01:31,663 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 20:01:31,671 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 20:13:43,728 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 20:13:43,728 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 20:14:33,467 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 20:14:33,625 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 20:14:33,635 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 20:14:39,641 - app.services - INFO - Sending command to 63.45.161.30:2255: $Measure,Start
|
||||
2025-12-23 20:14:39,935 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 20:14:39,935 - app.routers - ERROR - Unexpected error starting measurement on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 20:23:03,818 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 20:23:03,818 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 20:23:58,949 - app.services - INFO - Sending command to 63.45.161.30:2255: DOD?
|
||||
2025-12-23 20:23:59,105 - app.services - INFO - Parsed 64 data points from DOD response
|
||||
2025-12-23 20:23:59,115 - app.routers - INFO - Retrieved live status for unit nl43-1
|
||||
2025-12-23 20:24:00,869 - app.services - INFO - Sending command to 63.45.161.30:2255: $Measure,1
|
||||
2025-12-23 20:24:01,185 - app.services - ERROR - Communication error with 63.45.161.30:2255: Command error - device did not recognize command
|
||||
2025-12-23 20:24:01,185 - app.routers - ERROR - Unexpected error starting measurement on nl43-1: Command error - device did not recognize command
|
||||
2025-12-23 20:29:41,079 - app.main - INFO - Database tables initialized
|
||||
2025-12-23 20:29:41,079 - app.main - INFO - CORS allowed origins: ['*']
|
||||
2025-12-23 20:29:56,999 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Start
|
||||
2025-12-23 20:29:57,135 - app.routers - INFO - Started measurement on unit nl43-1
|
||||
2025-12-23 20:30:46,229 - app.services - INFO - Sending command to 63.45.161.30:2255: Measure,Stop
|
||||
2025-12-23 20:30:46,455 - app.routers - INFO - Stopped measurement on unit nl43-1
|
||||
662
docs/API.md
Normal file
662
docs/API.md
Normal file
@@ -0,0 +1,662 @@
|
||||
# SLMM API Documentation
|
||||
|
||||
REST API for controlling Rion NL-43/NL-53 Sound Level Meters via TCP and FTP.
|
||||
|
||||
Base URL: `http://localhost:8000/api/nl43`
|
||||
|
||||
All endpoints require a `unit_id` parameter identifying the device.
|
||||
|
||||
## Device Configuration
|
||||
|
||||
### Get Device Config
|
||||
```
|
||||
GET /{unit_id}/config
|
||||
```
|
||||
Returns the device configuration including host, port, and enabled protocols.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"unit_id": "nl43-1",
|
||||
"host": "192.168.1.100",
|
||||
"tcp_port": 2255,
|
||||
"tcp_enabled": true,
|
||||
"ftp_enabled": false,
|
||||
"web_enabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Device Config
|
||||
```
|
||||
PUT /{unit_id}/config
|
||||
```
|
||||
Update device configuration.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"host": "192.168.1.100",
|
||||
"tcp_port": 2255,
|
||||
"tcp_enabled": true,
|
||||
"ftp_enabled": false,
|
||||
"ftp_username": "admin",
|
||||
"ftp_password": "password",
|
||||
"web_enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
## Device Status
|
||||
|
||||
### Get Cached Status
|
||||
```
|
||||
GET /{unit_id}/status
|
||||
```
|
||||
Returns the last cached measurement snapshot from the database.
|
||||
|
||||
### Get Live Status
|
||||
```
|
||||
GET /{unit_id}/live
|
||||
```
|
||||
Requests fresh DOD (Display On Demand) data from the device and returns current measurements.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"unit_id": "nl43-1",
|
||||
"measurement_state": "Measure",
|
||||
"lp": "65.2",
|
||||
"leq": "68.4",
|
||||
"lmax": "82.1",
|
||||
"lmin": "42.3",
|
||||
"lpeak": "89.5",
|
||||
"battery_level": "80",
|
||||
"power_source": "Battery",
|
||||
"sd_remaining_mb": "2048",
|
||||
"sd_free_ratio": "50"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stream Live Data (WebSocket)
|
||||
```
|
||||
WS /{unit_id}/live
|
||||
```
|
||||
Opens a WebSocket connection and streams continuous DRD (Display Real-time Data) from the device.
|
||||
|
||||
## Measurement Control
|
||||
|
||||
### Start Measurement
|
||||
```
|
||||
POST /{unit_id}/start
|
||||
```
|
||||
Starts measurement on the device.
|
||||
|
||||
### Stop Measurement
|
||||
```
|
||||
POST /{unit_id}/stop
|
||||
```
|
||||
Stops measurement on the device.
|
||||
|
||||
### Pause Measurement
|
||||
```
|
||||
POST /{unit_id}/pause
|
||||
```
|
||||
Pauses the current measurement.
|
||||
|
||||
### Resume Measurement
|
||||
```
|
||||
POST /{unit_id}/resume
|
||||
```
|
||||
Resumes a paused measurement.
|
||||
|
||||
### Reset Measurement
|
||||
```
|
||||
POST /{unit_id}/reset
|
||||
```
|
||||
Resets the measurement data.
|
||||
|
||||
### Get Measurement State
|
||||
```
|
||||
GET /{unit_id}/measurement-state
|
||||
```
|
||||
Gets the current measurement state to determine if the device is actively measuring.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"measurement_state": "Start",
|
||||
"is_measuring": true
|
||||
}
|
||||
```
|
||||
|
||||
**Values:**
|
||||
- `measurement_state`: "Start" (measuring) or "Stop" (not measuring)
|
||||
- `is_measuring`: Boolean convenience field (true if measuring, false otherwise)
|
||||
|
||||
**Note:** Elapsed time and interval counts are displayed on the device OSD but not available via command. Track elapsed time in your application by recording the start time when you call the `/start` endpoint.
|
||||
|
||||
### Manual Store
|
||||
```
|
||||
POST /{unit_id}/store
|
||||
```
|
||||
Manually stores the current measurement data.
|
||||
|
||||
## Device Information
|
||||
|
||||
### Get Battery Level
|
||||
```
|
||||
GET /{unit_id}/battery
|
||||
```
|
||||
Returns the battery level.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"battery_level": "80"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Clock
|
||||
```
|
||||
GET /{unit_id}/clock
|
||||
```
|
||||
Returns the device clock time.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"clock": "2025/12/24,02:30:15"
|
||||
}
|
||||
```
|
||||
|
||||
### Set Clock
|
||||
```
|
||||
PUT /{unit_id}/clock
|
||||
```
|
||||
Sets the device clock time.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"datetime": "2025/12/24,02:30:15"
|
||||
}
|
||||
```
|
||||
|
||||
## Measurement Settings
|
||||
|
||||
### Get Frequency Weighting
|
||||
```
|
||||
GET /{unit_id}/frequency-weighting?channel=Main
|
||||
```
|
||||
Gets the frequency weighting (A, C, or Z) for a channel.
|
||||
|
||||
**Query Parameters:**
|
||||
- `channel` (optional): Main, Sub1, Sub2, or Sub3 (default: Main)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"frequency_weighting": "A",
|
||||
"channel": "Main"
|
||||
}
|
||||
```
|
||||
|
||||
### Set Frequency Weighting
|
||||
```
|
||||
PUT /{unit_id}/frequency-weighting
|
||||
```
|
||||
Sets the frequency weighting.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"weighting": "A",
|
||||
"channel": "Main"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Time Weighting
|
||||
```
|
||||
GET /{unit_id}/time-weighting?channel=Main
|
||||
```
|
||||
Gets the time weighting (F, S, or I) for a channel.
|
||||
|
||||
**Query Parameters:**
|
||||
- `channel` (optional): Main, Sub1, Sub2, or Sub3 (default: Main)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"time_weighting": "F",
|
||||
"channel": "Main"
|
||||
}
|
||||
```
|
||||
|
||||
### Set Time Weighting
|
||||
```
|
||||
PUT /{unit_id}/time-weighting
|
||||
```
|
||||
Sets the time weighting.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"weighting": "F",
|
||||
"channel": "Main"
|
||||
}
|
||||
```
|
||||
|
||||
**Values:**
|
||||
- `F` - Fast (125ms)
|
||||
- `S` - Slow (1s)
|
||||
- `I` - Impulse (35ms)
|
||||
|
||||
## 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
|
||||
```
|
||||
POST /{unit_id}/ftp/enable
|
||||
```
|
||||
Enables FTP server on the device.
|
||||
|
||||
**Note:** FTP and TCP are mutually exclusive. Enabling FTP will temporarily disable TCP control.
|
||||
|
||||
### Disable FTP
|
||||
```
|
||||
POST /{unit_id}/ftp/disable
|
||||
```
|
||||
Disables FTP server on the device.
|
||||
|
||||
### Get FTP Status
|
||||
```
|
||||
GET /{unit_id}/ftp/status
|
||||
```
|
||||
Checks if FTP is enabled on the device.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"ftp_status": "On",
|
||||
"ftp_enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### List Files
|
||||
```
|
||||
GET /{unit_id}/ftp/files?path=/
|
||||
```
|
||||
Lists files and directories at the specified path.
|
||||
|
||||
**Query Parameters:**
|
||||
- `path` (optional): Directory path to list (default: /)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"path": "/NL43_DATA/",
|
||||
"count": 3,
|
||||
"files": [
|
||||
{
|
||||
"name": "measurement_001.wav",
|
||||
"path": "/NL43_DATA/measurement_001.wav",
|
||||
"size": 102400,
|
||||
"modified": "Dec 24 2025",
|
||||
"is_dir": false
|
||||
},
|
||||
{
|
||||
"name": "folder1",
|
||||
"path": "/NL43_DATA/folder1",
|
||||
"size": 0,
|
||||
"modified": "Dec 23 2025",
|
||||
"is_dir": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Download File
|
||||
```
|
||||
POST /{unit_id}/ftp/download
|
||||
```
|
||||
Downloads a file from the device via FTP.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"remote_path": "/NL43_DATA/measurement_001.wav"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
Returns the file as a binary download with appropriate `Content-Disposition` header.
|
||||
|
||||
## Error Responses
|
||||
|
||||
All endpoints return standard HTTP status codes:
|
||||
|
||||
- `200` - Success
|
||||
- `404` - Device config not found
|
||||
- `403` - TCP communication is disabled
|
||||
- `502` - Failed to communicate with device
|
||||
- `504` - Device communication timeout
|
||||
- `500` - Internal server error
|
||||
|
||||
**Error Response Format:**
|
||||
```json
|
||||
{
|
||||
"detail": "Error message"
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Terra-view Integration Example
|
||||
|
||||
```javascript
|
||||
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`)
|
||||
.then(r => r.json())
|
||||
)
|
||||
);
|
||||
|
||||
// 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 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 measurement file
|
||||
const latestFile = files
|
||||
.filter(f => !f.is_dir)
|
||||
.sort((a, b) => b.modified - a.modified)[0];
|
||||
|
||||
if (latestFile) {
|
||||
const download = await fetch(`http://localhost:8000/api/nl43/${device}/ftp/download`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ remote_path: latestFile.path })
|
||||
});
|
||||
|
||||
const blob = await download.blob();
|
||||
// Save to local storage or process...
|
||||
}
|
||||
|
||||
// Disable FTP to re-enable TCP
|
||||
await fetch(`http://localhost:8000/api/nl43/${device}/ftp/disable`, {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
The NL43 protocol requires ≥1 second between commands to the same device. The API automatically enforces this rate limit.
|
||||
|
||||
## Notes
|
||||
|
||||
- TCP and FTP protocols are mutually exclusive on the device
|
||||
- FTP uses active mode (requires device to connect back to server)
|
||||
- WebSocket streaming keeps a persistent connection - limit concurrent streams
|
||||
- All measurements are stored in the database for quick access via `/status` endpoint
|
||||
770
docs/nl43_Command_ref.md
Normal file
770
docs/nl43_Command_ref.md
Normal file
@@ -0,0 +1,770 @@
|
||||
# NL-43/NL-53 Sound Level Meter - AI Command Reference
|
||||
## Quick Reference for Programming & Development
|
||||
|
||||
---
|
||||
|
||||
## 🚀 QUICK START - COMMAND SYNTAX
|
||||
|
||||
### Setting Command (Change device settings)
|
||||
```
|
||||
Format: $Command Name,Parameter[CR][LF]
|
||||
Example: $LCD Auto Off,Short[CR][LF]
|
||||
$Backlight,On[CR][LF]
|
||||
```
|
||||
|
||||
**Critical Rules:**
|
||||
- **Prefix:** `$` (auto-added by device, shows command processing status)
|
||||
- **Separator:** `,` (comma required after command name)
|
||||
- **Spaces:** Must preserve exact spacing in command names
|
||||
- **Terminator:** `[CR][LF]` (0x0D 0x0A)
|
||||
- **Case:** Insensitive (LCD = lcd = Lcd)
|
||||
- **Wait:** Minimum 1 second after receiving `$` response before next command
|
||||
|
||||
### Request Command (Get device status/data)
|
||||
```
|
||||
Format: Command Name?[CR][LF]
|
||||
Example: Battery Level?[CR][LF]
|
||||
System Version?[CR][LF]
|
||||
```
|
||||
|
||||
**Critical Rules:**
|
||||
- **Suffix:** `?` after command name
|
||||
- **No prefix:** No `$` for requests
|
||||
- **Terminator:** `[CR][LF]`
|
||||
- **Case:** Insensitive
|
||||
|
||||
---
|
||||
|
||||
## 📋 RESULT CODES
|
||||
|
||||
Every command returns a result code:
|
||||
|
||||
```
|
||||
Format: R+####
|
||||
```
|
||||
|
||||
| Code | Status | Meaning |
|
||||
|------|--------|---------|
|
||||
| `R+0000` | ✅ Success | Command executed successfully |
|
||||
| `R+0001` | ❌ Command Error | Command not recognized |
|
||||
| `R+0002` | ❌ Parameter Error | Wrong parameter format or count |
|
||||
| `R+0003` | ❌ Specification Error | Used set when should request (or vice versa) |
|
||||
| `R+0004` | ❌ Status Error | Device not in correct state for this command |
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ TIMING & CONTROL VALUES
|
||||
|
||||
| Operation | Time | Notes |
|
||||
|-----------|------|-------|
|
||||
| Device response time | < 3 seconds | Max time for device to respond |
|
||||
| Between characters | < 100 ms | When sending multi-char strings |
|
||||
| Device idle time | < 200 ms | After device sends data |
|
||||
| **Recommended wait** | **≥ 1 second** | **After `$` before next command** |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 COMMUNICATION CODES
|
||||
|
||||
| Code | Hex | Purpose |
|
||||
|------|-----|---------|
|
||||
| `[CR]` | 0x0D | First terminator (Carriage Return) |
|
||||
| `[LF]` | 0x0A | Second terminator (Line Feed) |
|
||||
| `[SUB]` | 0x1A | Stop request |
|
||||
|
||||
---
|
||||
|
||||
## 📚 COMMAND CATEGORIES & FULL LIST
|
||||
|
||||
### Communication
|
||||
| Command | Type | Function |
|
||||
|---------|------|----------|
|
||||
| `Echo` | S/R | Communication echo ON/OFF |
|
||||
|
||||
### System
|
||||
| Command | Type | Function |
|
||||
|---------|------|----------|
|
||||
| `System Version` | R | Get firmware version |
|
||||
| `Type` | R | Get device type (NL-43/NL-53) |
|
||||
| `Serial Number` | R | Get device serial number |
|
||||
| `Clock` | S/R | Set/get current time |
|
||||
| `Language` | S/R | Set/get display language |
|
||||
| `Index Number` | S/R | Set/get index number |
|
||||
| `Key Lock` | S/R | Enable/disable key lock |
|
||||
|
||||
### Display
|
||||
| Command | Type | Function |
|
||||
|---------|------|----------|
|
||||
| `Backlight` | S/R | Control backlight ON/OFF |
|
||||
| `Backlight Auto Off` | S/R | Set auto-off time for backlight |
|
||||
| `LCD` | S/R | Control LCD ON/OFF |
|
||||
| `LCD Auto Off` | S/R | Set auto-off time for LCD |
|
||||
| `Backlight Brightness` | S/R | Set backlight brightness |
|
||||
| `Battery Type` | S/R | Set battery type |
|
||||
| `Battery Level` | R | Get current battery level |
|
||||
| `Output Level Range Upper` | S/R | Set upper limit of bar graph |
|
||||
| `Output Level Range Lower` | S/R | Set lower limit of bar graph |
|
||||
| `Display Leq` | S/R | Enable/disable Leq display |
|
||||
| `Display LE` | S/R | Enable/disable LE display |
|
||||
| `Display Lpeak` | S/R | Enable/disable Lpeak display |
|
||||
| `Display Lmax` | S/R | Enable/disable Lmax display |
|
||||
| `Display Lmin` | S/R | Enable/disable Lmin display |
|
||||
| `Display LN1` | S/R | Enable/disable L5 display |
|
||||
| `Display LN2` | S/R | Enable/disable L10 display |
|
||||
| `Display LN3` | S/R | Enable/disable L50 display |
|
||||
| `Display LN4` | S/R | Enable/disable L90 display |
|
||||
| `Display LN5` | S/R | Enable/disable L95 display |
|
||||
| `Time Level Time Scale` | S/R | Set time-level time scale |
|
||||
|
||||
### SD Card
|
||||
| Command | Type | Function |
|
||||
|---------|------|----------|
|
||||
| `SD Card Total Size` | R | Get total SD card capacity |
|
||||
| `SD Card Free Size` | R | Get SD card free space |
|
||||
| `SD Card Percentage` | R | Get SD card free space ratio |
|
||||
|
||||
### Measurement
|
||||
| Command | Type | Function |
|
||||
|---------|------|----------|
|
||||
| `Frequency Weighting (Main)` | S/R | Set main frequency weighting (A/C/Z) |
|
||||
| `Frequency Weighting (Sub1/2/3)` | S/R | Set sub frequency weighting |
|
||||
| `Time Weighting (Main)` | S/R | Set main time weighting (F/S/I) |
|
||||
| `Time Weighting (Sub1/2/3)` | S/R | Set sub time weighting |
|
||||
| `Measure` | S/R | Start/stop measurement |
|
||||
| `Pause` | S/R | Pause measurement |
|
||||
| `Windscreen Correction` | S/R | Enable/disable windscreen correction |
|
||||
|
||||
### Storage
|
||||
| Command | Type | Function |
|
||||
|---------|------|----------|
|
||||
| `Store Mode` | S/R | Set storage mode (Manual/Auto) |
|
||||
| `Store Name` | S/R | Set storage name |
|
||||
| `Manual Address` | S/R | Set manual storage address |
|
||||
| `Manual Store` | S | Execute manual storage |
|
||||
| `Overwrite` | S/R | Enable/disable storage overwriting |
|
||||
| `Measurement Time Preset Manual` | S/R | Set manual measurement time |
|
||||
| `Measurement Time Preset Auto` | S/R | Set auto measurement time |
|
||||
|
||||
### I/O
|
||||
| Command | Type | Function |
|
||||
|---------|------|----------|
|
||||
| `AC OUT` | S/R | Set AC output ON/OFF |
|
||||
| `DC OUT` | S/R | Set DC output ON/OFF |
|
||||
| `Baud Rate` | S/R | Set RS-232C baud rate |
|
||||
| `USB Class` | S/R | Set USB communication mode |
|
||||
| `Ethernet` | S/R | Enable/disable LAN function |
|
||||
| `Ethernet DHCP` | S/R | Enable/disable DHCP |
|
||||
| `Ethernet IP` | S/R | Set IP address |
|
||||
| `Ethernet Subnet` | S/R | Set subnet mask |
|
||||
| `Ethernet Gateway` | S/R | Set default gateway |
|
||||
| `Web` | S/R | Enable/disable web application |
|
||||
| `FTP` | S/R | Enable/disable FTP |
|
||||
| `TCP` | S/R | Enable/disable TCP communication |
|
||||
|
||||
### Data Output
|
||||
| Command | Type | Function |
|
||||
|---------|------|----------|
|
||||
| `DOD` | R | Output current displayed value |
|
||||
| `DRD` | R | Continuous data output |
|
||||
| `DRD?status` | R | Continuous output with status |
|
||||
| `DLC` | R | Output final calculation result |
|
||||
|
||||
---
|
||||
|
||||
## 💡 DETAILED COMMAND SYNTAX
|
||||
|
||||
### Echo (Communication Echo)
|
||||
**Purpose:** Enable/disable echo of sent commands
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Echo,Off[CR][LF] # Disable echo
|
||||
$Echo,On[CR][LF] # Enable echo
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Echo?[CR][LF]
|
||||
Response: Off | On
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### System Version
|
||||
**Purpose:** Get firmware version
|
||||
|
||||
**Request:**
|
||||
```
|
||||
System Version?[CR][LF]
|
||||
Response: "xx.xx.xxxx" # Example: "01.02.0034"
|
||||
```
|
||||
|
||||
**Note:** No setting command available
|
||||
|
||||
---
|
||||
|
||||
### Type
|
||||
**Purpose:** Get device type
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Type?[CR][LF]
|
||||
Response: "NL-43" | "NL-53"
|
||||
```
|
||||
|
||||
**Note:** No setting command available
|
||||
|
||||
---
|
||||
|
||||
### Serial Number
|
||||
**Purpose:** Get device serial number
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Serial Number?[CR][LF]
|
||||
Response: "xxxxxxxx" # 8-digit number (00000000-99999999)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Clock
|
||||
**Purpose:** Set/get current date and time
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Clock,2025/12/24 14:30:00[CR][LF]
|
||||
|
||||
Format: $Clock,YYYY/MM/DD HH:MM:SS[CR][LF]
|
||||
- Year: 2023-2079
|
||||
- Month: 1-12
|
||||
- Day: 1-31
|
||||
- Hour: 0-23
|
||||
- Minute: 0-59
|
||||
- Second: 0-59
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Clock?[CR][LF]
|
||||
Response: 2025/12/24 14:30:00
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Language
|
||||
**Purpose:** Set/get display language
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Language,English[CR][LF]
|
||||
|
||||
Options:
|
||||
- Japanese
|
||||
- English
|
||||
- Germany
|
||||
- Spanish
|
||||
- French
|
||||
- Simplified Chinese
|
||||
- Korean
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Language?[CR][LF]
|
||||
Response: English
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Index Number
|
||||
**Purpose:** Set/get index number
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Index Number,0042[CR][LF]
|
||||
|
||||
Format: 0000-9999 (4 digits)
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Index Number?[CR][LF]
|
||||
Response: 0042
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Key Lock
|
||||
**Purpose:** Enable/disable key lock
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Key Lock,Off[CR][LF]
|
||||
$Key Lock,On[CR][LF]
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Key Lock?[CR][LF]
|
||||
Response: Off | On
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Backlight
|
||||
**Purpose:** Control backlight ON/OFF
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Backlight,Off[CR][LF]
|
||||
$Backlight,On[CR][LF]
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Backlight?[CR][LF]
|
||||
Response: Off | On
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Backlight Auto Off
|
||||
**Purpose:** Set backlight auto-off timer
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Backlight Auto Off,Short[CR][LF]
|
||||
$Backlight Auto Off,Long[CR][LF]
|
||||
$Backlight Auto Off,Off[CR][LF]
|
||||
|
||||
Options:
|
||||
- Short: 30 seconds
|
||||
- Long: 60 seconds
|
||||
- Off: Always on
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Backlight Auto Off?[CR][LF]
|
||||
Response: Short | Long | Off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### LCD Auto Off
|
||||
**Purpose:** Set LCD auto-off timer
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$LCD Auto Off,Short[CR][LF]
|
||||
$LCD Auto Off,Long[CR][LF]
|
||||
$LCD Auto Off,Off[CR][LF]
|
||||
|
||||
Options:
|
||||
- Short: 5 minutes
|
||||
- Long: 10 minutes
|
||||
- Off: Always on
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
LCD Auto Off?[CR][LF]
|
||||
Response: Short | Long | Off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Backlight Brightness
|
||||
**Purpose:** Set backlight brightness
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Backlight Brightness,1[CR][LF]
|
||||
|
||||
Range: 1-4 (1=dimmest, 4=brightest)
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Backlight Brightness?[CR][LF]
|
||||
Response: 1 | 2 | 3 | 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Battery Type
|
||||
**Purpose:** Set battery type
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Battery Type,Alkaline[CR][LF]
|
||||
$Battery Type,NiMH[CR][LF]
|
||||
|
||||
Options:
|
||||
- Alkaline: Alkaline batteries
|
||||
- NiMH: Nickel-metal hydride batteries
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Battery Type?[CR][LF]
|
||||
Response: Alkaline | NiMH
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Battery Level
|
||||
**Purpose:** Get current battery level
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Battery Level?[CR][LF]
|
||||
Response: 0-100 # Percentage (0=empty, 100=full)
|
||||
```
|
||||
|
||||
**Note:** Read-only command
|
||||
|
||||
---
|
||||
|
||||
### Measure
|
||||
**Purpose:** Start/stop measurement
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Measure,Run[CR][LF] # Start measurement
|
||||
$Measure,Stop[CR][LF] # Stop measurement
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Measure?[CR][LF]
|
||||
Response: Run | Stop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pause
|
||||
**Purpose:** Pause/resume measurement
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Pause,On[CR][LF] # Pause measurement
|
||||
$Pause,Off[CR][LF] # Resume measurement
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Pause?[CR][LF]
|
||||
Response: On | Off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Frequency Weighting (Main)
|
||||
**Purpose:** Set main frequency weighting
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Frequency Weighting,A[CR][LF]
|
||||
$Frequency Weighting,C[CR][LF]
|
||||
$Frequency Weighting,Z[CR][LF]
|
||||
|
||||
Options:
|
||||
- A: A-weighting (most common)
|
||||
- C: C-weighting
|
||||
- Z: Z-weighting (flat response)
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Frequency Weighting?[CR][LF]
|
||||
Response: A | C | Z
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Time Weighting (Main)
|
||||
**Purpose:** Set main time weighting
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Time Weighting,F[CR][LF]
|
||||
$Time Weighting,S[CR][LF]
|
||||
$Time Weighting,I[CR][LF]
|
||||
|
||||
Options:
|
||||
- F: Fast (125ms)
|
||||
- S: Slow (1000ms)
|
||||
- I: Impulse
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Time Weighting?[CR][LF]
|
||||
Response: F | S | I
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Baud Rate
|
||||
**Purpose:** Set RS-232C communication speed
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Baud Rate,9600[CR][LF]
|
||||
$Baud Rate,19200[CR][LF]
|
||||
$Baud Rate,38400[CR][LF]
|
||||
$Baud Rate,57600[CR][LF]
|
||||
$Baud Rate,115200[CR][LF]
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Baud Rate?[CR][LF]
|
||||
Response: 9600 | 19200 | 38400 | 57600 | 115200
|
||||
```
|
||||
|
||||
**Note:** Default is usually 38400
|
||||
|
||||
---
|
||||
|
||||
### Ethernet IP
|
||||
**Purpose:** Set IP address for LAN connection
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Ethernet IP,192.168.1.100[CR][LF]
|
||||
|
||||
Format: xxx.xxx.xxx.xxx (0-255 for each octet)
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Ethernet IP?[CR][LF]
|
||||
Response: 192.168.1.100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Ethernet DHCP
|
||||
**Purpose:** Enable/disable automatic IP address assignment
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$Ethernet DHCP,Off[CR][LF] # Manual IP
|
||||
$Ethernet DHCP,On[CR][LF] # Automatic IP
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
Ethernet DHCP?[CR][LF]
|
||||
Response: Off | On
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DOD (Display Output Data)
|
||||
**Purpose:** Get current displayed measurement value
|
||||
|
||||
**Request:**
|
||||
```
|
||||
DOD?[CR][LF]
|
||||
|
||||
Response format:
|
||||
ddd.d,cc.c,aa.a
|
||||
- ddd.d: Main display value
|
||||
- cc.c: Sub display value
|
||||
- aa.a: Additional value
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```
|
||||
075.2,072.1,080.5
|
||||
# 75.2 dB main, 72.1 dB sub, 80.5 dB peak
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DRD (Data Real-time Display)
|
||||
**Purpose:** Continuous output of measurement data
|
||||
|
||||
**Request:**
|
||||
```
|
||||
DRD?[CR][LF]
|
||||
```
|
||||
|
||||
**Stop continuous output:**
|
||||
```
|
||||
[SUB] (0x1A)
|
||||
```
|
||||
|
||||
**Response:** Continuous stream of measurement values until stop requested
|
||||
|
||||
---
|
||||
|
||||
### USB Class
|
||||
**Purpose:** Set USB communication mode
|
||||
|
||||
**Setting:**
|
||||
```
|
||||
$USB Class,Serial[CR][LF] # Serial communication mode
|
||||
$USB Class,Mass Storage[CR][LF] # Mass storage mode (file access)
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
USB Class?[CR][LF]
|
||||
Response: Serial | Mass Storage
|
||||
```
|
||||
|
||||
**Important:** When in Mass Storage mode, communication commands are blocked
|
||||
|
||||
---
|
||||
|
||||
## 🎯 COMMON PROGRAMMING PATTERNS
|
||||
|
||||
### Basic Connection Test
|
||||
```python
|
||||
# 1. Check device type
|
||||
send("Type?[CR][LF]")
|
||||
wait_for_response() # Should get "NL-43" or "NL-53"
|
||||
|
||||
# 2. Get version
|
||||
send("System Version?[CR][LF]")
|
||||
wait_for_response() # e.g., "01.02.0034"
|
||||
|
||||
# 3. Enable echo for debugging
|
||||
send("$Echo,On[CR][LF]")
|
||||
wait_for_response() # Should get "R+0000"
|
||||
```
|
||||
|
||||
### Start Basic Measurement
|
||||
```python
|
||||
# 1. Set frequency weighting to A
|
||||
send("$Frequency Weighting,A[CR][LF]")
|
||||
wait_1_second()
|
||||
|
||||
# 2. Set time weighting to Fast
|
||||
send("$Time Weighting,F[CR][LF]")
|
||||
wait_1_second()
|
||||
|
||||
# 3. Start measurement
|
||||
send("$Measure,Run[CR][LF]")
|
||||
wait_1_second()
|
||||
|
||||
# 4. Get current reading
|
||||
send("DOD?[CR][LF]")
|
||||
reading = wait_for_response()
|
||||
```
|
||||
|
||||
### Configure Network
|
||||
```python
|
||||
# 1. Disable DHCP for static IP
|
||||
send("$Ethernet DHCP,Off[CR][LF]")
|
||||
wait_1_second()
|
||||
|
||||
# 2. Set IP address
|
||||
send("$Ethernet IP,192.168.1.100[CR][LF]")
|
||||
wait_1_second()
|
||||
|
||||
# 3. Set subnet
|
||||
send("$Ethernet Subnet,255.255.255.0[CR][LF]")
|
||||
wait_1_second()
|
||||
|
||||
# 4. Set gateway
|
||||
send("$Ethernet Gateway,192.168.1.1[CR][LF]")
|
||||
wait_1_second()
|
||||
|
||||
# 5. Enable Ethernet
|
||||
send("$Ethernet,On[CR][LF]")
|
||||
wait_1_second()
|
||||
```
|
||||
|
||||
### Continuous Data Acquisition
|
||||
```python
|
||||
# Start continuous output
|
||||
send("DRD?[CR][LF]")
|
||||
|
||||
# Read data in loop
|
||||
while acquiring:
|
||||
data = read_line()
|
||||
process(data)
|
||||
|
||||
# Stop when done
|
||||
send("[SUB]") # 0x1A
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ COMMON ERRORS & TROUBLESHOOTING
|
||||
|
||||
### R+0001 - Command Not Recognized
|
||||
- Check spelling (spaces matter!)
|
||||
- Verify command exists for your model
|
||||
- Check for extra/missing spaces in command name
|
||||
|
||||
### R+0002 - Parameter Error
|
||||
- Verify parameter format matches spec
|
||||
- Check parameter value is in valid range
|
||||
- Ensure comma separator is present
|
||||
|
||||
### R+0003 - Specification Error
|
||||
- Using `$` prefix on request command
|
||||
- Using `?` on setting command
|
||||
- Command type mismatch
|
||||
|
||||
### R+0004 - Status Error
|
||||
- Device busy (measuring, storing, etc.)
|
||||
- Stop measurement before changing settings
|
||||
- Check device is powered on and ready
|
||||
- May need to wait longer between commands
|
||||
|
||||
### No Response
|
||||
- Check baud rate matches (default 38400)
|
||||
- Verify `[CR][LF]` line endings
|
||||
- Wait minimum 1 second between commands
|
||||
- Check cable connection
|
||||
- Verify USB Class mode is "Serial" not "Mass Storage"
|
||||
|
||||
---
|
||||
|
||||
## 📝 NOTES FOR AI ASSISTANTS
|
||||
|
||||
**When helping with code:**
|
||||
1. Always include the `$` prefix for setting commands
|
||||
2. Always include `?` suffix for request commands
|
||||
3. Always include `[CR][LF]` terminators
|
||||
4. Always wait ≥1 second between commands
|
||||
5. Check for `R+0000` success code after settings
|
||||
6. Handle error codes appropriately
|
||||
|
||||
**For serial communication:**
|
||||
- Use 8 data bits, no parity, 1 stop bit (8N1)
|
||||
- Default baud: 38400
|
||||
- Hardware flow control: None
|
||||
- Hex codes: CR=0x0D, LF=0x0A, SUB=0x1A
|
||||
|
||||
**For network communication:**
|
||||
- TCP port: Varies by configuration
|
||||
- Ensure device is in correct USB/Ethernet mode
|
||||
- Commands are identical over serial/network
|
||||
|
||||
---
|
||||
|
||||
## 📖 DOCUMENT SOURCE
|
||||
|
||||
This reference was extracted from:
|
||||
- **Document:** NL-43/NL-53 Communication Guide
|
||||
- **Filename:** NL-43_NL-53_Communication_Guide_66132.pdf
|
||||
- **Manufacturer:** RION Co., Ltd.
|
||||
- **Full manual available at:** https://rion-sv.com/nl-43_53_63/manual/
|
||||
|
||||
**Document Coverage:**
|
||||
- Full communication protocol specification
|
||||
- RS-232C and USB serial connection
|
||||
- Ethernet/LAN configuration
|
||||
- Complete command reference
|
||||
- Data output formats
|
||||
- Timing specifications
|
||||
|
||||
For complete technical details, measurement specifications, and advanced features, refer to the full PDF documentation.
|
||||
59
migrate_add_ftp_credentials.py
Executable file
59
migrate_add_ftp_credentials.py
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add FTP username and password columns to nl43_config table.
|
||||
Run this once to update existing database schema.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path(__file__).parent / "data" / "slmm.db"
|
||||
|
||||
|
||||
def migrate():
|
||||
"""Add ftp_username and ftp_password columns to nl43_config table."""
|
||||
|
||||
if not DB_PATH.exists():
|
||||
print(f"Database not found at {DB_PATH}")
|
||||
print("No migration needed - database will be created with new schema")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if columns already exist
|
||||
cursor.execute("PRAGMA table_info(nl43_config)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
|
||||
if "ftp_username" in columns and "ftp_password" in columns:
|
||||
print("✓ FTP credential columns already exist, no migration needed")
|
||||
return
|
||||
|
||||
# Add ftp_username column if it doesn't exist
|
||||
if "ftp_username" not in columns:
|
||||
print("Adding ftp_username column...")
|
||||
cursor.execute("ALTER TABLE nl43_config ADD COLUMN ftp_username TEXT")
|
||||
print("✓ Added ftp_username column")
|
||||
|
||||
# Add ftp_password column if it doesn't exist
|
||||
if "ftp_password" not in columns:
|
||||
print("Adding ftp_password column...")
|
||||
cursor.execute("ALTER TABLE nl43_config ADD COLUMN ftp_password TEXT")
|
||||
print("✓ Added ftp_password column")
|
||||
|
||||
conn.commit()
|
||||
print("\n✓ Migration completed successfully!")
|
||||
print("\nYou can now set FTP credentials via the web UI or database.")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"✗ Migration failed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
@@ -2,3 +2,4 @@ fastapi
|
||||
uvicorn
|
||||
sqlalchemy
|
||||
pydantic
|
||||
aioftp
|
||||
|
||||
65
set_ftp_credentials.py
Executable file
65
set_ftp_credentials.py
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Helper script to set FTP credentials for a device.
|
||||
Usage: python3 set_ftp_credentials.py <unit_id> <username> <password>
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path(__file__).parent / "data" / "slmm.db"
|
||||
|
||||
|
||||
def set_credentials(unit_id: str, username: str, password: str):
|
||||
"""Set FTP credentials for a device."""
|
||||
|
||||
if not DB_PATH.exists():
|
||||
print(f"Error: Database not found at {DB_PATH}")
|
||||
sys.exit(1)
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if unit exists
|
||||
cursor.execute("SELECT unit_id FROM nl43_config WHERE unit_id = ?", (unit_id,))
|
||||
if not cursor.fetchone():
|
||||
print(f"Error: Unit '{unit_id}' not found in database")
|
||||
print("\nAvailable units:")
|
||||
cursor.execute("SELECT unit_id FROM nl43_config")
|
||||
for row in cursor.fetchall():
|
||||
print(f" - {row[0]}")
|
||||
sys.exit(1)
|
||||
|
||||
# Update credentials
|
||||
cursor.execute(
|
||||
"UPDATE nl43_config SET ftp_username = ?, ftp_password = ? WHERE unit_id = ?",
|
||||
(username, password, unit_id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
print(f"✓ FTP credentials updated for unit '{unit_id}'")
|
||||
print(f" Username: {username}")
|
||||
print(f" Password: {'*' * len(password)}")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python3 set_ftp_credentials.py <unit_id> <username> <password>")
|
||||
print("\nExample:")
|
||||
print(" python3 set_ftp_credentials.py nl43-1 admin mypassword")
|
||||
sys.exit(1)
|
||||
|
||||
unit_id = sys.argv[1]
|
||||
username = sys.argv[2]
|
||||
password = sys.argv[3]
|
||||
|
||||
set_credentials(unit_id, username, password)
|
||||
@@ -7,16 +7,38 @@
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; margin: 24px; max-width: 900px; }
|
||||
fieldset { margin-bottom: 16px; padding: 12px; }
|
||||
legend { font-weight: 600; }
|
||||
label { display: block; margin-bottom: 6px; font-weight: 600; }
|
||||
input { width: 100%; padding: 8px; margin-bottom: 10px; }
|
||||
button { padding: 8px 12px; margin-right: 8px; }
|
||||
#log { background: #f6f8fa; border: 1px solid #d0d7de; padding: 12px; min-height: 120px; white-space: pre-wrap; }
|
||||
.diagnostic-item { margin: 8px 0; padding: 8px; border-left: 4px solid #888; background: #f6f8fa; }
|
||||
.diagnostic-item.pass { border-left-color: #0a0; }
|
||||
.diagnostic-item.fail { border-left-color: #d00; }
|
||||
.diagnostic-item.warning { border-left-color: #fa0; }
|
||||
.diagnostic-item.skip { border-left-color: #888; }
|
||||
.diagnostic-status { font-weight: 600; margin-right: 8px; text-transform: uppercase; font-size: 0.85em; }
|
||||
.diagnostic-status.pass { color: #0a0; }
|
||||
.diagnostic-status.fail { color: #d00; }
|
||||
.diagnostic-status.warning { color: #fa0; }
|
||||
.diagnostic-status.skip { color: #888; }
|
||||
#diagnosticsSummary { font-size: 1.1em; font-weight: 600; margin-bottom: 12px; padding: 8px; border-radius: 4px; }
|
||||
#diagnosticsSummary.pass { background: #d4edda; color: #155724; }
|
||||
#diagnosticsSummary.fail { background: #f8d7da; color: #721c24; }
|
||||
#diagnosticsSummary.degraded { background: #fff3cd; color: #856404; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SLMM NL43 Standalone</h1>
|
||||
<p>Configure a unit (host/port), then use controls to Start/Stop and fetch live status.</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>🔍 Connection Diagnostics</legend>
|
||||
<button onclick="runDiagnostics()">Run Diagnostics</button>
|
||||
<button onclick="clearDiagnostics()">Clear</button>
|
||||
<div id="diagnosticsResults" style="margin-top: 12px;"></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Unit Config</legend>
|
||||
<label>Unit ID</label>
|
||||
@@ -25,15 +47,87 @@
|
||||
<input id="host" value="127.0.0.1" />
|
||||
<label>Port</label>
|
||||
<input id="port" type="number" value="80" />
|
||||
<button onclick="saveConfig()">Save Config</button>
|
||||
|
||||
<div style="margin: 12px 0;">
|
||||
<label style="display: inline-flex; align-items: center; margin-right: 16px;">
|
||||
<input type="checkbox" id="tcpEnabled" checked style="width: auto; margin-right: 6px;" />
|
||||
TCP Enabled
|
||||
</label>
|
||||
<label style="display: inline-flex; align-items: center;">
|
||||
<input type="checkbox" id="ftpEnabled" style="width: auto; margin-right: 6px;" />
|
||||
FTP Enabled
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="ftpCredentials" style="display: none; margin-top: 12px; padding: 12px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 4px;">
|
||||
<label>FTP Username</label>
|
||||
<input id="ftpUsername" value="USER" />
|
||||
<label>FTP Password</label>
|
||||
<input id="ftpPassword" type="password" value="0000" />
|
||||
</div>
|
||||
|
||||
<button onclick="saveConfig()" style="margin-top: 12px;">Save Config</button>
|
||||
<button onclick="loadConfig()">Load Config</button>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Controls</legend>
|
||||
<legend>Measurement Controls</legend>
|
||||
<button onclick="start()">Start</button>
|
||||
<button onclick="stop()">Stop</button>
|
||||
<button onclick="live()">Fetch Live (DOD?)</button>
|
||||
<button onclick="pause()">Pause</button>
|
||||
<button onclick="resume()">Resume</button>
|
||||
<button onclick="reset()">Reset</button>
|
||||
<button onclick="store()">Store Data</button>
|
||||
<button onclick="live()">Fetch Live (DOD)</button>
|
||||
<button onclick="getResults()">Get Results (DLC)</button>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Power Management</legend>
|
||||
<button onclick="sleepDevice()">Sleep</button>
|
||||
<button onclick="wakeDevice()">Wake</button>
|
||||
<button onclick="getSleepStatus()">Check Sleep Status</button>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Device Info</legend>
|
||||
<button onclick="getBattery()">Get Battery</button>
|
||||
<button onclick="getClock()">Get Clock</button>
|
||||
<button onclick="syncClock()">Sync Clock to PC</button>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Measurement Settings</legend>
|
||||
<button onclick="getAllSettings()" style="margin-bottom: 12px; font-weight: bold;">Get ALL Settings</button>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="display: inline; margin-right: 8px;">Frequency Weighting:</label>
|
||||
<button onclick="getFreqWeighting()">Get</button>
|
||||
<button onclick="setFreqWeighting('A')">Set A</button>
|
||||
<button onclick="setFreqWeighting('C')">Set C</button>
|
||||
<button onclick="setFreqWeighting('Z')">Set Z</button>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: inline; margin-right: 8px;">Time Weighting:</label>
|
||||
<button onclick="getTimeWeighting()">Get</button>
|
||||
<button onclick="setTimeWeighting('F')">Set Fast</button>
|
||||
<button onclick="setTimeWeighting('S')">Set Slow</button>
|
||||
<button onclick="setTimeWeighting('I')">Set Impulse</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Live Stream (DRD)</legend>
|
||||
<button id="streamBtn" onclick="toggleStream()">Start Stream</button>
|
||||
<span id="streamStatus" style="margin-left: 12px; color: #888;">Not connected</span>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>FTP File Download</legend>
|
||||
<button onclick="enableFTP()">Enable FTP</button>
|
||||
<button onclick="disableFTP()">Disable FTP</button>
|
||||
<button onclick="checkFTPStatus()">Check FTP Status</button>
|
||||
<button onclick="listFiles()">List Files</button>
|
||||
<div id="fileList" style="margin-top: 12px; max-height: 200px; overflow-y: auto; background: #f6f8fa; border: 1px solid #d0d7de; padding: 8px;"></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
@@ -49,19 +143,119 @@
|
||||
<script>
|
||||
const logEl = document.getElementById('log');
|
||||
const statusEl = document.getElementById('status');
|
||||
const streamBtn = document.getElementById('streamBtn');
|
||||
const streamStatus = document.getElementById('streamStatus');
|
||||
|
||||
let ws = null;
|
||||
let streamUpdateCount = 0;
|
||||
|
||||
function log(msg) {
|
||||
logEl.textContent += msg + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function toggleFtpCredentials() {
|
||||
const ftpEnabled = document.getElementById('ftpEnabled').checked;
|
||||
const ftpCredentials = document.getElementById('ftpCredentials');
|
||||
ftpCredentials.style.display = ftpEnabled ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Add event listener for FTP checkbox
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('ftpEnabled').addEventListener('change', toggleFtpCredentials);
|
||||
});
|
||||
|
||||
async function runDiagnostics() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const resultsEl = document.getElementById('diagnosticsResults');
|
||||
|
||||
resultsEl.innerHTML = '<p style="color: #888;">Running diagnostics...</p>';
|
||||
log('Running diagnostics...');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/nl43/${unitId}/diagnostics`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
resultsEl.innerHTML = `<p style="color: #d00;">Diagnostics failed: ${data.detail || 'Unknown error'}</p>`;
|
||||
log(`Diagnostics failed: ${res.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build results HTML
|
||||
let html = '';
|
||||
|
||||
// Overall status summary
|
||||
const statusText = {
|
||||
'pass': '✓ All systems operational',
|
||||
'fail': '✗ Connection failed',
|
||||
'degraded': '⚠ Partial connectivity'
|
||||
};
|
||||
|
||||
html += `<div id="diagnosticsSummary" class="${data.overall_status}">`;
|
||||
html += statusText[data.overall_status] || data.overall_status;
|
||||
html += `</div>`;
|
||||
|
||||
// Individual test results
|
||||
const testNames = {
|
||||
'config_exists': '📋 Configuration',
|
||||
'tcp_enabled': '🔌 TCP Enabled',
|
||||
'modem_reachable': '📡 Modem/Router Reachable',
|
||||
'tcp_connection': '🌐 Device Port Connection',
|
||||
'command_response': '💬 Device Response',
|
||||
'ftp_status': '📁 FTP Status'
|
||||
};
|
||||
|
||||
for (const [testKey, testResult] of Object.entries(data.tests)) {
|
||||
const testName = testNames[testKey] || testKey;
|
||||
html += `<div class="diagnostic-item ${testResult.status}">`;
|
||||
html += `<span class="diagnostic-status ${testResult.status}">${testResult.status}</span>`;
|
||||
html += `<strong>${testName}:</strong> ${testResult.message}`;
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `<p style="margin-top: 12px; font-size: 0.9em; color: #666;">Last run: ${new Date(data.timestamp).toLocaleString()}</p>`;
|
||||
|
||||
resultsEl.innerHTML = html;
|
||||
log(`Diagnostics complete: ${data.overall_status}`);
|
||||
|
||||
} catch (err) {
|
||||
resultsEl.innerHTML = `<p style="color: #d00;">Error running diagnostics: ${err.message}</p>`;
|
||||
log(`Diagnostics error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function clearDiagnostics() {
|
||||
document.getElementById('diagnosticsResults').innerHTML = '';
|
||||
log('Diagnostics cleared');
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const host = document.getElementById('host').value;
|
||||
const port = parseInt(document.getElementById('port').value, 10);
|
||||
const tcpEnabled = document.getElementById('tcpEnabled').checked;
|
||||
const ftpEnabled = document.getElementById('ftpEnabled').checked;
|
||||
const ftpUsername = document.getElementById('ftpUsername').value;
|
||||
const ftpPassword = document.getElementById('ftpPassword').value;
|
||||
|
||||
const config = {
|
||||
host,
|
||||
tcp_port: port,
|
||||
tcp_enabled: tcpEnabled,
|
||||
ftp_enabled: ftpEnabled
|
||||
};
|
||||
|
||||
// Only include FTP credentials if FTP is enabled
|
||||
if (ftpEnabled) {
|
||||
config.ftp_username = ftpUsername;
|
||||
config.ftp_password = ftpPassword;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/nl43/${unitId}/config`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ host, tcp_port: port })
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
const data = await res.json();
|
||||
log(`Saved config: ${JSON.stringify(data)}`);
|
||||
@@ -78,6 +272,12 @@
|
||||
const data = response.data;
|
||||
document.getElementById('host').value = data.host;
|
||||
document.getElementById('port').value = data.tcp_port;
|
||||
document.getElementById('tcpEnabled').checked = data.tcp_enabled || false;
|
||||
document.getElementById('ftpEnabled').checked = data.ftp_enabled || false;
|
||||
|
||||
// Show/hide FTP credentials based on FTP enabled status
|
||||
toggleFtpCredentials();
|
||||
|
||||
log(`Loaded config: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
@@ -93,6 +293,13 @@
|
||||
log(`Stop: ${res.status}`);
|
||||
}
|
||||
|
||||
async function store() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/store`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Store: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
async function live() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/live`);
|
||||
@@ -104,6 +311,425 @@
|
||||
statusEl.textContent = JSON.stringify(data.data, null, 2);
|
||||
log(`Live: ${JSON.stringify(data.data)}`);
|
||||
}
|
||||
|
||||
async function getResults() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/results`);
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
log(`Get Results failed: ${res.status} ${JSON.stringify(data)}`);
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = JSON.stringify(data.data, null, 2);
|
||||
log(`Results (DLC): Retrieved final calculation data`);
|
||||
}
|
||||
|
||||
// New measurement control functions
|
||||
async function pause() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/pause`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Pause: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/resume`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Resume: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/reset`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Reset: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
// Power management functions
|
||||
async function sleepDevice() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/sleep`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Device entering sleep mode`);
|
||||
} else {
|
||||
log(`Sleep failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function wakeDevice() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/wake`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Device waking from sleep mode`);
|
||||
} else {
|
||||
log(`Wake failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSleepStatus() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/sleep/status`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Sleep Status: ${data.sleep_status}`);
|
||||
} else {
|
||||
log(`Get Sleep Status failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Device info functions
|
||||
async function getBattery() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/battery`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Battery Level: ${data.battery_level}%`);
|
||||
} else {
|
||||
log(`Get Battery failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getClock() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/clock`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Device Clock: ${data.clock}`);
|
||||
} else {
|
||||
log(`Get Clock failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncClock() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const now = new Date();
|
||||
const datetime = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')},${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
const res = await fetch(`/api/nl43/${unitId}/clock`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ datetime })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Clock synced to: ${datetime}`);
|
||||
} else {
|
||||
log(`Sync Clock failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Measurement settings functions
|
||||
async function 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`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Frequency Weighting (Main): ${data.frequency_weighting}`);
|
||||
} else {
|
||||
log(`Get Freq Weighting failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setFreqWeighting(weighting) {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/frequency-weighting`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ weighting, channel: 'Main' })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Frequency Weighting set to: ${weighting}`);
|
||||
} else {
|
||||
log(`Set Freq Weighting failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getTimeWeighting() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/time-weighting?channel=Main`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Time Weighting (Main): ${data.time_weighting}`);
|
||||
} else {
|
||||
log(`Get Time Weighting failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setTimeWeighting(weighting) {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/time-weighting`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ weighting, channel: 'Main' })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`Time Weighting set to: ${weighting}`);
|
||||
} else {
|
||||
log(`Set Time Weighting failed: ${res.status} - ${data.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStream() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
stopStream();
|
||||
} else {
|
||||
startStream();
|
||||
}
|
||||
}
|
||||
|
||||
function startStream() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/api/nl43/${unitId}/stream`;
|
||||
|
||||
log(`Connecting to WebSocket: ${wsUrl}`);
|
||||
streamUpdateCount = 0;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
log('WebSocket connected - DRD streaming started');
|
||||
streamBtn.textContent = 'Stop Stream';
|
||||
streamStatus.textContent = 'Connected';
|
||||
streamStatus.style.color = '#0a0';
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.error) {
|
||||
log(`Stream error: ${data.error} - ${data.detail || ''}`);
|
||||
return;
|
||||
}
|
||||
|
||||
streamUpdateCount++;
|
||||
|
||||
// Update status display with live data
|
||||
const displayData = {
|
||||
unit_id: data.unit_id,
|
||||
timestamp: data.timestamp,
|
||||
lp: data.lp,
|
||||
leq: data.leq,
|
||||
lmax: data.lmax,
|
||||
lmin: data.lmin,
|
||||
lpeak: data.lpeak
|
||||
};
|
||||
statusEl.textContent = JSON.stringify(displayData, null, 2);
|
||||
|
||||
// Log every 10th update to avoid spamming
|
||||
if (streamUpdateCount % 10 === 0) {
|
||||
log(`Stream update #${streamUpdateCount}: Lp=${data.lp} Leq=${data.leq} Lpeak=${data.lpeak}`);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
log('WebSocket error occurred');
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
log(`WebSocket closed (received ${streamUpdateCount} updates)`);
|
||||
streamBtn.textContent = 'Start Stream';
|
||||
streamStatus.textContent = 'Not connected';
|
||||
streamStatus.style.color = '#888';
|
||||
ws = null;
|
||||
};
|
||||
}
|
||||
|
||||
function stopStream() {
|
||||
if (ws) {
|
||||
log('Closing WebSocket...');
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (ws) ws.close();
|
||||
});
|
||||
|
||||
// FTP Functions
|
||||
async function enableFTP() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/enable`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Enable FTP: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
async function disableFTP() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/disable`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
log(`Disable FTP: ${res.status} - ${data.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
async function checkFTPStatus() {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/status`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
log(`FTP Status: ${data.ftp_status} (enabled: ${data.ftp_enabled})`);
|
||||
} else {
|
||||
log(`FTP Status check failed: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
let currentPath = '/';
|
||||
|
||||
async function listFiles(path = '/') {
|
||||
currentPath = path;
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/files?path=${encodeURIComponent(path)}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
log(`List files failed: ${res.status} ${JSON.stringify(data)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileListEl = document.getElementById('fileList');
|
||||
fileListEl.innerHTML = '';
|
||||
|
||||
// Add breadcrumb navigation
|
||||
const breadcrumb = document.createElement('div');
|
||||
breadcrumb.style.marginBottom = '8px';
|
||||
breadcrumb.style.padding = '4px';
|
||||
breadcrumb.style.background = '#e1e4e8';
|
||||
breadcrumb.style.borderRadius = '3px';
|
||||
breadcrumb.innerHTML = '<strong>Path:</strong> ';
|
||||
|
||||
const pathParts = path.split('/').filter(p => p);
|
||||
let builtPath = '/';
|
||||
|
||||
// Root link
|
||||
const rootLink = document.createElement('a');
|
||||
rootLink.href = '#';
|
||||
rootLink.textContent = '/';
|
||||
rootLink.style.marginRight = '4px';
|
||||
rootLink.onclick = (e) => { e.preventDefault(); listFiles('/'); };
|
||||
breadcrumb.appendChild(rootLink);
|
||||
|
||||
// Path component links
|
||||
pathParts.forEach((part, idx) => {
|
||||
builtPath += part + '/';
|
||||
const linkPath = builtPath;
|
||||
|
||||
const separator = document.createElement('span');
|
||||
separator.textContent = ' / ';
|
||||
breadcrumb.appendChild(separator);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = '#';
|
||||
link.textContent = part;
|
||||
link.style.marginRight = '4px';
|
||||
if (idx === pathParts.length - 1) {
|
||||
link.style.fontWeight = 'bold';
|
||||
link.style.color = '#000';
|
||||
}
|
||||
link.onclick = (e) => { e.preventDefault(); listFiles(linkPath); };
|
||||
breadcrumb.appendChild(link);
|
||||
});
|
||||
|
||||
fileListEl.appendChild(breadcrumb);
|
||||
|
||||
if (data.files.length === 0) {
|
||||
const emptyDiv = document.createElement('div');
|
||||
emptyDiv.textContent = 'No files found';
|
||||
emptyDiv.style.padding = '8px';
|
||||
fileListEl.appendChild(emptyDiv);
|
||||
log(`No files found in ${path}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Found ${data.count} files in ${path}`);
|
||||
|
||||
data.files.forEach(file => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.style.marginBottom = '8px';
|
||||
fileDiv.style.padding = '4px';
|
||||
fileDiv.style.borderBottom = '1px solid #ddd';
|
||||
|
||||
const icon = file.is_dir ? '📁' : '📄';
|
||||
const size = file.is_dir ? '' : ` (${(file.size / 1024).toFixed(1)} KB)`;
|
||||
|
||||
if (file.is_dir) {
|
||||
fileDiv.innerHTML = `
|
||||
${icon} <a href="#" onclick="event.preventDefault(); listFiles('${file.path}');" style="font-weight: bold;">${file.name}</a>
|
||||
<br><small style="color: #666;">${file.path}</small>
|
||||
`;
|
||||
} else {
|
||||
fileDiv.innerHTML = `
|
||||
${icon} <strong>${file.name}</strong>${size}
|
||||
<button onclick="downloadFile('${file.path}')" style="margin-left: 8px; padding: 2px 6px; font-size: 0.9em;">Download</button>
|
||||
<br><small style="color: #666;">${file.path}</small>
|
||||
`;
|
||||
}
|
||||
|
||||
fileListEl.appendChild(fileDiv);
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFile(remotePath) {
|
||||
const unitId = document.getElementById('unitId').value;
|
||||
log(`Downloading file: ${remotePath}...`);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/nl43/${unitId}/ftp/download`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ remote_path: remotePath })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
log(`Download failed: ${res.status} - ${data.detail || JSON.stringify(data)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger browser download
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = remotePath.split('/').pop();
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
log(`Downloaded: ${remotePath}`);
|
||||
} catch (error) {
|
||||
log(`Download error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
98
test_settings_endpoint.py
Normal file
98
test_settings_endpoint.py
Normal 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!")
|
||||
Reference in New Issue
Block a user