Compare commits
3 Commits
dc77a362ce
...
v0.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
| b15d434fce | |||
| 7b4e12c127 | |||
| 742a98a8ed |
@@ -332,12 +332,10 @@ class RecurringScheduleService:
|
|||||||
)
|
)
|
||||||
actions.append(start_action)
|
actions.append(start_action)
|
||||||
|
|
||||||
# Create STOP action (stop_cycle handles download when include_download is True)
|
# Create STOP action
|
||||||
stop_notes = json.dumps({
|
stop_notes = json.dumps({
|
||||||
"schedule_name": schedule.name,
|
"schedule_name": schedule.name,
|
||||||
"schedule_id": schedule.id,
|
"schedule_id": schedule.id,
|
||||||
"schedule_type": "weekly_calendar",
|
|
||||||
"include_download": schedule.include_download,
|
|
||||||
})
|
})
|
||||||
stop_action = ScheduledAction(
|
stop_action = ScheduledAction(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
@@ -352,6 +350,27 @@ class RecurringScheduleService:
|
|||||||
)
|
)
|
||||||
actions.append(stop_action)
|
actions.append(stop_action)
|
||||||
|
|
||||||
|
# Create DOWNLOAD action if enabled (1 minute after stop)
|
||||||
|
if schedule.include_download:
|
||||||
|
download_time = end_utc + timedelta(minutes=1)
|
||||||
|
download_notes = json.dumps({
|
||||||
|
"schedule_name": schedule.name,
|
||||||
|
"schedule_id": schedule.id,
|
||||||
|
"schedule_type": "weekly_calendar",
|
||||||
|
})
|
||||||
|
download_action = ScheduledAction(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
project_id=schedule.project_id,
|
||||||
|
location_id=schedule.location_id,
|
||||||
|
unit_id=unit_id,
|
||||||
|
action_type="download",
|
||||||
|
device_type=schedule.device_type,
|
||||||
|
scheduled_time=download_time,
|
||||||
|
execution_status="pending",
|
||||||
|
notes=download_notes,
|
||||||
|
)
|
||||||
|
actions.append(download_action)
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def _generate_interval_actions(
|
def _generate_interval_actions(
|
||||||
|
|||||||
@@ -295,20 +295,9 @@ class SchedulerService:
|
|||||||
stop_cycle handles:
|
stop_cycle handles:
|
||||||
1. Stop measurement
|
1. Stop measurement
|
||||||
2. Enable FTP
|
2. Enable FTP
|
||||||
3. Download measurement folder to SLMM local storage
|
3. Download measurement folder
|
||||||
|
4. Verify download
|
||||||
After stop_cycle, if download succeeded, this method fetches the ZIP
|
|
||||||
from SLMM and extracts it into Terra-View's project directory, creating
|
|
||||||
DataFile records for each file.
|
|
||||||
"""
|
"""
|
||||||
import hashlib
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import zipfile
|
|
||||||
import httpx
|
|
||||||
from pathlib import Path
|
|
||||||
from backend.models import DataFile
|
|
||||||
|
|
||||||
# Parse notes for download preference
|
# Parse notes for download preference
|
||||||
include_download = True
|
include_download = True
|
||||||
try:
|
try:
|
||||||
@@ -319,7 +308,7 @@ class SchedulerService:
|
|||||||
pass # Notes is plain text, not JSON
|
pass # Notes is plain text, not JSON
|
||||||
|
|
||||||
# Execute the full stop cycle via device controller
|
# Execute the full stop cycle via device controller
|
||||||
# SLMM handles stop, FTP enable, and download to SLMM-local storage
|
# SLMM handles stop, FTP enable, and download
|
||||||
cycle_response = await self.device_controller.stop_cycle(
|
cycle_response = await self.device_controller.stop_cycle(
|
||||||
unit_id,
|
unit_id,
|
||||||
action.device_type,
|
action.device_type,
|
||||||
@@ -351,81 +340,10 @@ class SchedulerService:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# If SLMM downloaded the folder successfully, fetch the ZIP from SLMM
|
|
||||||
# and extract it into Terra-View's project directory, creating DataFile records
|
|
||||||
files_created = 0
|
|
||||||
if include_download and cycle_response.get("download_success") and active_session:
|
|
||||||
folder_name = cycle_response.get("downloaded_folder") # e.g. "Auto_0058"
|
|
||||||
remote_path = f"/NL-43/{folder_name}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
SLMM_BASE_URL = os.getenv("SLMM_BASE_URL", "http://localhost:8100")
|
|
||||||
async with httpx.AsyncClient(timeout=600.0) as client:
|
|
||||||
zip_response = await client.post(
|
|
||||||
f"{SLMM_BASE_URL}/api/nl43/{unit_id}/ftp/download-folder",
|
|
||||||
json={"remote_path": remote_path}
|
|
||||||
)
|
|
||||||
|
|
||||||
if zip_response.is_success and len(zip_response.content) > 22:
|
|
||||||
base_dir = Path(f"data/Projects/{action.project_id}/{active_session.id}/{folder_name}")
|
|
||||||
base_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
file_type_map = {
|
|
||||||
'.wav': 'audio', '.mp3': 'audio',
|
|
||||||
'.csv': 'data', '.txt': 'data', '.json': 'data', '.dat': 'data',
|
|
||||||
'.rnd': 'data', '.rnh': 'data',
|
|
||||||
'.log': 'log',
|
|
||||||
'.zip': 'archive',
|
|
||||||
'.jpg': 'image', '.jpeg': 'image', '.png': 'image',
|
|
||||||
'.pdf': 'document',
|
|
||||||
}
|
|
||||||
|
|
||||||
with zipfile.ZipFile(io.BytesIO(zip_response.content)) as zf:
|
|
||||||
for zip_info in zf.filelist:
|
|
||||||
if zip_info.is_dir():
|
|
||||||
continue
|
|
||||||
file_data = zf.read(zip_info.filename)
|
|
||||||
file_path = base_dir / zip_info.filename
|
|
||||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with open(file_path, 'wb') as f:
|
|
||||||
f.write(file_data)
|
|
||||||
checksum = hashlib.sha256(file_data).hexdigest()
|
|
||||||
ext = os.path.splitext(zip_info.filename)[1].lower()
|
|
||||||
data_file = DataFile(
|
|
||||||
id=str(uuid.uuid4()),
|
|
||||||
session_id=active_session.id,
|
|
||||||
file_path=str(file_path.relative_to("data")),
|
|
||||||
file_type=file_type_map.get(ext, 'data'),
|
|
||||||
file_size_bytes=len(file_data),
|
|
||||||
downloaded_at=datetime.utcnow(),
|
|
||||||
checksum=checksum,
|
|
||||||
file_metadata=json.dumps({
|
|
||||||
"source": "stop_cycle",
|
|
||||||
"remote_path": remote_path,
|
|
||||||
"unit_id": unit_id,
|
|
||||||
"folder_name": folder_name,
|
|
||||||
"relative_path": zip_info.filename,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
db.add(data_file)
|
|
||||||
files_created += 1
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
logger.info(f"Created {files_created} DataFile records for session {active_session.id} from {folder_name}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"ZIP from SLMM for {folder_name} was empty or failed, skipping DataFile creation")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to extract ZIP and create DataFile records for {folder_name}: {e}")
|
|
||||||
# Don't fail the stop action — the device was stopped successfully
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "stopped",
|
"status": "stopped",
|
||||||
"session_id": active_session.id if active_session else None,
|
"session_id": active_session.id if active_session else None,
|
||||||
"cycle_response": cycle_response,
|
"cycle_response": cycle_response,
|
||||||
"files_created": files_created,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _execute_download(
|
async def _execute_download(
|
||||||
|
|||||||
@@ -659,7 +659,7 @@ class SLMMClient:
|
|||||||
|
|
||||||
# Format as Auto_XXXX folder name
|
# Format as Auto_XXXX folder name
|
||||||
folder_name = f"Auto_{index_number:04d}"
|
folder_name = f"Auto_{index_number:04d}"
|
||||||
remote_path = f"/NL-43/{folder_name}"
|
remote_path = f"/NL43_DATA/{folder_name}"
|
||||||
|
|
||||||
# Download the folder
|
# Download the folder
|
||||||
result = await self.download_folder(unit_id, remote_path)
|
result = await self.download_folder(unit_id, remote_path)
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ services:
|
|||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- PORT=8100
|
- PORT=8100
|
||||||
- CORS_ORIGINS=*
|
- CORS_ORIGINS=*
|
||||||
- TCP_IDLE_TTL=-1
|
|
||||||
- TCP_MAX_AGE=-1
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8100/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8100/health"]
|
||||||
|
|||||||
Reference in New Issue
Block a user