diff --git a/README.md b/README.md
index fd9dbd5..f404d5a 100644
--- a/README.md
+++ b/README.md
@@ -308,7 +308,7 @@ print(response.json())
|-------|------|-------------|
| id | string | Unit identifier (primary key) |
| 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 |
| retired | boolean | Removes the unit from deployments but preserves history |
| note | string | Notes about the unit |
@@ -334,6 +334,39 @@ print(response.json())
| phone_number | string | Cellular number for the modem |
| 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)
| Field | Type | Description |
diff --git a/backend/main.py b/backend/main.py
index a5d3803..e43ef10 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -111,26 +111,6 @@ async def startup_event():
await start_scheduler()
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")
def shutdown_event():
"""Clean up services on app shutdown"""
diff --git a/backend/migrate_add_slm_fields.py b/backend/migrate_add_slm_fields.py
index 1c1b50e..fc7995d 100644
--- a/backend/migrate_add_slm_fields.py
+++ b/backend/migrate_add_slm_fields.py
@@ -71,7 +71,7 @@ def migrate():
print("\n○ No migration needed - all columns already exist.")
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__":
diff --git a/backend/migrate_standardize_device_types.py b/backend/migrate_standardize_device_types.py
new file mode 100644
index 0000000..45b85ac
--- /dev/null
+++ b/backend/migrate_standardize_device_types.py
@@ -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()
diff --git a/backend/models.py b/backend/models.py
index 723c1dc..87bf3fe 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -19,14 +19,17 @@ class RosterUnit(Base):
Roster table: represents our *intended assignment* of a unit.
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"
# Core fields (all device types)
id = Column(String, primary_key=True, index=True)
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)
retired = Column(Boolean, default=False)
note = Column(String, nullable=True)
@@ -197,7 +200,7 @@ class UnitAssignment(Base):
notes = Column(Text, nullable=True)
# 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
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)
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)
executed_at = Column(DateTime, nullable=True)
diff --git a/backend/routers/project_locations.py b/backend/routers/project_locations.py
index 801e21a..40f3d5d 100644
--- a/backend/routers/project_locations.py
+++ b/backend/routers/project_locations.py
@@ -273,7 +273,7 @@ async def assign_unit_to_location(
raise HTTPException(status_code=404, detail="Unit not found")
# 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:
raise HTTPException(
status_code=400,
@@ -375,7 +375,7 @@ async def get_available_units(
Filters by device type matching the location 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
all_units = db.query(RosterUnit).filter(
@@ -397,7 +397,7 @@ async def get_available_units(
"id": unit.id,
"device_type": unit.device_type,
"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
if unit.id not in assigned_unit_ids
diff --git a/backend/routers/projects.py b/backend/routers/projects.py
index 0bc828a..af1aa6e 100644
--- a/backend/routers/projects.py
+++ b/backend/routers/projects.py
@@ -549,7 +549,7 @@ async def get_ftp_browser(
location = db.query(MonitoringLocation).filter_by(id=assignment.location_id).first()
# Only include SLM units
- if unit and unit.device_type == "sound_level_meter":
+ if unit and unit.device_type == "slm":
units_data.append({
"assignment": assignment,
"unit": unit,
diff --git a/backend/routers/roster_edit.py b/backend/routers/roster_edit.py
index 83488b3..e3752d5 100644
--- a/backend/routers/roster_edit.py
+++ b/backend/routers/roster_edit.py
@@ -223,7 +223,7 @@ async def add_roster_unit(
db.commit()
# 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...")
result = await sync_slm_to_slmm_cache(
unit_id=id,
diff --git a/backend/routers/roster_rename.py b/backend/routers/roster_rename.py
index bf9a14a..c99082d 100644
--- a/backend/routers/roster_rename.py
+++ b/backend/routers/roster_rename.py
@@ -106,7 +106,7 @@ async def rename_unit(
db.commit()
# 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...")
result = await sync_slm_to_slmm_cache(
unit_id=new_id,
diff --git a/backend/routers/scheduler.py b/backend/routers/scheduler.py
index caf64cf..40c9b26 100644
--- a/backend/routers/scheduler.py
+++ b/backend/routers/scheduler.py
@@ -131,7 +131,7 @@ async def create_scheduled_action(
raise HTTPException(status_code=404, detail="Location not found")
# 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)
unit_id = form_data.get("unit_id")
@@ -188,7 +188,7 @@ async def schedule_recording_session(
if not location:
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")
start_time = datetime.fromisoformat(form_data.get("start_time"))
diff --git a/backend/routers/slm_dashboard.py b/backend/routers/slm_dashboard.py
index 9b20456..767626d 100644
--- a/backend/routers/slm_dashboard.py
+++ b/backend/routers/slm_dashboard.py
@@ -35,7 +35,7 @@ async def get_slm_stats(request: Request, db: Session = Depends(get_db)):
Returns HTML partial with stat cards.
"""
# 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
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.
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
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.
"""
# 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:
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.
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:
return HTMLResponse(
@@ -262,7 +262,7 @@ async def save_slm_config(request: Request, unit_id: str, db: Session = Depends(
Save SLM configuration.
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:
return {"status": "error", "detail": f"Unit {unit_id} not found"}
diff --git a/backend/routers/slm_ui.py b/backend/routers/slm_ui.py
index d0945f6..6aed25b 100644
--- a/backend/routers/slm_ui.py
+++ b/backend/routers/slm_ui.py
@@ -30,7 +30,7 @@ async def slm_detail_page(request: Request, unit_id: str, db: Session = Depends(
# Get roster unit
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")
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
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")
# 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 {
"unit_id": unit_id,
- "device_type": "sound_level_meter",
+ "device_type": "slm",
"deployed": unit.deployed,
"model": unit.slm_model or "NL-43",
"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."""
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")
# Get current status from SLMM
diff --git a/backend/services/device_controller.py b/backend/services/device_controller.py
index a9aa80d..bb995e6 100644
--- a/backend/services/device_controller.py
+++ b/backend/services/device_controller.py
@@ -31,7 +31,7 @@ class DeviceController:
Usage:
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")
"""
@@ -53,7 +53,7 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
config: Device-specific recording configuration
Returns:
@@ -63,7 +63,7 @@ class DeviceController:
UnsupportedDeviceTypeError: Device type not supported
DeviceControllerError: Operation failed
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
return await self.slmm_client.start_recording(unit_id, config)
except SLMMClientError as e:
@@ -81,7 +81,7 @@ class DeviceController:
else:
raise UnsupportedDeviceTypeError(
f"Device type '{device_type}' is not supported. "
- f"Supported types: sound_level_meter, seismograph"
+ f"Supported types: slm, seismograph"
)
async def stop_recording(
@@ -94,12 +94,12 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
Returns:
Response dict from device module
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
return await self.slmm_client.stop_recording(unit_id)
except SLMMClientError as e:
@@ -126,12 +126,12 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
Returns:
Response dict from device module
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
return await self.slmm_client.pause_recording(unit_id)
except SLMMClientError as e:
@@ -157,12 +157,12 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
Returns:
Response dict from device module
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
return await self.slmm_client.resume_recording(unit_id)
except SLMMClientError as e:
@@ -192,12 +192,12 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
Returns:
Status dict from device module
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
return await self.slmm_client.get_unit_status(unit_id)
except SLMMClientError as e:
@@ -224,12 +224,12 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
Returns:
Live data dict from device module
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
return await self.slmm_client.get_live_data(unit_id)
except SLMMClientError as e:
@@ -261,14 +261,14 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
destination_path: Local path to save files
files: List of filenames, or None for all
Returns:
Download result with file list
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
return await self.slmm_client.download_files(
unit_id,
@@ -304,13 +304,13 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
config: Configuration parameters
Returns:
Updated config from device module
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
return await self.slmm_client.update_unit_config(
unit_id,
@@ -347,12 +347,12 @@ class DeviceController:
Args:
unit_id: Unit identifier
- device_type: "sound_level_meter" | "seismograph"
+ device_type: "slm" | "seismograph"
Returns:
True if device is reachable, False otherwise
"""
- if device_type == "sound_level_meter":
+ if device_type == "slm":
try:
status = await self.slmm_client.get_unit_status(unit_id)
return status.get("last_seen") is not None
diff --git a/backend/services/scheduler.py b/backend/services/scheduler.py
index 678f8ec..3bcde91 100644
--- a/backend/services/scheduler.py
+++ b/backend/services/scheduler.py
@@ -207,7 +207,7 @@ class SchedulerService:
project_id=action.project_id,
location_id=action.location_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(),
status="recording",
session_metadata=json.dumps({"scheduled_action_id": action.id}),
@@ -272,7 +272,7 @@ class SchedulerService:
# Build destination path
# Example: data/Projects/{project-id}/sound/{location-name}/session-{timestamp}/
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 = (
f"data/Projects/{project.id}/{location_type_dir}/"
diff --git a/docs/DEVICE_TYPE_DASHBOARDS.md b/docs/DEVICE_TYPE_DASHBOARDS.md
index e6c8913..b39878a 100644
--- a/docs/DEVICE_TYPE_DASHBOARDS.md
+++ b/docs/DEVICE_TYPE_DASHBOARDS.md
@@ -125,7 +125,7 @@ seismos = db.query(RosterUnit).filter_by(
### Sound Level Meters Query
```python
slms = db.query(RosterUnit).filter_by(
- device_type="sound_level_meter",
+ device_type="slm",
retired=False
).all()
```
diff --git a/docs/DEVICE_TYPE_SCHEMA.md b/docs/DEVICE_TYPE_SCHEMA.md
new file mode 100644
index 0000000..4624e5b
--- /dev/null
+++ b/docs/DEVICE_TYPE_SCHEMA.md
@@ -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
+
+```
+
+### 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
diff --git a/docs/DEVICE_TYPE_SLM_SUPPORT.md b/docs/DEVICE_TYPE_SLM_SUPPORT.md
index 1c0fd2d..ae452c6 100644
--- a/docs/DEVICE_TYPE_SLM_SUPPORT.md
+++ b/docs/DEVICE_TYPE_SLM_SUPPORT.md
@@ -1,5 +1,7 @@
# 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
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):
- 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
@@ -125,7 +127,7 @@ The form automatically shows/hides relevant fields based on device type:
## 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`)
- Be available for live monitoring and control
- Use the configured `slm_host` and `slm_tcp_port` for device communication
diff --git a/docs/MODEM_INTEGRATION.md b/docs/MODEM_INTEGRATION.md
index b0e5586..b27194e 100644
--- a/docs/MODEM_INTEGRATION.md
+++ b/docs/MODEM_INTEGRATION.md
@@ -300,7 +300,7 @@ slm.deployed_with_modem_id = "modem-001"
```json
{
"id": "nl43-001",
- "device_type": "sound_level_meter",
+ "device_type": "slm",
"deployed_with_modem_id": "modem-001",
"slm_tcp_port": 2255,
"slm_model": "NL-43",
diff --git a/docs/SOUND_LEVEL_METERS_DASHBOARD.md b/docs/SOUND_LEVEL_METERS_DASHBOARD.md
index 9b00f62..215b882 100644
--- a/docs/SOUND_LEVEL_METERS_DASHBOARD.md
+++ b/docs/SOUND_LEVEL_METERS_DASHBOARD.md
@@ -135,7 +135,7 @@ The dashboard communicates with the SLMM backend service running on port 8100:
SLM-specific fields in the RosterUnit model:
```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_tcp_port = Integer # TCP control port (default 2255)
slm_model = String # NL-43, NL-53, etc.
diff --git a/sync_slms_to_slmm.py b/sync_slms_to_slmm.py
index 9fe3451..c8039ee 100755
--- a/sync_slms_to_slmm.py
+++ b/sync_slms_to_slmm.py
@@ -25,7 +25,7 @@ async def sync_all_slms():
try:
# Get all SLM devices from Terra-View (source of truth)
slm_devices = db.query(RosterUnit).filter_by(
- device_type="sound_level_meter"
+ device_type="slm"
).all()
logger.info(f"Found {len(slm_devices)} SLM devices in Terra-View roster")
diff --git a/templates/partials/devices_table.html b/templates/partials/devices_table.html
index f9c9115..ed5aca6 100644
--- a/templates/partials/devices_table.html
+++ b/templates/partials/devices_table.html
@@ -85,6 +85,10 @@
Modem
+ {% elif unit.device_type == 'slm' %}
+
+ SLM
+
{% else %}
Seismograph
diff --git a/templates/roster.html b/templates/roster.html
index c596f55..f437570 100644
--- a/templates/roster.html
+++ b/templates/roster.html
@@ -57,7 +57,7 @@
-
+