# Changelog All notable changes to SLMM (Sound Level Meter Manager) will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added #### Wedge Detection & Automatic Recovery - **Failure classification** - Poll failures are now probed and classified: `wedged` (control port REFUSES — device stack alive, listener gone), `offline` (timeout — power/cellular loss), or `ok` (transient). The wedge signature matches the Feb 2026 investigation: refused on 2255 while FTP/21 accepts is definitive (high confidence); refused alone is medium confidence. - **RecoveryManager** (`app/recovery.py`) - Per-device recovery state machine: confirm wedge → trigger reset → wait for boot → reconnect → resume measurement (via existing `start_cycle` with overwrite protection) → log the full incident to device logs (category `RECOVERY`). - **Pluggable reset backends**: - `manual` (default) - logs/flags the wedge for a human; still auto-resumes measurement when the device returns - `webhook` - HTTP GET/POST to a relay controller (Pi GPIO relay, Shelly, Tasmota, etc.) that power-cycles the NL-43 - **Safety guards** - per-device `auto_recovery_enabled` master switch (default off), confirmation threshold (`WEDGE_CONFIRM_FAILURES`, default 2 consecutive failures), windowed attempt limit (default 2 per 6 h), one recovery at a time per device. - **New endpoints**: - `GET /api/nl43/{unit_id}/recovery/status` - connection/recovery state, wedge count, last result - `POST /api/nl43/{unit_id}/recovery/probe` - diagnostic port probe + classification (no action taken) - `POST /api/nl43/{unit_id}/recovery/trigger` - manually start recovery (bypasses confirmation threshold) - **Config fields** (via `PUT /{unit_id}/config`): `auto_recovery_enabled`, `reset_backend`, `reset_webhook_url`, `reset_webhook_method`, `recovery_boot_wait_seconds`, `recovery_reconnect_timeout`, `recovery_auto_resume`, `recovery_max_attempts`, `recovery_window_minutes` - **Status fields**: `connection_state`, `last_wedge_at`, `wedge_count`, `recovery_state`, `last_recovery_at`, `last_recovery_result` - **Migration**: `migrate_add_recovery_fields.py` - **Test suite**: `test_wedge_recovery.py` - fake wedge-able NL-43 + fake relay webhook; covers classification, end-to-end recovery with measurement resume, and gating (23 assertions) ## [0.3.0] - 2026-02-17 ### Added #### Persistent TCP Connection Pool - **Connection reuse** - TCP connections are cached per device and reused across commands, eliminating repeated TCP handshakes over cellular modems - **OS-level TCP keepalive** - Configurable keepalive probes keep cellular NAT tables alive and detect dead connections early (default: probe after 15s idle, every 10s, 3 failures = dead) - **Transparent retry** - If a cached connection goes stale, the system automatically retries with a fresh connection so failures are never visible to the caller - **Stale connection detection** - Multi-layer detection via idle TTL, max age, transport state, and reader EOF checks - **Background cleanup** - Periodic task (every 30s) evicts expired connections from the pool - **Master switch** - Set `TCP_PERSISTENT_ENABLED=false` to revert to per-request connection behavior #### Connection Pool Diagnostics - `GET /api/nl43/_connections/status` - View pool configuration, active connections, age/idle times, and keepalive settings - `POST /api/nl43/_connections/flush` - Force-close all cached connections (useful for debugging) - **Connections tab on roster page** - Live UI showing pool config, active connections with age/idle/alive status, auto-refreshes every 5s, and flush button #### Environment Variables - `TCP_PERSISTENT_ENABLED` (default: `true`) - Master switch for persistent connections - `TCP_IDLE_TTL` (default: `300`) - Close idle connections after N seconds - `TCP_MAX_AGE` (default: `1800`) - Force reconnect after N seconds - `TCP_KEEPALIVE_IDLE` (default: `15`) - Seconds idle before keepalive probes start - `TCP_KEEPALIVE_INTERVAL` (default: `10`) - Seconds between keepalive probes - `TCP_KEEPALIVE_COUNT` (default: `3`) - Failed probes before declaring connection dead ### Changed - **Health check endpoint** (`/health/devices`) - Now uses connection pool instead of opening throwaway TCP connections; checks for existing live connections first (zero-cost), only opens new connection through pool if needed - **Diagnostics endpoint** - Removed separate port 443 modem check (extra handshake waste); TCP reachability test now uses connection pool - **DRD streaming** - Streaming connections now get TCP keepalive options set; cached connections are evicted before opening dedicated streaming socket - **Default timeouts tuned for cellular** - Idle TTL raised to 300s (5 min), max age raised to 1800s (30 min) to survive typical polling intervals over cellular links ### Technical Details #### Architecture - `ConnectionPool` class in `services.py` manages a single cached connection per device key (NL-43 only supports one TCP connection at a time) - Uses existing per-device asyncio locks and rate limiting — no changes to concurrency model - Pool is a module-level singleton initialized from environment variables at import time - Lifecycle managed via FastAPI lifespan: cleanup task starts on startup, all connections closed on shutdown - `_send_command_unlocked()` refactored to use acquire/release/discard pattern with single-retry fallback - Command parsing extracted to `_execute_command()` method for reuse between primary and retry paths #### Cellular Modem Optimizations - Keepalive probes at 15s prevent cellular NAT tables from expiring (typically 30-60s timeout) - 300s idle TTL ensures connections survive between polling cycles (default 60s interval) - 1800s max age allows a single socket to serve ~30 minutes of polling before forced reconnect - Health checks and diagnostics produce zero additional TCP handshakes when a pooled connection exists - Stale `$` prompt bytes drained from idle connections before command reuse ### Breaking Changes None. This release is fully backward-compatible with v0.2.x. Set `TCP_PERSISTENT_ENABLED=false` for identical behavior to previous versions. --- ## [0.2.1] - 2026-01-23 ### Added - **Roster management**: UI and API endpoints for managing device rosters. - **Delete config endpoint**: Remove device configuration alongside cached status data. - **Scheduler hooks**: `start_cycle` and `stop_cycle` helpers for Terra-View scheduling integration. ### Changed - **FTP logging**: Connection, authentication, and transfer phases now log explicitly. - **Documentation**: Reorganized docs/scripts and updated API notes for FTP/TCP verification. ## [0.2.0] - 2026-01-15 ### Added #### Background Polling System - **Continuous automatic device polling** - Background service that continuously polls configured devices - **Per-device configurable intervals** - Each device can have custom polling interval (10-3600 seconds, default 60) - **Automatic offline detection** - Devices automatically marked unreachable after 3 consecutive failures - **Reachability tracking** - Database fields track device health with failure counters and error messages - **Dynamic sleep scheduling** - Polling service adjusts sleep intervals based on device configurations - **Graceful lifecycle management** - Background poller starts on application startup and stops cleanly on shutdown #### New API Endpoints - `GET /api/nl43/{unit_id}/polling/config` - Get device polling configuration - `PUT /api/nl43/{unit_id}/polling/config` - Update polling interval and enable/disable per-device polling - `GET /api/nl43/_polling/status` - Get global polling status for all devices with reachability info #### Database Schema Changes - **NL43Config table**: - `poll_interval_seconds` (Integer, default 60) - Polling interval in seconds - `poll_enabled` (Boolean, default true) - Enable/disable background polling per device - **NL43Status table**: - `is_reachable` (Boolean, default true) - Current device reachability status - `consecutive_failures` (Integer, default 0) - Count of consecutive poll failures - `last_poll_attempt` (DateTime) - Last time background poller attempted to poll - `last_success` (DateTime) - Last successful poll timestamp - `last_error` (Text) - Last error message (truncated to 500 chars) #### New Files - `app/background_poller.py` - Background polling service implementation - `migrate_add_polling_fields.py` - Database migration script for v0.2.0 schema changes - `test_polling.sh` - Comprehensive test script for polling functionality - `CHANGELOG.md` - This changelog file ### Changed - **Enhanced status endpoint** - `GET /api/nl43/{unit_id}/status` now includes polling-related fields (is_reachable, consecutive_failures, last_poll_attempt, last_success, last_error) - **Application startup** - Added lifespan context manager in `app/main.py` to manage background poller lifecycle - **Performance improvement** - Terra-View requests now return cached data instantly (<100ms) instead of waiting for device queries (1-2 seconds) ### Technical Details #### Architecture - Background poller runs as async task using `asyncio.create_task()` - Uses existing `NL43Client` and `persist_snapshot()` functions - no code duplication - Respects existing 1-second rate limiting per device - Efficient resource usage - skips work when no devices configured - WebSocket streaming remains unaffected - separate real-time data path #### Default Behavior - Existing devices automatically get 60-second polling interval - Existing status records default to `is_reachable=true` - Migration is additive-only - no data loss - Polling can be disabled per-device via `poll_enabled=false` #### Recommended Intervals - Critical monitoring: 30 seconds - Normal monitoring: 60 seconds (default) - Battery conservation: 300 seconds (5 minutes) - Development/testing: 10 seconds (minimum allowed) ### Migration Notes To upgrade from v0.1.x to v0.2.0: 1. **Stop the service** (if running): ```bash docker compose down slmm # OR # Stop your uvicorn process ``` 2. **Update code**: ```bash git pull # OR copy new files ``` 3. **Run migration**: ```bash cd slmm python3 migrate_add_polling_fields.py ``` 4. **Restart service**: ```bash docker compose up -d --build slmm # OR uvicorn app.main:app --host 0.0.0.0 --port 8100 ``` 5. **Verify polling is active**: ```bash curl http://localhost:8100/api/nl43/_polling/status | jq '.' ``` You should see `"poller_running": true` and all configured devices listed. ### Breaking Changes None. This release is fully backward-compatible with v0.1.x. All existing endpoints and functionality remain unchanged. --- ## [0.1.0] - 2025-12-XX ### Added - Initial release - REST API for NL43/NL53 sound level meter control - TCP command protocol implementation - FTP file download support - WebSocket streaming for real-time data (DRD) - Device configuration management - Measurement control (start, stop, pause, resume, reset, store) - Device information endpoints (battery, clock, results) - Measurement settings management (frequency/time weighting) - Sleep mode control - Rate limiting (1-second minimum between commands) - SQLite database for device configs and status cache - Health check endpoints - Comprehensive API documentation - NL43 protocol documentation ### Database Schema (v0.1.0) - **NL43Config table** - Device connection configuration - **NL43Status table** - Measurement snapshot cache --- ## Version History Summary - **v0.3.0** (2026-02-17) - Persistent TCP connections with keepalive for cellular modem reliability - **v0.2.1** (2026-01-23) - Roster management, scheduler hooks, FTP logging, doc cleanup - **v0.2.0** (2026-01-15) - Background Polling System - **v0.1.0** (2025-12-XX) - Initial Release