feat: standardize device type for Sound Level Meters (SLM)
- Updated all instances of device_type from "sound_level_meter" to "slm" across the codebase. - Enhanced documentation to reflect the new device type standardization. - Added migration script to convert legacy device types in the database. - Updated relevant API endpoints, models, and frontend templates to use the new device type. - Ensured backward compatibility by deprecating the old device type without data loss.
This commit is contained in:
35
README.md
35
README.md
@@ -308,7 +308,7 @@ print(response.json())
|
|||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| id | string | Unit identifier (primary key) |
|
| id | string | Unit identifier (primary key) |
|
||||||
| unit_type | string | Hardware model name (default: `series3`) |
|
| unit_type | string | Hardware model name (default: `series3`) |
|
||||||
| device_type | string | `seismograph` or `modem` discriminator |
|
| device_type | string | Device type: `"seismograph"`, `"modem"`, or `"slm"` (sound level meter) |
|
||||||
| deployed | boolean | Whether the unit is in the field |
|
| deployed | boolean | Whether the unit is in the field |
|
||||||
| retired | boolean | Removes the unit from deployments but preserves history |
|
| retired | boolean | Removes the unit from deployments but preserves history |
|
||||||
| note | string | Notes about the unit |
|
| note | string | Notes about the unit |
|
||||||
@@ -334,6 +334,39 @@ print(response.json())
|
|||||||
| phone_number | string | Cellular number for the modem |
|
| phone_number | string | Cellular number for the modem |
|
||||||
| hardware_model | string | Modem hardware reference |
|
| hardware_model | string | Modem hardware reference |
|
||||||
|
|
||||||
|
**Sound Level Meter (SLM) fields**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| slm_host | string | Direct IP address for SLM (if not using modem) |
|
||||||
|
| slm_tcp_port | integer | TCP control port (default: 2255) |
|
||||||
|
| slm_ftp_port | integer | FTP file transfer port (default: 21) |
|
||||||
|
| slm_model | string | Device model (NL-43, NL-53) |
|
||||||
|
| slm_serial_number | string | Manufacturer serial number |
|
||||||
|
| slm_frequency_weighting | string | Frequency weighting setting (A, C, Z) |
|
||||||
|
| slm_time_weighting | string | Time weighting setting (F=Fast, S=Slow) |
|
||||||
|
| slm_measurement_range | string | Measurement range setting |
|
||||||
|
| slm_last_check | datetime | Last status check timestamp |
|
||||||
|
| deployed_with_modem_id | string | Modem pairing (shared with seismographs) |
|
||||||
|
|
||||||
|
### Device Type Schema
|
||||||
|
|
||||||
|
Terra-View supports three device types with the following standardized `device_type` values:
|
||||||
|
|
||||||
|
- **`"seismograph"`** (default) - Seismic monitoring devices (Series 3, Series 4, Micromate)
|
||||||
|
- Uses: calibration dates, modem pairing
|
||||||
|
- Examples: BE1234, UM12345 (Series 3/4 units)
|
||||||
|
|
||||||
|
- **`"modem"`** - Field modems and network equipment
|
||||||
|
- Uses: IP address, phone number, hardware model
|
||||||
|
- Examples: MDM001, MODEM-2025-01
|
||||||
|
|
||||||
|
- **`"slm"`** - Sound level meters (Rion NL-43/NL-53)
|
||||||
|
- Uses: TCP/FTP configuration, measurement settings, modem pairing
|
||||||
|
- Examples: SLM-43-01, NL43-001
|
||||||
|
|
||||||
|
**Important**: All `device_type` values must be lowercase. The legacy value `"sound_level_meter"` has been deprecated in favor of the shorter `"slm"`. Run `backend/migrate_standardize_device_types.py` to update existing databases.
|
||||||
|
|
||||||
### Emitter Table (Device Check-ins)
|
### Emitter Table (Device Check-ins)
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|
|||||||
@@ -111,26 +111,6 @@ async def startup_event():
|
|||||||
await start_scheduler()
|
await start_scheduler()
|
||||||
logger.info("Scheduler service started")
|
logger.info("Scheduler service started")
|
||||||
|
|
||||||
# Sync all SLMs to SLMM on startup
|
|
||||||
logger.info("Syncing SLM devices to SLMM...")
|
|
||||||
try:
|
|
||||||
from backend.services.slmm_sync import sync_all_slms_to_slmm, cleanup_orphaned_slmm_devices
|
|
||||||
from backend.database import SessionLocal
|
|
||||||
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
|
||||||
# Sync all SLMs from roster to SLMM
|
|
||||||
sync_results = await sync_all_slms_to_slmm(db)
|
|
||||||
logger.info(f"SLM sync complete: {sync_results}")
|
|
||||||
|
|
||||||
# Clean up orphaned devices in SLMM
|
|
||||||
cleanup_results = await cleanup_orphaned_slmm_devices(db)
|
|
||||||
logger.info(f"SLMM cleanup complete: {cleanup_results}")
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error syncing SLMs to SLMM on startup: {e}")
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
def shutdown_event():
|
def shutdown_event():
|
||||||
"""Clean up services on app shutdown"""
|
"""Clean up services on app shutdown"""
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def migrate():
|
|||||||
print("\n○ No migration needed - all columns already exist.")
|
print("\n○ No migration needed - all columns already exist.")
|
||||||
|
|
||||||
print("\nSound level meter fields are now available in the roster table.")
|
print("\nSound level meter fields are now available in the roster table.")
|
||||||
print("You can now set device_type='sound_level_meter' for SLM devices.")
|
print("Note: Use device_type='slm' for Sound Level Meters. Legacy 'sound_level_meter' has been deprecated.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
106
backend/migrate_standardize_device_types.py
Normal file
106
backend/migrate_standardize_device_types.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""
|
||||||
|
Database Migration: Standardize device_type values
|
||||||
|
|
||||||
|
This migration ensures all device_type values follow the official schema:
|
||||||
|
- "seismograph" - Seismic monitoring devices
|
||||||
|
- "modem" - Field modems and network equipment
|
||||||
|
- "slm" - Sound level meters (NL-43/NL-53)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Converts "sound_level_meter" → "slm"
|
||||||
|
- Safe to run multiple times (idempotent)
|
||||||
|
- No data loss
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python backend/migrate_standardize_device_types.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add parent directory to path so we can import backend modules
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./data/seismo_fleet.db"
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
"""Standardize device_type values in the database"""
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("=" * 70)
|
||||||
|
print("Database Migration: Standardize device_type values")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check for existing "sound_level_meter" values
|
||||||
|
result = db.execute(
|
||||||
|
text("SELECT COUNT(*) as count FROM roster WHERE device_type = 'sound_level_meter'")
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
count_to_migrate = result[0] if result else 0
|
||||||
|
|
||||||
|
if count_to_migrate == 0:
|
||||||
|
print("✓ No records need migration - all device_type values are already standardized")
|
||||||
|
print()
|
||||||
|
print("Current device_type distribution:")
|
||||||
|
|
||||||
|
# Show distribution
|
||||||
|
distribution = db.execute(
|
||||||
|
text("SELECT device_type, COUNT(*) as count FROM roster GROUP BY device_type ORDER BY count DESC")
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
for row in distribution:
|
||||||
|
device_type, count = row
|
||||||
|
print(f" - {device_type}: {count} units")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Migration not needed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {count_to_migrate} record(s) with device_type='sound_level_meter'")
|
||||||
|
print()
|
||||||
|
print("Converting 'sound_level_meter' → 'slm'...")
|
||||||
|
|
||||||
|
# Perform the migration
|
||||||
|
db.execute(
|
||||||
|
text("UPDATE roster SET device_type = 'slm' WHERE device_type = 'sound_level_meter'")
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
print(f"✓ Successfully migrated {count_to_migrate} record(s)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Show final distribution
|
||||||
|
print("Updated device_type distribution:")
|
||||||
|
distribution = db.execute(
|
||||||
|
text("SELECT device_type, COUNT(*) as count FROM roster GROUP BY device_type ORDER BY count DESC")
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
for row in distribution:
|
||||||
|
device_type, count = row
|
||||||
|
print(f" - {device_type}: {count} units")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print("Migration completed successfully!")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print(f"\n❌ Error during migration: {e}")
|
||||||
|
print("\nRolling back changes...")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate()
|
||||||
@@ -19,14 +19,17 @@ class RosterUnit(Base):
|
|||||||
Roster table: represents our *intended assignment* of a unit.
|
Roster table: represents our *intended assignment* of a unit.
|
||||||
This is editable from the GUI.
|
This is editable from the GUI.
|
||||||
|
|
||||||
Supports multiple device types (seismograph, modem, sound_level_meter) with type-specific fields.
|
Supports multiple device types with type-specific fields:
|
||||||
|
- "seismograph" - Seismic monitoring devices (default)
|
||||||
|
- "modem" - Field modems and network equipment
|
||||||
|
- "slm" - Sound level meters (NL-43/NL-53)
|
||||||
"""
|
"""
|
||||||
__tablename__ = "roster"
|
__tablename__ = "roster"
|
||||||
|
|
||||||
# Core fields (all device types)
|
# Core fields (all device types)
|
||||||
id = Column(String, primary_key=True, index=True)
|
id = Column(String, primary_key=True, index=True)
|
||||||
unit_type = Column(String, default="series3") # Backward compatibility
|
unit_type = Column(String, default="series3") # Backward compatibility
|
||||||
device_type = Column(String, default="seismograph") # "seismograph" | "modem" | "sound_level_meter"
|
device_type = Column(String, default="seismograph") # "seismograph" | "modem" | "slm"
|
||||||
deployed = Column(Boolean, default=True)
|
deployed = Column(Boolean, default=True)
|
||||||
retired = Column(Boolean, default=False)
|
retired = Column(Boolean, default=False)
|
||||||
note = Column(String, nullable=True)
|
note = Column(String, nullable=True)
|
||||||
@@ -197,7 +200,7 @@ class UnitAssignment(Base):
|
|||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
|
|
||||||
# Denormalized for efficient queries
|
# Denormalized for efficient queries
|
||||||
device_type = Column(String, nullable=False) # sound_level_meter | seismograph
|
device_type = Column(String, nullable=False) # "slm" | "seismograph"
|
||||||
project_id = Column(String, nullable=False, index=True) # FK to Project.id
|
project_id = Column(String, nullable=False, index=True) # FK to Project.id
|
||||||
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
@@ -216,7 +219,7 @@ class ScheduledAction(Base):
|
|||||||
unit_id = Column(String, nullable=True, index=True) # FK to RosterUnit.id (nullable if location-based)
|
unit_id = Column(String, nullable=True, index=True) # FK to RosterUnit.id (nullable if location-based)
|
||||||
|
|
||||||
action_type = Column(String, nullable=False) # start, stop, download, calibrate
|
action_type = Column(String, nullable=False) # start, stop, download, calibrate
|
||||||
device_type = Column(String, nullable=False) # sound_level_meter | seismograph
|
device_type = Column(String, nullable=False) # "slm" | "seismograph"
|
||||||
|
|
||||||
scheduled_time = Column(DateTime, nullable=False, index=True)
|
scheduled_time = Column(DateTime, nullable=False, index=True)
|
||||||
executed_at = Column(DateTime, nullable=True)
|
executed_at = Column(DateTime, nullable=True)
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ async def assign_unit_to_location(
|
|||||||
raise HTTPException(status_code=404, detail="Unit not found")
|
raise HTTPException(status_code=404, detail="Unit not found")
|
||||||
|
|
||||||
# Check device type matches location type
|
# Check device type matches location type
|
||||||
expected_device_type = "sound_level_meter" if location.location_type == "sound" else "seismograph"
|
expected_device_type = "slm" if location.location_type == "sound" else "seismograph"
|
||||||
if unit.device_type != expected_device_type:
|
if unit.device_type != expected_device_type:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
@@ -375,7 +375,7 @@ async def get_available_units(
|
|||||||
Filters by device type matching the location type.
|
Filters by device type matching the location type.
|
||||||
"""
|
"""
|
||||||
# Determine required device type
|
# Determine required device type
|
||||||
required_device_type = "sound_level_meter" if location_type == "sound" else "seismograph"
|
required_device_type = "slm" if location_type == "sound" else "seismograph"
|
||||||
|
|
||||||
# Get all units of the required type that are deployed and not retired
|
# Get all units of the required type that are deployed and not retired
|
||||||
all_units = db.query(RosterUnit).filter(
|
all_units = db.query(RosterUnit).filter(
|
||||||
@@ -397,7 +397,7 @@ async def get_available_units(
|
|||||||
"id": unit.id,
|
"id": unit.id,
|
||||||
"device_type": unit.device_type,
|
"device_type": unit.device_type,
|
||||||
"location": unit.address or unit.location,
|
"location": unit.address or unit.location,
|
||||||
"model": unit.slm_model if unit.device_type == "sound_level_meter" else unit.unit_type,
|
"model": unit.slm_model if unit.device_type == "slm" else unit.unit_type,
|
||||||
}
|
}
|
||||||
for unit in all_units
|
for unit in all_units
|
||||||
if unit.id not in assigned_unit_ids
|
if unit.id not in assigned_unit_ids
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ async def get_ftp_browser(
|
|||||||
location = db.query(MonitoringLocation).filter_by(id=assignment.location_id).first()
|
location = db.query(MonitoringLocation).filter_by(id=assignment.location_id).first()
|
||||||
|
|
||||||
# Only include SLM units
|
# Only include SLM units
|
||||||
if unit and unit.device_type == "sound_level_meter":
|
if unit and unit.device_type == "slm":
|
||||||
units_data.append({
|
units_data.append({
|
||||||
"assignment": assignment,
|
"assignment": assignment,
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ async def add_roster_unit(
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# If sound level meter, sync config to SLMM cache
|
# If sound level meter, sync config to SLMM cache
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
logger.info(f"Syncing SLM {id} config to SLMM cache...")
|
logger.info(f"Syncing SLM {id} config to SLMM cache...")
|
||||||
result = await sync_slm_to_slmm_cache(
|
result = await sync_slm_to_slmm_cache(
|
||||||
unit_id=id,
|
unit_id=id,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ async def rename_unit(
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# If sound level meter, sync updated config to SLMM cache
|
# If sound level meter, sync updated config to SLMM cache
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
logger.info(f"Syncing renamed SLM {new_id} (was {old_id}) config to SLMM cache...")
|
logger.info(f"Syncing renamed SLM {new_id} (was {old_id}) config to SLMM cache...")
|
||||||
result = await sync_slm_to_slmm_cache(
|
result = await sync_slm_to_slmm_cache(
|
||||||
unit_id=new_id,
|
unit_id=new_id,
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ async def create_scheduled_action(
|
|||||||
raise HTTPException(status_code=404, detail="Location not found")
|
raise HTTPException(status_code=404, detail="Location not found")
|
||||||
|
|
||||||
# Determine device type from location
|
# Determine device type from location
|
||||||
device_type = "sound_level_meter" if location.location_type == "sound" else "seismograph"
|
device_type = "slm" if location.location_type == "sound" else "seismograph"
|
||||||
|
|
||||||
# Get unit_id (optional - can be determined from assignment at execution time)
|
# Get unit_id (optional - can be determined from assignment at execution time)
|
||||||
unit_id = form_data.get("unit_id")
|
unit_id = form_data.get("unit_id")
|
||||||
@@ -188,7 +188,7 @@ async def schedule_recording_session(
|
|||||||
if not location:
|
if not location:
|
||||||
raise HTTPException(status_code=404, detail="Location not found")
|
raise HTTPException(status_code=404, detail="Location not found")
|
||||||
|
|
||||||
device_type = "sound_level_meter" if location.location_type == "sound" else "seismograph"
|
device_type = "slm" if location.location_type == "sound" else "seismograph"
|
||||||
unit_id = form_data.get("unit_id")
|
unit_id = form_data.get("unit_id")
|
||||||
|
|
||||||
start_time = datetime.fromisoformat(form_data.get("start_time"))
|
start_time = datetime.fromisoformat(form_data.get("start_time"))
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async def get_slm_stats(request: Request, db: Session = Depends(get_db)):
|
|||||||
Returns HTML partial with stat cards.
|
Returns HTML partial with stat cards.
|
||||||
"""
|
"""
|
||||||
# Query all SLMs
|
# Query all SLMs
|
||||||
all_slms = db.query(RosterUnit).filter_by(device_type="sound_level_meter").all()
|
all_slms = db.query(RosterUnit).filter_by(device_type="slm").all()
|
||||||
|
|
||||||
# Count deployed vs benched
|
# Count deployed vs benched
|
||||||
deployed_count = sum(1 for slm in all_slms if slm.deployed and not slm.retired)
|
deployed_count = sum(1 for slm in all_slms if slm.deployed and not slm.retired)
|
||||||
@@ -69,7 +69,7 @@ async def get_slm_units(
|
|||||||
Get list of SLM units for the sidebar.
|
Get list of SLM units for the sidebar.
|
||||||
Returns HTML partial with unit cards.
|
Returns HTML partial with unit cards.
|
||||||
"""
|
"""
|
||||||
query = db.query(RosterUnit).filter_by(device_type="sound_level_meter")
|
query = db.query(RosterUnit).filter_by(device_type="slm")
|
||||||
|
|
||||||
# Filter by project if provided
|
# Filter by project if provided
|
||||||
if project:
|
if project:
|
||||||
@@ -129,7 +129,7 @@ async def get_live_view(request: Request, unit_id: str, db: Session = Depends(ge
|
|||||||
Returns HTML partial with live metrics and chart.
|
Returns HTML partial with live metrics and chart.
|
||||||
"""
|
"""
|
||||||
# Get unit from database
|
# Get unit from database
|
||||||
unit = db.query(RosterUnit).filter_by(id=unit_id, device_type="sound_level_meter").first()
|
unit = db.query(RosterUnit).filter_by(id=unit_id, device_type="slm").first()
|
||||||
|
|
||||||
if not unit:
|
if not unit:
|
||||||
return templates.TemplateResponse("partials/slm_live_view_error.html", {
|
return templates.TemplateResponse("partials/slm_live_view_error.html", {
|
||||||
@@ -242,7 +242,7 @@ async def get_slm_config(request: Request, unit_id: str, db: Session = Depends(g
|
|||||||
Get configuration form for a specific SLM unit.
|
Get configuration form for a specific SLM unit.
|
||||||
Returns HTML partial with configuration form.
|
Returns HTML partial with configuration form.
|
||||||
"""
|
"""
|
||||||
unit = db.query(RosterUnit).filter_by(id=unit_id, device_type="sound_level_meter").first()
|
unit = db.query(RosterUnit).filter_by(id=unit_id, device_type="slm").first()
|
||||||
|
|
||||||
if not unit:
|
if not unit:
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
@@ -262,7 +262,7 @@ async def save_slm_config(request: Request, unit_id: str, db: Session = Depends(
|
|||||||
Save SLM configuration.
|
Save SLM configuration.
|
||||||
Updates unit parameters in the database.
|
Updates unit parameters in the database.
|
||||||
"""
|
"""
|
||||||
unit = db.query(RosterUnit).filter_by(id=unit_id, device_type="sound_level_meter").first()
|
unit = db.query(RosterUnit).filter_by(id=unit_id, device_type="slm").first()
|
||||||
|
|
||||||
if not unit:
|
if not unit:
|
||||||
return {"status": "error", "detail": f"Unit {unit_id} not found"}
|
return {"status": "error", "detail": f"Unit {unit_id} not found"}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ async def slm_detail_page(request: Request, unit_id: str, db: Session = Depends(
|
|||||||
|
|
||||||
# Get roster unit
|
# Get roster unit
|
||||||
unit = db.query(RosterUnit).filter_by(id=unit_id).first()
|
unit = db.query(RosterUnit).filter_by(id=unit_id).first()
|
||||||
if not unit or unit.device_type != "sound_level_meter":
|
if not unit or unit.device_type != "slm":
|
||||||
raise HTTPException(status_code=404, detail="Sound level meter not found")
|
raise HTTPException(status_code=404, detail="Sound level meter not found")
|
||||||
|
|
||||||
return templates.TemplateResponse("slm_detail.html", {
|
return templates.TemplateResponse("slm_detail.html", {
|
||||||
@@ -46,7 +46,7 @@ async def get_slm_summary(unit_id: str, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
# Get roster unit
|
# Get roster unit
|
||||||
unit = db.query(RosterUnit).filter_by(id=unit_id).first()
|
unit = db.query(RosterUnit).filter_by(id=unit_id).first()
|
||||||
if not unit or unit.device_type != "sound_level_meter":
|
if not unit or unit.device_type != "slm":
|
||||||
raise HTTPException(status_code=404, detail="Sound level meter not found")
|
raise HTTPException(status_code=404, detail="Sound level meter not found")
|
||||||
|
|
||||||
# Try to get live status from SLMM
|
# Try to get live status from SLMM
|
||||||
@@ -61,7 +61,7 @@ async def get_slm_summary(unit_id: str, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"unit_id": unit_id,
|
"unit_id": unit_id,
|
||||||
"device_type": "sound_level_meter",
|
"device_type": "slm",
|
||||||
"deployed": unit.deployed,
|
"deployed": unit.deployed,
|
||||||
"model": unit.slm_model or "NL-43",
|
"model": unit.slm_model or "NL-43",
|
||||||
"location": unit.address or unit.location,
|
"location": unit.address or unit.location,
|
||||||
@@ -89,7 +89,7 @@ async def slm_controls_partial(request: Request, unit_id: str, db: Session = Dep
|
|||||||
"""Render SLM control panel partial."""
|
"""Render SLM control panel partial."""
|
||||||
|
|
||||||
unit = db.query(RosterUnit).filter_by(id=unit_id).first()
|
unit = db.query(RosterUnit).filter_by(id=unit_id).first()
|
||||||
if not unit or unit.device_type != "sound_level_meter":
|
if not unit or unit.device_type != "slm":
|
||||||
raise HTTPException(status_code=404, detail="Sound level meter not found")
|
raise HTTPException(status_code=404, detail="Sound level meter not found")
|
||||||
|
|
||||||
# Get current status from SLMM
|
# Get current status from SLMM
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class DeviceController:
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
controller = DeviceController()
|
controller = DeviceController()
|
||||||
await controller.start_recording("nl43-001", "sound_level_meter", config={})
|
await controller.start_recording("nl43-001", "slm", config={})
|
||||||
await controller.stop_recording("seismo-042", "seismograph")
|
await controller.stop_recording("seismo-042", "seismograph")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
config: Device-specific recording configuration
|
config: Device-specific recording configuration
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -63,7 +63,7 @@ class DeviceController:
|
|||||||
UnsupportedDeviceTypeError: Device type not supported
|
UnsupportedDeviceTypeError: Device type not supported
|
||||||
DeviceControllerError: Operation failed
|
DeviceControllerError: Operation failed
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
return await self.slmm_client.start_recording(unit_id, config)
|
return await self.slmm_client.start_recording(unit_id, config)
|
||||||
except SLMMClientError as e:
|
except SLMMClientError as e:
|
||||||
@@ -81,7 +81,7 @@ class DeviceController:
|
|||||||
else:
|
else:
|
||||||
raise UnsupportedDeviceTypeError(
|
raise UnsupportedDeviceTypeError(
|
||||||
f"Device type '{device_type}' is not supported. "
|
f"Device type '{device_type}' is not supported. "
|
||||||
f"Supported types: sound_level_meter, seismograph"
|
f"Supported types: slm, seismograph"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def stop_recording(
|
async def stop_recording(
|
||||||
@@ -94,12 +94,12 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response dict from device module
|
Response dict from device module
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
return await self.slmm_client.stop_recording(unit_id)
|
return await self.slmm_client.stop_recording(unit_id)
|
||||||
except SLMMClientError as e:
|
except SLMMClientError as e:
|
||||||
@@ -126,12 +126,12 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response dict from device module
|
Response dict from device module
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
return await self.slmm_client.pause_recording(unit_id)
|
return await self.slmm_client.pause_recording(unit_id)
|
||||||
except SLMMClientError as e:
|
except SLMMClientError as e:
|
||||||
@@ -157,12 +157,12 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response dict from device module
|
Response dict from device module
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
return await self.slmm_client.resume_recording(unit_id)
|
return await self.slmm_client.resume_recording(unit_id)
|
||||||
except SLMMClientError as e:
|
except SLMMClientError as e:
|
||||||
@@ -192,12 +192,12 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Status dict from device module
|
Status dict from device module
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
return await self.slmm_client.get_unit_status(unit_id)
|
return await self.slmm_client.get_unit_status(unit_id)
|
||||||
except SLMMClientError as e:
|
except SLMMClientError as e:
|
||||||
@@ -224,12 +224,12 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Live data dict from device module
|
Live data dict from device module
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
return await self.slmm_client.get_live_data(unit_id)
|
return await self.slmm_client.get_live_data(unit_id)
|
||||||
except SLMMClientError as e:
|
except SLMMClientError as e:
|
||||||
@@ -261,14 +261,14 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
destination_path: Local path to save files
|
destination_path: Local path to save files
|
||||||
files: List of filenames, or None for all
|
files: List of filenames, or None for all
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Download result with file list
|
Download result with file list
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
return await self.slmm_client.download_files(
|
return await self.slmm_client.download_files(
|
||||||
unit_id,
|
unit_id,
|
||||||
@@ -304,13 +304,13 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
config: Configuration parameters
|
config: Configuration parameters
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Updated config from device module
|
Updated config from device module
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
return await self.slmm_client.update_unit_config(
|
return await self.slmm_client.update_unit_config(
|
||||||
unit_id,
|
unit_id,
|
||||||
@@ -347,12 +347,12 @@ class DeviceController:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_id: Unit identifier
|
unit_id: Unit identifier
|
||||||
device_type: "sound_level_meter" | "seismograph"
|
device_type: "slm" | "seismograph"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if device is reachable, False otherwise
|
True if device is reachable, False otherwise
|
||||||
"""
|
"""
|
||||||
if device_type == "sound_level_meter":
|
if device_type == "slm":
|
||||||
try:
|
try:
|
||||||
status = await self.slmm_client.get_unit_status(unit_id)
|
status = await self.slmm_client.get_unit_status(unit_id)
|
||||||
return status.get("last_seen") is not None
|
return status.get("last_seen") is not None
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class SchedulerService:
|
|||||||
project_id=action.project_id,
|
project_id=action.project_id,
|
||||||
location_id=action.location_id,
|
location_id=action.location_id,
|
||||||
unit_id=unit_id,
|
unit_id=unit_id,
|
||||||
session_type="sound" if action.device_type == "sound_level_meter" else "vibration",
|
session_type="sound" if action.device_type == "slm" else "vibration",
|
||||||
started_at=datetime.utcnow(),
|
started_at=datetime.utcnow(),
|
||||||
status="recording",
|
status="recording",
|
||||||
session_metadata=json.dumps({"scheduled_action_id": action.id}),
|
session_metadata=json.dumps({"scheduled_action_id": action.id}),
|
||||||
@@ -272,7 +272,7 @@ class SchedulerService:
|
|||||||
# Build destination path
|
# Build destination path
|
||||||
# Example: data/Projects/{project-id}/sound/{location-name}/session-{timestamp}/
|
# Example: data/Projects/{project-id}/sound/{location-name}/session-{timestamp}/
|
||||||
session_timestamp = datetime.utcnow().strftime("%Y-%m-%d-%H%M")
|
session_timestamp = datetime.utcnow().strftime("%Y-%m-%d-%H%M")
|
||||||
location_type_dir = "sound" if action.device_type == "sound_level_meter" else "vibration"
|
location_type_dir = "sound" if action.device_type == "slm" else "vibration"
|
||||||
|
|
||||||
destination_path = (
|
destination_path = (
|
||||||
f"data/Projects/{project.id}/{location_type_dir}/"
|
f"data/Projects/{project.id}/{location_type_dir}/"
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ seismos = db.query(RosterUnit).filter_by(
|
|||||||
### Sound Level Meters Query
|
### Sound Level Meters Query
|
||||||
```python
|
```python
|
||||||
slms = db.query(RosterUnit).filter_by(
|
slms = db.query(RosterUnit).filter_by(
|
||||||
device_type="sound_level_meter",
|
device_type="slm",
|
||||||
retired=False
|
retired=False
|
||||||
).all()
|
).all()
|
||||||
```
|
```
|
||||||
|
|||||||
288
docs/DEVICE_TYPE_SCHEMA.md
Normal file
288
docs/DEVICE_TYPE_SCHEMA.md
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
# Device Type Schema - Terra-View
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Terra-View uses a single roster table to manage three different device types. The `device_type` field is the primary discriminator that determines which fields are relevant for each unit.
|
||||||
|
|
||||||
|
## Official device_type Values
|
||||||
|
|
||||||
|
As of **Terra-View v0.4.3**, the following device_type values are standardized:
|
||||||
|
|
||||||
|
### 1. `"seismograph"` (Default)
|
||||||
|
**Purpose**: Seismic monitoring devices
|
||||||
|
|
||||||
|
**Applicable Fields**:
|
||||||
|
- Common: id, unit_type, deployed, retired, note, project_id, location, address, coordinates
|
||||||
|
- Specific: last_calibrated, next_calibration_due, deployed_with_modem_id
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- `BE1234` - Series 3 seismograph
|
||||||
|
- `UM12345` - Series 4 Micromate unit
|
||||||
|
- `SEISMO-001` - Custom seismograph
|
||||||
|
|
||||||
|
**Unit Type Values**:
|
||||||
|
- `series3` - Series 3 devices (default)
|
||||||
|
- `series4` - Series 4 devices
|
||||||
|
- `micromate` - Micromate devices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `"modem"`
|
||||||
|
**Purpose**: Field modems and network equipment
|
||||||
|
|
||||||
|
**Applicable Fields**:
|
||||||
|
- Common: id, unit_type, deployed, retired, note, project_id, location, address, coordinates
|
||||||
|
- Specific: ip_address, phone_number, hardware_model
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- `MDM001` - Field modem
|
||||||
|
- `MODEM-2025-01` - Network modem
|
||||||
|
- `RAVEN-XTV-01` - Specific modem model
|
||||||
|
|
||||||
|
**Unit Type Values**:
|
||||||
|
- `modem` - Generic modem
|
||||||
|
- `raven-xtv` - Raven XTV model
|
||||||
|
- Custom values for specific hardware
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `"slm"` ⭐
|
||||||
|
**Purpose**: Sound level meters (Rion NL-43/NL-53)
|
||||||
|
|
||||||
|
**Applicable Fields**:
|
||||||
|
- Common: id, unit_type, deployed, retired, note, project_id, location, address, coordinates
|
||||||
|
- Specific: slm_host, slm_tcp_port, slm_ftp_port, slm_model, slm_serial_number, slm_frequency_weighting, slm_time_weighting, slm_measurement_range, slm_last_check, deployed_with_modem_id
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- `SLM-43-01` - NL-43 sound level meter
|
||||||
|
- `NL43-001` - NL-43 unit
|
||||||
|
- `NL53-002` - NL-53 unit
|
||||||
|
|
||||||
|
**Unit Type Values**:
|
||||||
|
- `nl43` - Rion NL-43 model
|
||||||
|
- `nl53` - Rion NL-53 model
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration from Legacy Values
|
||||||
|
|
||||||
|
### Deprecated Values
|
||||||
|
|
||||||
|
The following device_type values have been **deprecated** and should be migrated:
|
||||||
|
|
||||||
|
- ❌ `"sound_level_meter"` → ✅ `"slm"`
|
||||||
|
|
||||||
|
### How to Migrate
|
||||||
|
|
||||||
|
Run the standardization migration script to update existing databases:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/serversdown/tmi/terra-view
|
||||||
|
python3 backend/migrate_standardize_device_types.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This script:
|
||||||
|
- Converts all `"sound_level_meter"` values to `"slm"`
|
||||||
|
- Is idempotent (safe to run multiple times)
|
||||||
|
- Shows before/after distribution of device types
|
||||||
|
- No data loss
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### RosterUnit Model (`backend/models.py`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RosterUnit(Base):
|
||||||
|
"""
|
||||||
|
Supports multiple device types:
|
||||||
|
- "seismograph" - Seismic monitoring devices (default)
|
||||||
|
- "modem" - Field modems and network equipment
|
||||||
|
- "slm" - Sound level meters (NL-43/NL-53)
|
||||||
|
"""
|
||||||
|
__tablename__ = "roster"
|
||||||
|
|
||||||
|
# Core fields (all device types)
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
unit_type = Column(String, default="series3")
|
||||||
|
device_type = Column(String, default="seismograph") # "seismograph" | "modem" | "slm"
|
||||||
|
deployed = Column(Boolean, default=True)
|
||||||
|
retired = Column(Boolean, default=False)
|
||||||
|
# ... other common fields
|
||||||
|
|
||||||
|
# Seismograph-specific
|
||||||
|
last_calibrated = Column(Date, nullable=True)
|
||||||
|
next_calibration_due = Column(Date, nullable=True)
|
||||||
|
|
||||||
|
# Modem-specific
|
||||||
|
ip_address = Column(String, nullable=True)
|
||||||
|
phone_number = Column(String, nullable=True)
|
||||||
|
hardware_model = Column(String, nullable=True)
|
||||||
|
|
||||||
|
# SLM-specific
|
||||||
|
slm_host = Column(String, nullable=True)
|
||||||
|
slm_tcp_port = Column(Integer, nullable=True)
|
||||||
|
slm_ftp_port = Column(Integer, nullable=True)
|
||||||
|
slm_model = Column(String, nullable=True)
|
||||||
|
slm_serial_number = Column(String, nullable=True)
|
||||||
|
slm_frequency_weighting = Column(String, nullable=True)
|
||||||
|
slm_time_weighting = Column(String, nullable=True)
|
||||||
|
slm_measurement_range = Column(String, nullable=True)
|
||||||
|
slm_last_check = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Shared fields (seismograph + SLM)
|
||||||
|
deployed_with_modem_id = Column(String, nullable=True) # FK to modem
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Usage
|
||||||
|
|
||||||
|
### Adding a New Unit
|
||||||
|
|
||||||
|
**Seismograph**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8001/api/roster/add \
|
||||||
|
-F "id=BE1234" \
|
||||||
|
-F "device_type=seismograph" \
|
||||||
|
-F "unit_type=series3" \
|
||||||
|
-F "deployed=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Modem**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8001/api/roster/add \
|
||||||
|
-F "id=MDM001" \
|
||||||
|
-F "device_type=modem" \
|
||||||
|
-F "ip_address=192.0.2.10" \
|
||||||
|
-F "phone_number=+1-555-0100"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sound Level Meter**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8001/api/roster/add \
|
||||||
|
-F "id=SLM-43-01" \
|
||||||
|
-F "device_type=slm" \
|
||||||
|
-F "slm_host=63.45.161.30" \
|
||||||
|
-F "slm_tcp_port=2255" \
|
||||||
|
-F "slm_model=NL-43"
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSV Import Format
|
||||||
|
|
||||||
|
```csv
|
||||||
|
unit_id,unit_type,device_type,deployed,slm_host,slm_tcp_port,slm_model
|
||||||
|
SLM-43-01,nl43,slm,true,63.45.161.30,2255,NL-43
|
||||||
|
SLM-43-02,nl43,slm,true,63.45.161.31,2255,NL-43
|
||||||
|
BE1234,series3,seismograph,true,,,
|
||||||
|
MDM001,modem,modem,true,,,
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend Behavior
|
||||||
|
|
||||||
|
### Device Type Selection
|
||||||
|
|
||||||
|
**Templates**: `unit_detail.html`, `roster.html`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<select name="device_type">
|
||||||
|
<option value="seismograph">Seismograph</option>
|
||||||
|
<option value="modem">Modem</option>
|
||||||
|
<option value="slm">Sound Level Meter</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional Field Display
|
||||||
|
|
||||||
|
JavaScript functions check `device_type` to show/hide relevant fields:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function toggleDetailFields() {
|
||||||
|
const deviceType = document.getElementById('device_type').value;
|
||||||
|
|
||||||
|
if (deviceType === 'seismograph') {
|
||||||
|
// Show calibration fields
|
||||||
|
} else if (deviceType === 'modem') {
|
||||||
|
// Show network fields
|
||||||
|
} else if (deviceType === 'slm') {
|
||||||
|
// Show SLM configuration fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
### Always Use Lowercase
|
||||||
|
|
||||||
|
✅ **Correct**:
|
||||||
|
```python
|
||||||
|
if unit.device_type == "slm":
|
||||||
|
# Handle sound level meter
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Incorrect**:
|
||||||
|
```python
|
||||||
|
if unit.device_type == "SLM": # Wrong - case sensitive
|
||||||
|
if unit.device_type == "sound_level_meter": # Deprecated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Patterns
|
||||||
|
|
||||||
|
**Filter by device type**:
|
||||||
|
```python
|
||||||
|
# Get all SLMs
|
||||||
|
slms = db.query(RosterUnit).filter_by(device_type="slm").all()
|
||||||
|
|
||||||
|
# Get deployed seismographs
|
||||||
|
seismos = db.query(RosterUnit).filter_by(
|
||||||
|
device_type="seismograph",
|
||||||
|
deployed=True
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Get all modems
|
||||||
|
modems = db.query(RosterUnit).filter_by(device_type="modem").all()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Verify Device Type Distribution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick check
|
||||||
|
sqlite3 data/seismo_fleet.db "SELECT device_type, COUNT(*) FROM roster GROUP BY device_type;"
|
||||||
|
|
||||||
|
# Detailed view
|
||||||
|
sqlite3 data/seismo_fleet.db "SELECT id, device_type, unit_type, deployed FROM roster ORDER BY device_type, id;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check for Legacy Values
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Should return 0 rows after migration
|
||||||
|
sqlite3 data/seismo_fleet.db "SELECT id FROM roster WHERE device_type = 'sound_level_meter';"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v0.4.3** (2026-01-16) - Standardized device_type values, deprecated `"sound_level_meter"` → `"slm"`
|
||||||
|
- **v0.4.0** (2026-01-05) - Added SLM support with `"sound_level_meter"` value
|
||||||
|
- **v0.2.0** (2025-12-03) - Added modem device type
|
||||||
|
- **v0.1.0** (2024-11-20) - Initial release with seismograph-only support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [README.md](../README.md) - Main project documentation with data model
|
||||||
|
- [DEVICE_TYPE_SLM_SUPPORT.md](DEVICE_TYPE_SLM_SUPPORT.md) - Legacy SLM implementation notes
|
||||||
|
- [SOUND_LEVEL_METERS_DASHBOARD.md](SOUND_LEVEL_METERS_DASHBOARD.md) - SLM dashboard features
|
||||||
|
- [SLM_CONFIGURATION.md](SLM_CONFIGURATION.md) - SLM device configuration guide
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Sound Level Meter Device Type Support
|
# Sound Level Meter Device Type Support
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT**: This documentation uses the legacy `sound_level_meter` device type value. As of v0.4.3, the standardized value is `"slm"`. Run `backend/migrate_standardize_device_types.py` to update your database.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Added full support for "Sound Level Meter" as a device type in the roster management system. Users can now create, edit, and manage SLM units through the Fleet Roster interface.
|
Added full support for "Sound Level Meter" as a device type in the roster management system. Users can now create, edit, and manage SLM units through the Fleet Roster interface.
|
||||||
@@ -95,7 +97,7 @@ All SLM fields are updated when editing existing unit.
|
|||||||
|
|
||||||
The database schema already included SLM fields (no changes needed):
|
The database schema already included SLM fields (no changes needed):
|
||||||
- All fields are nullable to support multiple device types
|
- All fields are nullable to support multiple device types
|
||||||
- Fields are only relevant when `device_type = "sound_level_meter"`
|
- Fields are only relevant when `device_type = "slm"`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -125,7 +127,7 @@ The form automatically shows/hides relevant fields based on device type:
|
|||||||
|
|
||||||
## Integration with SLMM Dashboard
|
## Integration with SLMM Dashboard
|
||||||
|
|
||||||
Units with `device_type = "sound_level_meter"` will:
|
Units with `device_type = "slm"` will:
|
||||||
- Appear in the Sound Level Meters dashboard (`/sound-level-meters`)
|
- Appear in the Sound Level Meters dashboard (`/sound-level-meters`)
|
||||||
- Be available for live monitoring and control
|
- Be available for live monitoring and control
|
||||||
- Use the configured `slm_host` and `slm_tcp_port` for device communication
|
- Use the configured `slm_host` and `slm_tcp_port` for device communication
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ slm.deployed_with_modem_id = "modem-001"
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "nl43-001",
|
"id": "nl43-001",
|
||||||
"device_type": "sound_level_meter",
|
"device_type": "slm",
|
||||||
"deployed_with_modem_id": "modem-001",
|
"deployed_with_modem_id": "modem-001",
|
||||||
"slm_tcp_port": 2255,
|
"slm_tcp_port": 2255,
|
||||||
"slm_model": "NL-43",
|
"slm_model": "NL-43",
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ The dashboard communicates with the SLMM backend service running on port 8100:
|
|||||||
SLM-specific fields in the RosterUnit model:
|
SLM-specific fields in the RosterUnit model:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
device_type = "sound_level_meter" # Distinguishes SLMs from seismographs
|
device_type = "slm" # Distinguishes SLMs from seismographs
|
||||||
slm_host = String # Device IP or hostname
|
slm_host = String # Device IP or hostname
|
||||||
slm_tcp_port = Integer # TCP control port (default 2255)
|
slm_tcp_port = Integer # TCP control port (default 2255)
|
||||||
slm_model = String # NL-43, NL-53, etc.
|
slm_model = String # NL-43, NL-53, etc.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async def sync_all_slms():
|
|||||||
try:
|
try:
|
||||||
# Get all SLM devices from Terra-View (source of truth)
|
# Get all SLM devices from Terra-View (source of truth)
|
||||||
slm_devices = db.query(RosterUnit).filter_by(
|
slm_devices = db.query(RosterUnit).filter_by(
|
||||||
device_type="sound_level_meter"
|
device_type="slm"
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
logger.info(f"Found {len(slm_devices)} SLM devices in Terra-View roster")
|
logger.info(f"Found {len(slm_devices)} SLM devices in Terra-View roster")
|
||||||
|
|||||||
@@ -85,6 +85,10 @@
|
|||||||
<span class="px-2 py-1 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 text-xs font-medium">
|
<span class="px-2 py-1 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 text-xs font-medium">
|
||||||
Modem
|
Modem
|
||||||
</span>
|
</span>
|
||||||
|
{% elif unit.device_type == 'slm' %}
|
||||||
|
<span class="px-2 py-1 rounded-full bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300 text-xs font-medium">
|
||||||
|
SLM
|
||||||
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="px-2 py-1 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 text-xs font-medium">
|
<span class="px-2 py-1 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 text-xs font-medium">
|
||||||
Seismograph
|
Seismograph
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<button class="filter-btn filter-device-type active-filter" data-value="all">All</button>
|
<button class="filter-btn filter-device-type active-filter" data-value="all">All</button>
|
||||||
<button class="filter-btn filter-device-type" data-value="seismograph">Seismographs</button>
|
<button class="filter-btn filter-device-type" data-value="seismograph">Seismographs</button>
|
||||||
<button class="filter-btn filter-device-type" data-value="modem">Modems</button>
|
<button class="filter-btn filter-device-type" data-value="modem">Modems</button>
|
||||||
<button class="filter-btn filter-device-type" data-value="sound_level_meter">SLMs</button>
|
<button class="filter-btn filter-device-type" data-value="slm">SLMs</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Filter -->
|
<!-- Status Filter -->
|
||||||
|
|||||||
Reference in New Issue
Block a user