Reverses the "entry goes in with the work" rule from two commits ago. That was wrong on the evidence: of the docs(changelog) commits in history, 3 of 4 in seismo-relay and 2 of 4 in Terra-View were made directly on dev. The rule was generalized from one unrepresentative commit rather than from the pattern. It also caused the exact problem it was supposed to avoid. With four worktrees in flight, every branch edits the same few lines at the top of CHANGELOG.md; feat/ach-rescue-on-connect and feat/sensor-check-h5 collide on that file and nothing else. Writing the entry once, on dev, after the merge removes the whole conflict class. The second benefit is accuracy: an entry written after the merge describes what actually landed, including anything that changed during conflict resolution. The sensor-check branch is a live example — its Unreleased preamble describes a release that no longer looks like that. The failure mode of writing it later is forgetting, so the merge is explicitly not finished until Unreleased is updated — same sitting, reconstructed from the branch commit messages. Unchanged: no preamble under Unreleased, the mandatory operational consequence, and cutting the version on dev when ready to ship to main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qcu9ByJfuKBQxmrWb8rSrN
SLMM - Sound Level Meter Manager
Version 0.4.0
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 customized front ends or cli.
Features
- Live Monitor (fan-out): One shared cached live feed per device — many clients subscribe to the same stream instead of fighting over the meter's single TCP connection
- Alert Engine: Per-device threshold rules with onset/clear events, cooldowns, acks, and 24/7 evaluation
- History & Percentiles: Downsampled DOD trail + history endpoint for live-chart backfill; LN1/LN2 (L1/L10) percentiles surfaced through the feed
- Persistent TCP Connections: Cached per-device connections with OS-level keepalive, tuned for cellular modem reliability
- Background Polling: Continuous automatic polling of devices with configurable intervals
- Offline Detection: Automatic device reachability tracking with failure counters
- 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
- Connection Diagnostics: Live UI and API endpoints for monitoring TCP connection pool status
Architecture
┌─────────────────┐ ┌──────────────────────────────┐ ┌─────────────────┐
│ │◄───────►│ SLMM API │◄───────►│ NL43/NL53 │
│ (Frontend) │ HTTP │ • REST Endpoints │ TCP │ Sound Meters │
└─────────────────┘ │ • WebSocket Streaming │ (kept │ (via cellular │
│ • Background Poller │ alive) │ modem) │
│ • Connection Pool (v0.3) │ └─────────────────┘
└──────────────────────────────┘
│
▼
┌──────────────┐
│ SQLite DB │
│ • Config │
│ • Status │
└──────────────┘
Live Monitor — Fan-Out Feed (v0.4.0)
The NL-43 allows only one TCP control connection at a time, so multiple clients polling the same device directly would contend for it. The monitor solves this with a single shared, cached feed per device:
- One reader, many subscribers: a single poller reads the device; every
WebSocket subscriber (
WS /api/nl43/{unit_id}/monitor) receives the same frames — an instant first frame from cache, then live updates. - Persistent + auto-start: a
monitor_enabledflag keeps the feed running and auto-starts it on boot. Enabled alert rules pin the monitor on for 24/7 evaluation even with no UI connected. - Adaptive & deduplicated: poll rate adapts to demand, unreachable devices back off, and the background poller skips units already covered by a monitor.
Alert Engine (v0.4.0)
Per-device threshold alerting evaluated against the live feed:
- Rules: metric + threshold +
cooldown_s, full CRUD per device - Events: onset/clear state machine, acknowledgement, and a device-offline alert when a monitored unit drops
- Robust: editing/deleting a rule resets its state and closes open events
Persistent TCP Connection Pool (v0.3.0)
SLMM maintains persistent TCP connections to devices with OS-level keepalive, designed for reliable operation over cellular modems:
- Connection Reuse: One cached TCP socket per device, reused across all commands (no repeated handshakes)
- TCP Keepalive: Probes keep cellular NAT tables alive and detect dead connections early
- Transparent Retry: Stale cached connections automatically retry with a fresh socket
- Configurable: Idle TTL (300s), max age (1800s), and keepalive timing via environment variables
- Diagnostics: Live UI on the roster page and API endpoints for monitoring pool status
Background Polling (v0.2.0)
Background polling service continuously queries devices and updates the status cache:
- Automatic Updates: Devices are polled at configurable intervals (10-3600 seconds)
- Offline Detection: Devices marked unreachable after 3 consecutive failures
- Per-Device Configuration: Each device can have a custom polling interval
- Resource Efficient: Dynamic sleep intervals and smart scheduling
Status requests return cached data instantly (<100ms) instead of waiting for device queries (1-2 seconds).
Quick Start
Prerequisites
- Python 3.10+
- pip package manager
Installation
- Clone the repository:
git clone <repository-url>
cd slmm
- Install dependencies:
pip install -r requirements.txt
Running the Server
# 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
Server:
PORT: Server port (default: 8100)CORS_ORIGINS: Comma-separated list of allowed origins (default: "*")
TCP Connection Pool:
TCP_PERSISTENT_ENABLED: Enable persistent connections (default: "true")TCP_IDLE_TTL: Close idle connections after N seconds (default: 300)TCP_MAX_AGE: Force reconnect after N seconds (default: 1800)TCP_KEEPALIVE_IDLE: Seconds idle before keepalive probes (default: 15)TCP_KEEPALIVE_INTERVAL: Seconds between keepalive probes (default: 10)TCP_KEEPALIVE_COUNT: Failed probes before declaring dead (default: 3)
Database
The SQLite database is automatically created at data/slmm.db on first run.
Logging
Logs are written to:
- Console output (stdout)
- 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 (updated by background poller) |
| GET | /api/nl43/{unit_id}/live |
Request fresh DOD data from device (bypasses cache) |
| GET | /api/nl43/{unit_id}/history |
Downsampled DOD trail for live-chart backfill |
| WS | /api/nl43/{unit_id}/stream |
WebSocket stream for real-time DRD data |
Live Monitor (fan-out feed)
| Method | Endpoint | Description |
|---|---|---|
| WS | /api/nl43/{unit_id}/monitor |
Subscribe to the shared cached live feed (instant first frame) |
| POST | /api/nl43/{unit_id}/monitor/start |
Start the device's monitor feed |
| POST | /api/nl43/{unit_id}/monitor/stop |
Stop the device's monitor feed |
| GET | /api/nl43/_monitor/status |
Global monitor status across devices |
| POST | /api/nl43/{unit_id}/disconnect |
Drop the device's pooled TCP connection |
| POST | /api/nl43/{unit_id}/deactivate |
Quiesce polling/monitoring for one device |
| POST | /api/nl43/_system/standby |
Global standby — quiesce all polling/monitoring |
Alerts
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/nl43/{unit_id}/alerts/rules |
List alert rules for a device |
| POST | /api/nl43/{unit_id}/alerts/rules |
Create an alert rule (metric, threshold, cooldown) |
| PUT | /api/nl43/{unit_id}/alerts/rules/{rule_id} |
Update a rule (resets its state, closes open events) |
| DELETE | /api/nl43/{unit_id}/alerts/rules/{rule_id} |
Delete a rule |
| GET | /api/nl43/{unit_id}/alerts/events |
List alert events (onset/clear) |
| POST | /api/nl43/{unit_id}/alerts/events/{event_id}/ack |
Acknowledge an event |
Background Polling
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/nl43/{unit_id}/polling/config |
Get device polling configuration |
| PUT | /api/nl43/{unit_id}/polling/config |
Update polling interval and enable/disable polling |
| GET | /api/nl43/_polling/status |
Get global polling status for all devices |
Connection Pool
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/nl43/_connections/status |
Get pool config, active connections, age/idle times |
| POST | /api/nl43/_connections/flush |
Force-close all cached TCP connections |
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.
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
│ ├── background_poller.py # Background polling service ⭐ NEW
│ └── 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
├── migrate_add_polling_fields.py # Database migration for v0.2.0 ⭐ NEW
├── test_polling.sh # Polling feature test script ⭐ NEW
├── API.md # Detailed API documentation
├── COMMUNICATION_GUIDE.md # NL43 protocol documentation
├── NL43_COMMANDS.md # Command reference
├── CHANGELOG.md # Version history ⭐ NEW
├── requirements.txt # Python dependencies
└── README.md # This file
Database Schema
NL43Config Table
Stores device connection configuration:
unit_id(PK): Unique device identifierhost: Device IP address or hostnametcp_port: TCP control port (default: 80)tcp_enabled: Enable/disable TCP communicationftp_enabled: Enable/disable FTP functionalityftp_username: FTP authentication usernameftp_password: FTP authentication passwordweb_enabled: Enable/disable web interface accesspoll_interval_seconds: Polling interval in seconds (10-3600, default: 60) ⭐ NEWpoll_enabled: Enable/disable background polling for this device ⭐ NEW
NL43Status Table
Caches latest measurement snapshot:
unit_id(PK): Unique device identifierlast_seen: Timestamp of last updatemeasurement_state: Current state (Measure/Stop)measurement_start_time: When measurement started (UTC)counter: Measurement interval counter (1-600)lp: Instantaneous sound pressure levelleq: Equivalent continuous sound levellmax: Maximum sound levellmin: Minimum sound levellpeak: Peak sound levelbattery_level: Battery percentagepower_source: Current power sourcesd_remaining_mb: Free SD card space (MB)sd_free_ratio: SD card free space ratioraw_payload: Raw device response datais_reachable: Device reachability status (Boolean)consecutive_failures: Count of consecutive poll failureslast_poll_attempt: Last time background poller attempted to polllast_success: Last successful poll timestamplast_error: Last error message (truncated to 500 chars)ln1/ln2: LN1/LN2 (L1/L10) percentile levels ⭐ v0.4.0
NL43Readings Table ⭐ v0.4.0
Downsampled DOD trail backing the live-chart history endpoint (one row/minute, pruned to a retention window — viewing only, not the report source):
id(PK),unit_id,timestamplp/leq/lmax/ln1/ln2: cached level samples
AlertRule Table ⭐ v0.4.0
Per-device threshold alert rules:
id(PK),unit_id,name,enabledmetric,comparison(above/below),threshold_db,clear_margin_db(hysteresis)duration_s(sustained),cooldown_s(min seconds between onsets)channels/recipients, optionalschedule_start/schedule_end/schedule_days
AlertEvent Table ⭐ v0.4.0
Alert onset/clear events for history, inbox, and acknowledgement:
id(PK),unit_id,rule_id,rule_name,metric,threshold_dbonset_at/onset_value,peak_value,clear_at,status(active/cleared)acknowledged_at/acknowledged_by,notes
New tables (
alert_rules,alert_events,nl43_readings) auto-create on startup. Existing-table columns ship with migrations:migrate_add_ln_percentiles.py,migrate_add_monitor_enabled.py.
Protocol Details
TCP Communication
- Uses ASCII command protocol over TCP
- Persistent connections with OS-level keepalive (tuned for cellular modems)
- Connections cached per device and reused across commands
- Transparent retry on stale connections
- 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
- Default NL43 FTP Credentials: Username:
USER, Password:0000
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
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_enabled": true,
"ftp_username": "USER",
"ftp_password": "0000"
}'
Start Measurement
curl -X POST http://localhost:8100/api/nl43/meter-001/start
Get Cached Status (Fast - from background poller)
curl http://localhost:8100/api/nl43/meter-001/status
Get Live Status (Bypasses cache)
curl http://localhost:8100/api/nl43/meter-001/live
Configure Background Polling ⭐ NEW
# Set polling interval to 30 seconds
curl -X PUT http://localhost:8100/api/nl43/meter-001/polling/config \
-H "Content-Type: application/json" \
-d '{
"poll_interval_seconds": 30,
"poll_enabled": true
}'
# Get polling configuration
curl http://localhost:8100/api/nl43/meter-001/polling/config
# Check global polling status
curl http://localhost:8100/api/nl43/_polling/status
Check Connection Pool Status
curl http://localhost:8100/api/nl43/_connections/status | jq '.'
Flush All Cached Connections
curl -X POST http://localhost:8100/api/nl43/_connections/flush
Verify Device Settings
curl http://localhost:8100/api/nl43/meter-001/settings
This returns all current device configuration:
{
"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)
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
# 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:
- Use the config endpoints to register and configure devices
- Poll or stream live status for real-time monitoring
- Use control endpoints to manage measurements
- Download files via FTP endpoints for analysis
See API.md for detailed integration examples.
Troubleshooting
Connection Issues
- Check connection pool status:
curl http://localhost:8100/api/nl43/_connections/status - Flush stale connections:
curl -X POST http://localhost:8100/api/nl43/_connections/flush - 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
Cellular Modem Issues
- If modem wedges from too many handshakes, ensure
TCP_PERSISTENT_ENABLED=true(default) - Increase
TCP_IDLE_TTLif connections expire between poll cycles - Keepalive probes (default: every 15s) keep NAT tables alive — adjust
TCP_KEEPALIVE_IDLEif needed - Set
TCP_PERSISTENT_ENABLED=falseto disable pooling for debugging
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
# Add test commands when implemented
pytest
Database Migrations
# Migrate to v0.2.0 (add background polling fields)
python3 migrate_add_polling_fields.py
# Legacy: Migrate 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>
Testing Background Polling
# Run comprehensive polling tests
./test_polling.sh [unit_id]
# Test settings endpoint
python3 test_settings_endpoint.py <unit_id>
# Test sleep mode auto-disable
python3 test_sleep_mode_auto_disable.py <unit_id>
Legacy Scripts
Old migration scripts and manual polling tools have been moved to archive/ for reference. See archive/README.md for details.
Contributing
This is a standalone module kept separate from the SFM/Terra-View codebase. When contributing:
- Maintain separation from frontend code
- Follow existing API patterns and error handling
- Update API documentation for new endpoints
- Ensure rate limiting is enforced for device commands
License
[Specify license here]
Related Documentation
- API.md - Complete API reference with examples
- COMMUNICATION_GUIDE.md - NL43 protocol details
- NL43_COMMANDS.md - Device command reference
- 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