Coverage for services/rf-acquisition/src/models/websdrs.py: 100%
65 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-25 16:18 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-25 16:18 +0000
1"""WebSDR configuration and data models."""
3from datetime import datetime
4from typing import Optional
5from pydantic import BaseModel, Field, HttpUrl
8class WebSDRConfig(BaseModel):
9 """Configuration for a single WebSDR receiver."""
11 id: int = Field(..., description="Unique identifier for the WebSDR")
12 name: str = Field(..., description="Friendly name (e.g., 'F5LEN Toulouse')")
13 url: HttpUrl = Field(..., description="Base URL of WebSDR (e.g., http://websdr.f5len.net:8901)")
14 location_name: str = Field(..., description="Location description")
15 latitude: float = Field(..., ge=-90, le=90, description="Latitude in decimal degrees")
16 longitude: float = Field(..., ge=-180, le=180, description="Longitude in decimal degrees")
17 is_active: bool = Field(default=True, description="Whether this receiver is currently active")
18 timeout_seconds: int = Field(default=30, description="Request timeout in seconds")
19 retry_count: int = Field(default=3, description="Number of retries on failure")
21 class Config:
22 json_schema_extra = {
23 "example": {
24 "id": 1,
25 "name": "F5LEN Toulouse",
26 "url": "http://websdr.f5len.net:8901",
27 "location_name": "Toulouse, France",
28 "latitude": 43.5,
29 "longitude": 1.4,
30 "is_active": True,
31 "timeout_seconds": 30,
32 "retry_count": 3
33 }
34 }
37class IQDataPoint(BaseModel):
38 """Single IQ sample."""
39 i: float = Field(..., description="In-phase component")
40 q: float = Field(..., description="Quadrature component")
43class AcquisitionRequest(BaseModel):
44 """Request to acquire IQ data from WebSDRs."""
46 frequency_mhz: float = Field(..., gt=0, description="Frequency in MHz")
47 duration_seconds: float = Field(..., gt=0, le=300, description="Duration in seconds (max 5 minutes)")
48 start_time: datetime = Field(default_factory=datetime.utcnow, description="Acquisition start time (UTC)")
49 websdrs: Optional[list[int]] = Field(default=None, description="Specific WebSDR IDs to use (None = all active)")
51 class Config:
52 json_schema_extra = {
53 "example": {
54 "frequency_mhz": 145.5,
55 "duration_seconds": 10,
56 "start_time": "2025-10-22T10:00:00Z",
57 "websdrs": None
58 }
59 }
62class SignalMetrics(BaseModel):
63 """Computed signal metrics for a measurement."""
65 snr_db: float = Field(..., description="Signal-to-Noise Ratio in dB")
66 psd_dbm: float = Field(..., description="Power Spectral Density in dBm/Hz")
67 frequency_offset_hz: float = Field(..., description="Frequency offset from target in Hz")
68 signal_power_dbm: float = Field(..., description="Signal power in dBm")
69 noise_power_dbm: float = Field(..., description="Noise power in dBm")
71 class Config:
72 json_schema_extra = {
73 "example": {
74 "snr_db": 15.5,
75 "psd_dbm": -80.2,
76 "frequency_offset_hz": 50.0,
77 "signal_power_dbm": -50.0,
78 "noise_power_dbm": -65.5
79 }
80 }
83class MeasurementRecord(BaseModel):
84 """Single measurement from one WebSDR receiver."""
86 websdrs_id: int = Field(..., description="Reference to WebSDR receiver")
87 frequency_mhz: float = Field(..., description="Target frequency in MHz")
88 sample_rate_khz: float = Field(default=12.5, description="Sample rate in kHz")
89 samples_count: int = Field(..., description="Total number of IQ samples")
90 timestamp_utc: datetime = Field(..., description="Timestamp of measurement (UTC)")
91 metrics: SignalMetrics = Field(..., description="Computed signal metrics")
92 iq_data_path: str = Field(..., description="Path to IQ data in MinIO (e.g., s3://bucket/key)")
93 metadata_json: Optional[dict] = Field(default=None, description="Additional metadata")
96class AcquisitionTaskResponse(BaseModel):
97 """Response when triggering an acquisition task."""
99 task_id: str = Field(..., description="Celery task ID for tracking")
100 status: str = Field(..., description="Initial task status")
101 message: str = Field(..., description="Human-readable message")
102 frequency_mhz: float = Field(..., description="Requested frequency")
103 websdrs_count: int = Field(..., description="Number of WebSDRs being used")
105 class Config:
106 json_schema_extra = {
107 "example": {
108 "task_id": "c2f8e4a0-9d5f-4c3b-a1e2-7f9c8b6d5a4e",
109 "status": "PENDING",
110 "message": "Acquisition task queued for 7 WebSDR receivers",
111 "frequency_mhz": 145.5,
112 "websdrs_count": 7
113 }
114 }
117class AcquisitionStatusResponse(BaseModel):
118 """Status of an ongoing acquisition task."""
120 task_id: str = Field(..., description="Celery task ID")
121 status: str = Field(..., description="Task status (PENDING, PROGRESS, SUCCESS, FAILURE, REVOKED)")
122 progress: float = Field(default=0.0, ge=0, le=100, description="Progress percentage")
123 message: str = Field(..., description="Status message")
124 measurements_collected: int = Field(default=0, description="Number of successful measurements")
125 errors: Optional[list[str]] = Field(default=None, description="Error messages if any")
126 result: Optional[dict] = Field(default=None, description="Result data when complete")
128 class Config:
129 json_schema_extra = {
130 "example": {
131 "task_id": "c2f8e4a0-9d5f-4c3b-a1e2-7f9c8b6d5a4e",
132 "status": "PROGRESS",
133 "progress": 57.14,
134 "message": "Fetching from 4/7 WebSDRs",
135 "measurements_collected": 4,
136 "errors": None,
137 "result": None
138 }
139 }
142class WebSDRFetcherConfig(BaseModel):
143 """Configuration for WebSDR fetcher behavior."""
145 timeout_seconds: int = Field(default=30, description="Individual request timeout")
146 retry_count: int = Field(default=3, description="Number of retries")
147 concurrent_requests: int = Field(default=7, description="Max concurrent requests")
148 backoff_factor: float = Field(default=2.0, description="Exponential backoff factor")