feat: Add comprehensive NL-43/NL-53 Communication Guide and command references

- Introduced a new communication guide detailing protocol basics, transport modes, and a quick startup checklist.
- Added a detailed list of commands with their functions and usage for NL-43/NL-53 devices.
- Created a verified quick reference for command formats to prevent common mistakes.
- Implemented an improvements document outlining critical fixes, security enhancements, reliability upgrades, and code quality improvements for the SLMM project.
- Enhanced the frontend with a new button to retrieve all device settings, along with corresponding JavaScript functionality.
- Added a test script for the new settings retrieval API endpoint to demonstrate its usage and validate functionality.
This commit is contained in:
serversdwn
2025-12-25 00:36:46 +00:00
parent c90544a712
commit f9139d6aa3
17 changed files with 1395 additions and 13 deletions

View File

@@ -725,6 +725,42 @@ async def get_ftp_status(unit_id: str, db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail=f"Failed to get FTP status: {str(e)}")
@router.get("/{unit_id}/settings")
async def get_all_settings(unit_id: str, db: Session = Depends(get_db)):
"""Get all current device settings for verification.
Returns a comprehensive view of all device configuration including:
- Measurement state and weightings
- Timing and interval settings
- Battery level and clock
- Sleep and FTP status
This is useful for verifying device configuration before starting measurements.
"""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
settings = await client.get_all_settings()
logger.info(f"Retrieved all settings for unit {unit_id}")
return {"status": "ok", "unit_id": unit_id, "settings": settings}
except ConnectionError as e:
logger.error(f"Failed to get settings for {unit_id}: {e}")
raise HTTPException(status_code=502, detail="Failed to communicate with device")
except TimeoutError:
logger.error(f"Timeout getting settings for {unit_id}")
raise HTTPException(status_code=504, detail="Device communication timeout")
except Exception as e:
logger.error(f"Unexpected error getting settings for {unit_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{unit_id}/ftp/files")
async def list_ftp_files(unit_id: str, path: str = "/", db: Session = Depends(get_db)):
"""List files on the device via FTP.
@@ -748,6 +784,14 @@ async def list_ftp_files(unit_id: str, path: str = "/", db: Session = Depends(ge
raise HTTPException(status_code=500, detail="Internal server error")
class TimingPayload(BaseModel):
preset: str
class IndexPayload(BaseModel):
index: int
class DownloadRequest(BaseModel):
remote_path: str
@@ -790,3 +834,178 @@ async def download_ftp_file(unit_id: str, payload: DownloadRequest, db: Session
except Exception as e:
logger.error(f"Unexpected error downloading file from {unit_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
# Timing/Interval Configuration Endpoints
@router.get("/{unit_id}/measurement-time")
async def get_measurement_time(unit_id: str, db: Session = Depends(get_db)):
"""Get current measurement time preset."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
preset = await client.get_measurement_time()
return {"status": "ok", "measurement_time": preset}
except Exception as e:
logger.error(f"Failed to get measurement time for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/measurement-time")
async def set_measurement_time(unit_id: str, payload: TimingPayload, db: Session = Depends(get_db)):
"""Set measurement time preset (10s, 1m, 5m, 10m, 15m, 30m, 1h, 8h, 24h, or custom like 00:05:30)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_measurement_time(payload.preset)
return {"status": "ok", "message": f"Measurement time set to {payload.preset}"}
except Exception as e:
logger.error(f"Failed to set measurement time for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/leq-interval")
async def get_leq_interval(unit_id: str, db: Session = Depends(get_db)):
"""Get current Leq calculation interval preset."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
preset = await client.get_leq_interval()
return {"status": "ok", "leq_interval": preset}
except Exception as e:
logger.error(f"Failed to get Leq interval for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/leq-interval")
async def set_leq_interval(unit_id: str, payload: TimingPayload, db: Session = Depends(get_db)):
"""Set Leq calculation interval preset (Off, 10s, 1m, 5m, 10m, 15m, 30m, 1h, 8h, 24h, or custom like 00:05:30)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_leq_interval(payload.preset)
return {"status": "ok", "message": f"Leq interval set to {payload.preset}"}
except Exception as e:
logger.error(f"Failed to set Leq interval for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/lp-interval")
async def get_lp_interval(unit_id: str, db: Session = Depends(get_db)):
"""Get current Lp store interval."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
preset = await client.get_lp_interval()
return {"status": "ok", "lp_interval": preset}
except Exception as e:
logger.error(f"Failed to get Lp interval for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/lp-interval")
async def set_lp_interval(unit_id: str, payload: TimingPayload, db: Session = Depends(get_db)):
"""Set Lp store interval (Off, 10ms, 25ms, 100ms, 200ms, 1s)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_lp_interval(payload.preset)
return {"status": "ok", "message": f"Lp interval set to {payload.preset}"}
except Exception as e:
logger.error(f"Failed to set Lp interval for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/index-number")
async def get_index_number(unit_id: str, db: Session = Depends(get_db)):
"""Get current index number for file numbering."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
index = await client.get_index_number()
return {"status": "ok", "index_number": index}
except Exception as e:
logger.error(f"Failed to get index number for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.put("/{unit_id}/index-number")
async def set_index_number(unit_id: str, payload: IndexPayload, db: Session = Depends(get_db)):
"""Set index number for file numbering (0000-9999)."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
await client.set_index_number(payload.index)
return {"status": "ok", "message": f"Index number set to {payload.index:04d}"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Failed to set index number for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))
@router.get("/{unit_id}/settings/all")
async def get_all_settings(unit_id: str, db: Session = Depends(get_db)):
"""Get all device settings for verification."""
cfg = db.query(NL43Config).filter_by(unit_id=unit_id).first()
if not cfg:
raise HTTPException(status_code=404, detail="NL43 config not found")
if not cfg.tcp_enabled:
raise HTTPException(status_code=403, detail="TCP communication is disabled for this device")
client = NL43Client(cfg.host, cfg.tcp_port, ftp_username=cfg.ftp_username, ftp_password=cfg.ftp_password)
try:
settings = await client.get_all_settings()
return {"status": "ok", "settings": settings}
except Exception as e:
logger.error(f"Failed to get all settings for {unit_id}: {e}")
raise HTTPException(status_code=502, detail=str(e))

View File

@@ -465,6 +465,146 @@ class NL43Client:
logger.info(f"DRD stream ended for {self.device_key}")
async def set_measurement_time(self, preset: str):
"""Set measurement time preset.
Args:
preset: Time preset (10s, 1m, 5m, 10m, 15m, 30m, 1h, 8h, 24h, or custom like "00:05:30")
"""
await self._send_command(f"Measurement Time Preset Manual,{preset}\r\n")
logger.info(f"Set measurement time to {preset} on {self.device_key}")
async def get_measurement_time(self) -> str:
"""Get current measurement time preset.
Returns: Current time preset setting
"""
resp = await self._send_command("Measurement Time Preset Manual?\r\n")
return resp.strip()
async def set_leq_interval(self, preset: str):
"""Set Leq calculation interval preset.
Args:
preset: Interval preset (Off, 10s, 1m, 5m, 10m, 15m, 30m, 1h, 8h, 24h, or custom like "00:05:30")
"""
await self._send_command(f"Leq Calculation Interval Preset,{preset}\r\n")
logger.info(f"Set Leq interval to {preset} on {self.device_key}")
async def get_leq_interval(self) -> str:
"""Get current Leq calculation interval preset.
Returns: Current interval preset setting
"""
resp = await self._send_command("Leq Calculation Interval Preset?\r\n")
return resp.strip()
async def set_lp_interval(self, preset: str):
"""Set Lp store interval.
Args:
preset: Store interval (Off, 10ms, 25ms, 100ms, 200ms, 1s)
"""
await self._send_command(f"Lp Store Interval,{preset}\r\n")
logger.info(f"Set Lp interval to {preset} on {self.device_key}")
async def get_lp_interval(self) -> str:
"""Get current Lp store interval.
Returns: Current store interval setting
"""
resp = await self._send_command("Lp Store Interval?\r\n")
return resp.strip()
async def set_index_number(self, index: int):
"""Set index number for file numbering.
Args:
index: Index number (0000-9999)
"""
if not 0 <= index <= 9999:
raise ValueError("Index must be between 0000 and 9999")
await self._send_command(f"Index Number,{index:04d}\r\n")
logger.info(f"Set index number to {index:04d} on {self.device_key}")
async def get_index_number(self) -> str:
"""Get current index number.
Returns: Current index number
"""
resp = await self._send_command("Index Number?\r\n")
return resp.strip()
async def get_all_settings(self) -> dict:
"""Query all device settings for verification.
Returns: Dictionary with all current device settings
"""
settings = {}
# Measurement settings
try:
settings["measurement_state"] = await self.get_measurement_state()
except Exception as e:
settings["measurement_state"] = f"Error: {e}"
try:
settings["frequency_weighting"] = await self.get_frequency_weighting()
except Exception as e:
settings["frequency_weighting"] = f"Error: {e}"
try:
settings["time_weighting"] = await self.get_time_weighting()
except Exception as e:
settings["time_weighting"] = f"Error: {e}"
# Timing/interval settings
try:
settings["measurement_time"] = await self.get_measurement_time()
except Exception as e:
settings["measurement_time"] = f"Error: {e}"
try:
settings["leq_interval"] = await self.get_leq_interval()
except Exception as e:
settings["leq_interval"] = f"Error: {e}"
try:
settings["lp_interval"] = await self.get_lp_interval()
except Exception as e:
settings["lp_interval"] = f"Error: {e}"
try:
settings["index_number"] = await self.get_index_number()
except Exception as e:
settings["index_number"] = f"Error: {e}"
# Device info
try:
settings["battery_level"] = await self.get_battery_level()
except Exception as e:
settings["battery_level"] = f"Error: {e}"
try:
settings["clock"] = await self.get_clock()
except Exception as e:
settings["clock"] = f"Error: {e}"
# Sleep mode
try:
settings["sleep_mode"] = await self.get_sleep_status()
except Exception as e:
settings["sleep_mode"] = f"Error: {e}"
# FTP status
try:
settings["ftp_status"] = await self.get_ftp_status()
except Exception as e:
settings["ftp_status"] = f"Error: {e}"
logger.info(f"Retrieved all settings for {self.device_key}")
return settings
async def enable_ftp(self):
"""Enable FTP server on the device.