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

1"""WebSDR configuration and data models.""" 

2 

3from datetime import datetime 

4from typing import Optional 

5from pydantic import BaseModel, Field, HttpUrl 

6 

7 

8class WebSDRConfig(BaseModel): 

9 """Configuration for a single WebSDR receiver.""" 

10 

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") 

20 

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 } 

35 

36 

37class IQDataPoint(BaseModel): 

38 """Single IQ sample.""" 

39 i: float = Field(..., description="In-phase component") 

40 q: float = Field(..., description="Quadrature component") 

41 

42 

43class AcquisitionRequest(BaseModel): 

44 """Request to acquire IQ data from WebSDRs.""" 

45 

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)") 

50 

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 } 

60 

61 

62class SignalMetrics(BaseModel): 

63 """Computed signal metrics for a measurement.""" 

64 

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") 

70 

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 } 

81 

82 

83class MeasurementRecord(BaseModel): 

84 """Single measurement from one WebSDR receiver.""" 

85 

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") 

94 

95 

96class AcquisitionTaskResponse(BaseModel): 

97 """Response when triggering an acquisition task.""" 

98 

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") 

104 

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 } 

115 

116 

117class AcquisitionStatusResponse(BaseModel): 

118 """Status of an ongoing acquisition task.""" 

119 

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") 

127 

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 } 

140 

141 

142class WebSDRFetcherConfig(BaseModel): 

143 """Configuration for WebSDR fetcher behavior.""" 

144 

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")