Skip to content

Commit a4d8fbc

Browse files
committed
Begin WSPRStation Implementation
1 parent aeff768 commit a4d8fbc

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.operatorfoundation.audiocoder
2+
3+
interface WSPRAudioSource {
4+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.operatorfoundation.audiocoder
2+
3+
data class WSPRDecodeResult(
4+
/**
5+
* Amateur radio callsign of the transmitting station.
6+
*
7+
* Examples: "W1AW", "JA1XYZ", "DL0ABC"
8+
* Standard amateur radio callsign format as assigned by national authorities.
9+
* May include portable/mobile indicators like "/P" or "/M".
10+
*/
11+
val callsign: String,
12+
13+
/**
14+
* Maidenhead grid square locator indicating station location.
15+
*
16+
* Examples: "FN31", "JO65", "PM96"
17+
* 4-character or 6-character Maidenhead locator system coordinate.
18+
* Provides approximate geographic location for propagation analysis.
19+
*/
20+
val gridSquare: String,
21+
22+
/**
23+
* Transmit power level in dBm (decibels relative to 1 milliwatt).
24+
*
25+
* Common values: 0, 3, 7, 10, 13, 17, 20, 23, 27, 30, 33, 37, 40, 43, 47, 50, 53, 57, 60
26+
* Higher values indicate more transmit power.
27+
* WSPR protocol encodes specific power levels, not arbitrary values.
28+
*/
29+
val powerLevelDbm: Int,
30+
31+
/**
32+
* Signal-to-noise ratio in dB at the receiving station.
33+
*
34+
* Typical range: -30 dB to +10 dB
35+
* Negative values are common and indicate weak signal conditions.
36+
* More negative values indicate weaker signals relative to noise floor.
37+
*/
38+
val signalToNoiseRatioDb: Float,
39+
40+
/**
41+
* Frequency offset from the expected signal frequency in Hz.
42+
*
43+
* Typical range: ±200 Hz from center frequency (1500 Hz)
44+
* Indicates transmitter frequency accuracy and drift.
45+
* Used for automatic frequency correction and propagation analysis.
46+
*/
47+
val frequencyOffsetHz: Double,
48+
49+
/**
50+
* Complete decoded message as received.
51+
*
52+
* Format: "CALLSIGN GRID POWER"
53+
* Example: "W1AW FN31 30"
54+
* Contains the raw decoded message for verification and logging.
55+
*/
56+
val completeMessage: String,
57+
58+
/**
59+
* Timestamp when this message was decoded (milliseconds since Unix epoch).
60+
*
61+
* Used for:
62+
* - Chronological sorting of decode results
63+
* - Time-based analysis of propagation
64+
* - Duplicate detection across decode cycles
65+
*/
66+
val decodeTimestamp: Long
67+
)
68+
{
69+
70+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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

Comments
 (0)