Files
terra-view/backend/services/device_controller.py
2026-01-12 18:07:26 +00:00

385 lines
12 KiB
Python

"""
Device Controller Service
Routes device operations to the appropriate backend module:
- SLMM for sound level meters
- SFM for seismographs (future implementation)
This abstraction allows Projects system to work with any device type
without knowing the underlying communication protocol.
"""
from typing import Dict, Any, Optional, List
from backend.services.slmm_client import get_slmm_client, SLMMClientError
class DeviceControllerError(Exception):
"""Base exception for device controller errors."""
pass
class UnsupportedDeviceTypeError(DeviceControllerError):
"""Raised when device type is not supported."""
pass
class DeviceController:
"""
Unified interface for controlling all device types.
Routes commands to appropriate backend module based on device_type.
Usage:
controller = DeviceController()
await controller.start_recording("nl43-001", "sound_level_meter", config={})
await controller.stop_recording("seismo-042", "seismograph")
"""
def __init__(self):
self.slmm_client = get_slmm_client()
# ========================================================================
# Recording Control
# ========================================================================
async def start_recording(
self,
unit_id: str,
device_type: str,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Start recording on a device.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "seismograph"
config: Device-specific recording configuration
Returns:
Response dict from device module
Raises:
UnsupportedDeviceTypeError: Device type not supported
DeviceControllerError: Operation failed
"""
if device_type == "sound_level_meter":
try:
return await self.slmm_client.start_recording(unit_id, config)
except SLMMClientError as e:
raise DeviceControllerError(f"SLMM error: {str(e)}")
elif device_type == "seismograph":
# TODO: Implement SFM client for seismograph control
# For now, return a placeholder response
return {
"status": "not_implemented",
"message": "Seismograph recording control not yet implemented",
"unit_id": unit_id,
}
else:
raise UnsupportedDeviceTypeError(
f"Device type '{device_type}' is not supported. "
f"Supported types: sound_level_meter, seismograph"
)
async def stop_recording(
self,
unit_id: str,
device_type: str,
) -> Dict[str, Any]:
"""
Stop recording on a device.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "seismograph"
Returns:
Response dict from device module
"""
if device_type == "sound_level_meter":
try:
return await self.slmm_client.stop_recording(unit_id)
except SLMMClientError as e:
raise DeviceControllerError(f"SLMM error: {str(e)}")
elif device_type == "seismograph":
# TODO: Implement SFM client
return {
"status": "not_implemented",
"message": "Seismograph recording control not yet implemented",
"unit_id": unit_id,
}
else:
raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}")
async def pause_recording(
self,
unit_id: str,
device_type: str,
) -> Dict[str, Any]:
"""
Pause recording on a device.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "seismograph"
Returns:
Response dict from device module
"""
if device_type == "sound_level_meter":
try:
return await self.slmm_client.pause_recording(unit_id)
except SLMMClientError as e:
raise DeviceControllerError(f"SLMM error: {str(e)}")
elif device_type == "seismograph":
return {
"status": "not_implemented",
"message": "Seismograph pause not yet implemented",
"unit_id": unit_id,
}
else:
raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}")
async def resume_recording(
self,
unit_id: str,
device_type: str,
) -> Dict[str, Any]:
"""
Resume paused recording on a device.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "seismograph"
Returns:
Response dict from device module
"""
if device_type == "sound_level_meter":
try:
return await self.slmm_client.resume_recording(unit_id)
except SLMMClientError as e:
raise DeviceControllerError(f"SLMM error: {str(e)}")
elif device_type == "seismograph":
return {
"status": "not_implemented",
"message": "Seismograph resume not yet implemented",
"unit_id": unit_id,
}
else:
raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}")
# ========================================================================
# Status & Monitoring
# ========================================================================
async def get_device_status(
self,
unit_id: str,
device_type: str,
) -> Dict[str, Any]:
"""
Get current device status.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "seismograph"
Returns:
Status dict from device module
"""
if device_type == "sound_level_meter":
try:
return await self.slmm_client.get_unit_status(unit_id)
except SLMMClientError as e:
raise DeviceControllerError(f"SLMM error: {str(e)}")
elif device_type == "seismograph":
# TODO: Implement SFM status check
return {
"status": "not_implemented",
"message": "Seismograph status not yet implemented",
"unit_id": unit_id,
}
else:
raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}")
async def get_live_data(
self,
unit_id: str,
device_type: str,
) -> Dict[str, Any]:
"""
Get live data from device.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "seismograph"
Returns:
Live data dict from device module
"""
if device_type == "sound_level_meter":
try:
return await self.slmm_client.get_live_data(unit_id)
except SLMMClientError as e:
raise DeviceControllerError(f"SLMM error: {str(e)}")
elif device_type == "seismograph":
return {
"status": "not_implemented",
"message": "Seismograph live data not yet implemented",
"unit_id": unit_id,
}
else:
raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}")
# ========================================================================
# Data Download
# ========================================================================
async def download_files(
self,
unit_id: str,
device_type: str,
destination_path: str,
files: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
Download data files from device.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "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":
try:
return await self.slmm_client.download_files(
unit_id,
destination_path,
files,
)
except SLMMClientError as e:
raise DeviceControllerError(f"SLMM error: {str(e)}")
elif device_type == "seismograph":
# TODO: Implement SFM file download
return {
"status": "not_implemented",
"message": "Seismograph file download not yet implemented",
"unit_id": unit_id,
}
else:
raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}")
# ========================================================================
# Device Configuration
# ========================================================================
async def update_device_config(
self,
unit_id: str,
device_type: str,
config: Dict[str, Any],
) -> Dict[str, Any]:
"""
Update device configuration.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "seismograph"
config: Configuration parameters
Returns:
Updated config from device module
"""
if device_type == "sound_level_meter":
try:
return await self.slmm_client.update_unit_config(
unit_id,
host=config.get("host"),
tcp_port=config.get("tcp_port"),
ftp_port=config.get("ftp_port"),
ftp_username=config.get("ftp_username"),
ftp_password=config.get("ftp_password"),
)
except SLMMClientError as e:
raise DeviceControllerError(f"SLMM error: {str(e)}")
elif device_type == "seismograph":
return {
"status": "not_implemented",
"message": "Seismograph config update not yet implemented",
"unit_id": unit_id,
}
else:
raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}")
# ========================================================================
# Health Check
# ========================================================================
async def check_device_connectivity(
self,
unit_id: str,
device_type: str,
) -> bool:
"""
Check if device is reachable.
Args:
unit_id: Unit identifier
device_type: "sound_level_meter" | "seismograph"
Returns:
True if device is reachable, False otherwise
"""
if device_type == "sound_level_meter":
try:
status = await self.slmm_client.get_unit_status(unit_id)
return status.get("last_seen") is not None
except:
return False
elif device_type == "seismograph":
# TODO: Implement SFM connectivity check
return False
else:
return False
# Singleton instance
_default_controller: Optional[DeviceController] = None
def get_device_controller() -> DeviceController:
"""
Get the default device controller instance.
Returns:
DeviceController instance
"""
global _default_controller
if _default_controller is None:
_default_controller = DeviceController()
return _default_controller