Skip to content

Commit 32e6053

Browse files
committed
Update WSPRDecodeResult.kt
1 parent a4d8fbc commit 32e6053

File tree

1 file changed

+195
-2
lines changed

1 file changed

+195
-2
lines changed

AudioCoder/src/main/java/org/operatorfoundation/audiocoder/WSPRDecodeResult.kt

Lines changed: 195 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,40 @@
11
package org.operatorfoundation.audiocoder
22

3+
import android.icu.util.Calendar
4+
import java.lang.Math.pow
5+
6+
/**
7+
* Represents a successfully decoded WSPR message with all associated metadata.
8+
*
9+
* WSPR (Weak Signal Propagation Reporter) messages contain standardized information
10+
* about the transmitting station, propagation conditions, and signal characteristics.
11+
*
12+
* A typical WSPR message contains:
13+
* - Station identification (callsign)
14+
* - Location information (Maidenhead grid square)
15+
* - Transmit power level (in dBm)
16+
* - Reception quality metrics (SNR, frequency offset)
17+
* - Decode timing information
18+
*
19+
* Example usage:
20+
* val result = WSPRDecodeResult(
21+
* callsign = "W1AW",
22+
* gridSquare = "FN31",
23+
* powerLevelDbm = 30,
24+
* signalToNoiseRatioDb = -15.2f,
25+
* frequencyOffsetHz = 1456.3,
26+
* completeMessage = "W1AW FN31 30",
27+
* decodeTimestamp = System.currentTimeMillis()
28+
* )
29+
*
30+
* println("Station ${result.callsign} in ${result.gridSquare}")
31+
* println("Signal quality: ${result.signalQualityDescription}")
32+
*/
333
data class WSPRDecodeResult(
434
/**
535
* Amateur radio callsign of the transmitting station.
636
*
7-
* Examples: "W1AW", "JA1XYZ", "DL0ABC"
37+
* Examples: "Q0QQQ"
838
* Standard amateur radio callsign format as assigned by national authorities.
939
* May include portable/mobile indicators like "/P" or "/M".
1040
*/
@@ -50,7 +80,7 @@ data class WSPRDecodeResult(
5080
* Complete decoded message as received.
5181
*
5282
* Format: "CALLSIGN GRID POWER"
53-
* Example: "W1AW FN31 30"
83+
* Example: "Q0QQQ FN31 30"
5484
* Contains the raw decoded message for verification and logging.
5585
*/
5686
val completeMessage: String,
@@ -66,5 +96,168 @@ data class WSPRDecodeResult(
6696
val decodeTimestamp: Long
6797
)
6898
{
99+
/**
100+
* Human-readable description of signal quality based on SNR.
101+
*/
102+
val signalQualityDescription: String
103+
get() = when {
104+
signalToNoiseRatioDb >= 0 -> "Excellent (${signalToNoiseRatioDb.format(1)} dB)"
105+
signalToNoiseRatioDb >= -10 -> "Good (${signalToNoiseRatioDb.format(1)} dB)"
106+
signalToNoiseRatioDb >= -20 -> "Fair (${signalToNoiseRatioDb.format(1)} dB)"
107+
signalToNoiseRatioDb >= -30 -> "Weak (${signalToNoiseRatioDb.format(1)} dB)"
108+
else -> "Very weak (${signalToNoiseRatioDb.format(1)} dB)"
109+
}
110+
111+
/**
112+
* Transmit power converted to watts.
113+
* Calculated from dBm using standard conversion formula: P(W) = 10^((P(dBm) - 30) / 10)
114+
*/
115+
val transmitPowerWatts: Double
116+
get() = pow(10.0, (powerLevelDbm - 30.0) / 10.0)
117+
118+
/**
119+
* Human-readable power level description.
120+
*/
121+
val powerLevelDescription: String
122+
get() = when {
123+
transmitPowerWatts < 0.001 -> "QRP (${transmitPowerWatts.format(3)} W)"
124+
transmitPowerWatts < 0.01 -> "Low power (${transmitPowerWatts.format(3)} W)"
125+
transmitPowerWatts < 0.1 -> "Medium power (${transmitPowerWatts.format(2)} W)"
126+
transmitPowerWatts < 1.0 -> "High power (${transmitPowerWatts.format(1)} W)"
127+
else -> "Very high power (${transmitPowerWatts.format(0)} W)"
128+
}
129+
130+
/**
131+
* Formatted frequency display showing offset from WSPR center frequency.
132+
* Displays the actual received frequency for technical analysis.
133+
*/
134+
val displayFrequency: String
135+
get() {
136+
val centerFrequencyHz = 1500.0 // WSPR center frequency
137+
val actualFrequencyHz = centerFrequencyHz + frequencyOffsetHz
138+
return "${actualFrequencyHz.format(1)} Hz (${frequencyOffsetHz.formatOffset()} Hz)"
139+
}
140+
141+
/**
142+
* Formatted timestamp for display purposes.
143+
* Shows decode time in local timezone with standard format.
144+
*/
145+
val formattedDecodeTime: String
146+
get() {
147+
val calendar = Calendar.getInstance()
148+
calendar.timeInMillis = decodeTimestamp
149+
return String.format(
150+
"%04d-%02d-%02d %02d:%02d:%02d",
151+
calendar.get(Calendar.YEAR),
152+
calendar.get(Calendar.MONTH) + 1,
153+
calendar.get(Calendar.DAY_OF_MONTH),
154+
calendar.get(Calendar.HOUR_OF_DAY),
155+
calendar.get(Calendar.MINUTE),
156+
calendar.get(Calendar.SECOND)
157+
)
158+
}
159+
160+
/**
161+
* Creates a summary string for logging or display.
162+
*/
163+
fun createSummaryLine(): String {
164+
return "$callsign ($gridSquare) ${powerLevelDbm}dBm SNR:${signalToNoiseRatioDb.format(1)}dB"
165+
}
166+
167+
/**
168+
* Creates detailed information for technical analysis.
169+
* Includes all available data for comprehensive record keeping.
170+
*/
171+
fun createDetailedReport(): String {
172+
return buildString {
173+
appendLine("WSPR Decode Result")
174+
appendLine("================")
175+
appendLine("Callsign: $callsign")
176+
appendLine("Grid Square: $gridSquare")
177+
appendLine("Power: $powerLevelDbm dBm ($powerLevelDescription)")
178+
appendLine("Signal Quality: $signalQualityDescription")
179+
appendLine("Frequency: $displayFrequency")
180+
appendLine("Complete Message: '$completeMessage'")
181+
appendLine("Decoded: $formattedDecodeTime")
182+
}
183+
}
184+
185+
/**
186+
* Checks if this decode result represents the same transmission as another.
187+
* Useful for duplicate detection across multiple decode attempts.
188+
*
189+
* @param other Another decode result to compare
190+
* @param timeToleranceMs Acceptable time difference for considering results as duplicates
191+
* @return true if the results likely represent the same transmission
192+
*/
193+
fun isSameTransmissionAs(other: WSPRDecodeResult, timeToleranceMs: Long = 5000L): Boolean
194+
{
195+
val timeDifference = kotlin.math.abs(decodeTimestamp - other.decodeTimestamp)
196+
val frequencyDifference = kotlin.math.abs(frequencyOffsetHz - other.frequencyOffsetHz)
197+
198+
return callsign == other.callsign &&
199+
gridSquare == other.gridSquare &&
200+
powerLevelDbm == other.powerLevelDbm &&
201+
timeDifference <= timeToleranceMs &&
202+
frequencyDifference < 5.0 // Within 5 Hz frequency tolerance
203+
}
69204

205+
companion object
206+
{
207+
/** Placeholder for unknown or invalid callsigns */
208+
const val UNKNOWN_CALLSIGN = "UNKNOWN"
209+
210+
/** Placeholder for unknown or invalid grid squares */
211+
const val UNKNOWN_GRID_SQUARE = "????"
212+
213+
/** Placeholder for empty or corrupted messages */
214+
const val EMPTY_MESSAGE = ""
215+
216+
/**
217+
* Creates a decode result representing a failed or corrupted decode.
218+
* Used when the decoder detects a signal but cannot extract valid information.
219+
*
220+
* @param partialMessage Any partial message content that was recovered
221+
* @param snr Signal-to-noise ratio if available
222+
* @param frequency Frequency offset if available
223+
* @return Decode result marked as invalid/incomplete
224+
*/
225+
fun createCorruptedDecode(
226+
partialMessage: String = EMPTY_MESSAGE,
227+
snr: Float = Float.NaN,
228+
frequency: Double = Double.NaN
229+
): WSPRDecodeResult
230+
{
231+
return WSPRDecodeResult(
232+
callsign = UNKNOWN_CALLSIGN,
233+
gridSquare = UNKNOWN_GRID_SQUARE,
234+
powerLevelDbm = 0,
235+
signalToNoiseRatioDb = snr,
236+
frequencyOffsetHz = frequency,
237+
completeMessage = partialMessage,
238+
decodeTimestamp = System.currentTimeMillis()
239+
)
240+
}
241+
}
70242
}
243+
244+
// ========== Extension Functions for Formatting ==========
245+
246+
/**
247+
* Formats a floating-point number to the specified number of decimal places.
248+
*/
249+
private fun Float.format(decimals: Int): String = "%.${decimals}f".format(this)
250+
251+
/**
252+
* Formats a double-precision number to the specified number of decimal places.
253+
*/
254+
private fun Double.format(decimals: Int): String = "%.${decimals}f".format(this)
255+
256+
/**
257+
* Formats a frequency offset with appropriate sign and units.
258+
*/
259+
private fun Double.formatOffset(): String
260+
{
261+
val sign = if (this >= 0) "+" else ""
262+
return "$sign${format(1)}"
263+
}

0 commit comments

Comments
 (0)