1+ package org.operatorfoundation.audiocoder
2+
3+ import kotlinx.coroutines.*
4+ import kotlinx.coroutines.flow.*
5+ import java.util.*
6+
7+ /* *
8+ * WSPR station provides complete amateur radio WSPR (Weak Signal Propagation Reporter) functionality.
9+ *
10+ * This class handles the complete WSPR workflow:
11+ * - Automatic timing synchronization with the global WSPR schedule
12+ * - Audio collection during transmission windows
13+ * - Signal decoding using the native WSPR decoder
14+ * - Result management and reporting
15+ *
16+ * The WSPR protocol operates on a strict 2-minute cycle:
17+ * - Even minutes (00, 02, 04...): Transmission/reception windows
18+ * - Odd minutes (01, 03, 05...): Silent periods for frequency coordination
19+ *
20+ * Usage:
21+ * ```kotlin
22+ * val audioSource = MyWSPRAudioSource()
23+ * val station = WSPRStation(audioSource)
24+ * station.start() // Begins automatic operation
25+ *
26+ * // Observe results
27+ * station.decodeResults.collect { results ->
28+ * // Handle decoded WSPR messages
29+ * }
30+ * ```
31+ *
32+ * @param audioSource Provider of audio data for WSPR processing
33+ * @param configuration Station operating parameters and preferences
34+ */
35+ class WSPRStation (
36+ private val audioSource : WSPRAudioSource ,
37+ private val configuration : WSPRStationConfiguration = WSPRStationConfiguration .createDefault()
38+ )
39+ {
40+ // ========== Core Components ==========
41+
42+ }
43+
44+ /* *
45+ * WSPR timing constants used throughout the station implementation.
46+ */
47+ object WSPRTimingConstants
48+ {
49+ /* * Standard WSPR cycle duration: 2 minutes */
50+ const val WSPR_CYCLE_DURATION_SECONDS = 120L
51+
52+ /* * WSPR transmission duration: approximately 110.g seconds */
53+ const val WSPR_TRANSMISSION_DURATION_SECONDS = 111L
54+
55+ /* * Native decoder audio collection requirement: exactly 114 seconds */
56+ const val AUDIO_COLLECTION_DURATION_SECONDS = 114L
57+ const val AUDIO_COLLECTION_DURATION_MILLISECONDS = AUDIO_COLLECTION_DURATION_SECONDS * 1000L
58+
59+ /* * Delay before starting decode after transmission begins: 2 seconds */
60+ const val DECODE_START_DELAY_SECONDS = 2L
61+
62+ /* * Duration of each audio chunk read during collection: 1 second */
63+ const val AUDIO_CHUNK_DURATION_MILLISECONDS = 1000L
64+
65+ /* * Pause between audio chunk reads: 100ms */
66+ const val AUDIO_COLLECTION_PAUSE_MILLISECONDS = 100L
67+
68+ /* * How often to update cycle information for UI: 1 second */
69+ const val CYCLE_INFORMATION_UPDATE_INTERVAL_MILLISECONDS = 1000L
70+
71+ /* * Brief pause between operations: 2 seconds */
72+ const val BRIEF_OPERATION_PAUSE_MILLISECONDS = 2000L
73+
74+ /* * Maximum delay for error backoff: 5 minutes */
75+ const val MAXIMUM_ERROR_BACKOFF_MILLISECONDS = 300_000L
76+ }
77+
78+ /* *
79+ * Configuration parameters for WSPR station operation.
80+ * Contains all user-configurable settings and operating parameters.
81+ */
82+ data class WSPRStationConfiguration (
83+ /* * WSPR operating frequency in MHz (e.g., 14.0956 for 20m band) */
84+ val operatingFrequencyMHz : Double ,
85+
86+ /* * Whether to user Lower Sideband mode (LSB) instead of Upper Sideband mode (USB) */
87+ val useLowerSidebandMode : Boolean ,
88+
89+ /* * Whether to use time-aligned decoding windows vs sliding windows */
90+ val useTimeAlignedDecoding : Boolean ,
91+
92+ /* * Station callsign for identification (optional) */
93+ val stationCallsign : String? ,
94+
95+ /* * Station Maidenhead grid square location (optional) */
96+ val stationGridSquare : String?
97+ )
98+ {
99+ companion object
100+ {
101+ /* *
102+ * Creates a default configuration suitable for most WSPR operations.
103+ * Uses 20m band (most popular), USB mode, and time-aligned decoding.
104+ */
105+ fun createDefault (): WSPRStationConfiguration
106+ {
107+ return WSPRStationConfiguration (
108+ operatingFrequencyMHz = WSPRBandplan .getDefaultFrequency(),
109+ useLowerSidebandMode = false ,
110+ useTimeAlignedDecoding = true ,
111+ stationCallsign = null ,
112+ stationGridSquare = null
113+ )
114+ }
115+
116+ /* *
117+ * Creates configuration for a specific WSPR band.
118+ *
119+ * @param bandName Band identifier (e.g., "20m", "40m", "80m")
120+ * @return Configuration for the specified band, or default if band not found
121+ */
122+ fun createForBand (bandName : String ): WSPRStationConfiguration
123+ {
124+ val band = WSPRBandplan .ALL_BANDS .find { it.name.equals(bandName, ignoreCase = true ) }
125+ ? : WSPRBandplan .ALL_BANDS .first { it.isPopular }
126+
127+ return WSPRStationConfiguration (
128+ operatingFrequencyMHz = band.dialFrequencyMHz,
129+ useLowerSidebandMode = false ,
130+ useTimeAlignedDecoding = true ,
131+ stationCallsign = null ,
132+ stationGridSquare = null
133+ )
134+ }
135+ }
136+ }
137+
138+ /* *
139+ * Represents the current state of WSPR station operation.
140+ * Provides detailed information about what the station is currently doing.
141+ */
142+ sealed class WSPRStationState
143+ {
144+ /* * Station is not running */
145+ object Stopped : WSPRStationState()
146+
147+ /* * Station is initializing and starting up */
148+ object Starting : WSPRStationState()
149+
150+ /* * Station is running and monitoring for decode opportunities */
151+ object Running : WSPRStationState()
152+
153+ /* * Station is shutting down */
154+ object Stopping : WSPRStationState()
155+
156+ /* *
157+ * Station is waiting for the next WSPR decode window to begin.
158+ * @param windowInfo Information about the upcoming decode window
159+ */
160+ data class WaitingForNextWindow (val windowInfo : WSPRDecodeWindowInformation ) : WSPRStationState()
161+
162+ /* * Station is preparing to collect audio (clearing buffers, etc.) */
163+ object PreparingForCollection : WSPRStationState()
164+
165+ /* * Station is actively collecting audio for decode */
166+ object CollectingAudio : WSPRStationState()
167+
168+ /* * Station is processing collected audio through the WSPR decoder */
169+ object ProcessingAudio : WSPRStationState()
170+
171+ /* *
172+ * Decode cycle completed successfully.
173+ * @param decodedSignalCount Number of WSPR signals found in this cycle
174+ */
175+ data class DecodeCompleted (val decodedSignalCount : Int ) : WSPRStationState()
176+
177+ /* *
178+ * Station encountered an error and requires attention.
179+ * @param errorDescription Human-readable description of the error
180+ */
181+ data class Error (val errorDescription : String ) : WSPRStationState()
182+ }
183+
184+ /* *
185+ * Custom exception for WSPR station-specific errors.
186+ * Provides structured error reporting for station operation failures.
187+ */
188+ class WSPRStationException (
189+ message : String ,
190+ cause : Throwable ? = null
191+ ) : Exception(message, cause)
0 commit comments