Skip to content

Commit db93252

Browse files
committed
WSPRStation updates
1 parent 878f548 commit db93252

File tree

5 files changed

+165
-21
lines changed

5 files changed

+165
-21
lines changed

.idea/caches/deviceStreaming.xml

Lines changed: 36 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package org.operatorfoundation.audiocoder
22

33
import kotlinx.coroutines.*
44
import kotlinx.coroutines.flow.*
5+
import org.operatorfoundation.audiocoder.WSPRTimingConstants.AUDIO_CHUNK_DURATION_MILLISECONDS
6+
import org.operatorfoundation.audiocoder.WSPRTimingConstants.AUDIO_COLLECTION_DURATION_MILLISECONDS
7+
import org.operatorfoundation.audiocoder.WSPRTimingConstants.AUDIO_COLLECTION_PAUSE_MILLISECONDS
8+
import org.operatorfoundation.audiocoder.WSPRTimingConstants.CYCLE_INFORMATION_UPDATE_INTERVAL_MILLISECONDS
59
import org.operatorfoundation.audiocoder.models.WSPRCycleInformation
610
import org.operatorfoundation.audiocoder.models.WSPRDecodeResult
711
import org.operatorfoundation.audiocoder.models.WSPRStationConfiguration
@@ -73,7 +77,7 @@ class WSPRStation(
7377
val stationState: StateFlow<WSPRStationState> = _stationState.asStateFlow()
7478

7579
/**
76-
* Most recent decode results.
80+
* Most recent WSPR decode results.
7781
* Updated after each successful decode cycle with all detected signals.
7882
*/
7983
private val _decodeResults = MutableStateFlow<List<WSPRDecodeResult>>(emptyList())
@@ -187,7 +191,8 @@ class WSPRStation(
187191
*/
188192
suspend fun requestImmediateDecode(): Result<List<WSPRDecodeResult>>
189193
{
190-
return try {
194+
return try
195+
{
191196
if (!timingCoordinator.isCurrentlyInValidDecodeWindow())
192197
{
193198
val nextWindowInfo = timingCoordinator.getTimeUntilNextDecodeWindow()
@@ -292,6 +297,71 @@ class WSPRStation(
292297
// Phase 2: Collect audio for the required duration
293298
_stationState.value = WSPRStationState.CollectingAudio
294299
val audioCollectionStartTime = System.currentTimeMillis()
300+
301+
while (System.currentTimeMillis() - audioCollectionStartTime < AUDIO_COLLECTION_DURATION_MILLISECONDS)
302+
{
303+
val audioChunk = audioSource.readAudioChunk(AUDIO_CHUNK_DURATION_MILLISECONDS)
304+
signalProcessor.addSamples(audioChunk)
305+
306+
// Brief pause to prevent excessive CPU usage
307+
delay(AUDIO_COLLECTION_PAUSE_MILLISECONDS)
308+
}
309+
310+
// Phase 3: Process collected audio through WSPR decoder
311+
_stationState.value = WSPRStationState.ProcessingAudio
312+
313+
val nativeDecodeResults = signalProcessor.decodeBufferedWSPR(
314+
dialFrequencyMHz = configuration.operatingFrequencyMHz,
315+
useLowerSideband = configuration.useLowerSidebandMode,
316+
useTimeAlignment = configuration.useTimeAlignedDecoding
317+
)
318+
319+
// Phase 4: Convert and store results
320+
val processedResults = convertNativeResultsToApplicationFormat(nativeDecodeResults)
321+
_decodeResults.value = processedResults
322+
323+
return processedResults
324+
}
325+
326+
/**
327+
* Converts native WSPR decoder results to application-friendly format.
328+
*
329+
* The native decoder returns WSPRMessage objects with specific field formats.
330+
* This method normalizes the data and adds application specific metadata.
331+
*
332+
* @param nativeResults Raw results from the native WSPR decoder
333+
* @return List of processed decode results with consistent formatting
334+
*/
335+
private fun convertNativeResultsToApplicationFormat(nativeResults: Array<WSPRMessage>?): List<WSPRDecodeResult>
336+
{
337+
if (nativeResults == null) return emptyList()
338+
339+
return nativeResults.map { nativeMessage ->
340+
WSPRDecodeResult(
341+
callsign = nativeMessage.call?.trim() ?: WSPRDecodeResult.UNKNOWN_CALLSIGN,
342+
gridSquare = nativeMessage.loc?.trim() ?: WSPRDecodeResult.UNKNOWN_GRID_SQUARE,
343+
powerLevelDbm = nativeMessage.power,
344+
signalToNoiseRatioDb = nativeMessage.snr,
345+
frequencyOffsetHz = nativeMessage.freq,
346+
completeMessage = nativeMessage.message?.trim() ?: WSPRDecodeResult.EMPTY_MESSAGE,
347+
decodeTimestamp = System.currentTimeMillis()
348+
)
349+
}
350+
}
351+
352+
/**
353+
* Starts background updates for cycle information display.
354+
* Updates cycle position and timing information every second for UI consumption.
355+
*/
356+
private fun startCycleInformationUpdates()
357+
{
358+
CoroutineScope(Dispatchers.IO).launch {
359+
while (stationOperationJob?.isActive == true)
360+
{
361+
_cycleInformation.value = timingCoordinator.getCurrentCycleInformation()
362+
delay(CYCLE_INFORMATION_UPDATE_INTERVAL_MILLISECONDS)
363+
}
364+
}
295365
}
296366

297367
}

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

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

33
import android.icu.util.Calendar
4+
import org.operatorfoundation.audiocoder.WSPRTimingConstants.DECODE_START_DELAY_SECONDS
45
import org.operatorfoundation.audiocoder.WSPRTimingConstants.DECODE_WINDOW_END_SECOND
56

67
import org.operatorfoundation.audiocoder.WSPRTimingConstants.MINUTES_PER_HOUR
78
import org.operatorfoundation.audiocoder.WSPRTimingConstants.MINUTES_PER_WSPR_CYCLE
9+
import org.operatorfoundation.audiocoder.WSPRTimingConstants.SECONDS_PER_MINUTE
810
import org.operatorfoundation.audiocoder.models.WSPRCycleInformation
911
import org.operatorfoundation.audiocoder.models.WSPRDecodeWindowInformation
1012

@@ -106,14 +108,14 @@ class WSPRTimingCoordinator
106108
val currentSecondInMinute = currentTimeCalendar.get(Calendar.SECOND)
107109

108110
// Cneck if we're in an even minute (transmission window)
109-
val isEvenMinute = (currentMinuteInHour % WSPRTimingConstants.MINUTES_PER_WSPR_CYCLE == 0)
111+
val isEvenMinute = (currentMinuteInHour % MINUTES_PER_WSPR_CYCLE == 0)
110112

111113
// Calculate position within the current 2-minute WSPR cycle (0-119 seconds)
112114
val cyclePositionSeconds = calculatePositionInCurrentWSPRCycle(currentMinuteInHour, currentSecondInMinute)
113115

114116
// Check timing constraints within the full 2-minute cycle
115-
val isPastDecodeStartDelay = (cyclePositionSeconds >= WSPRTimingConstants.DECODE_START_DELAY_SECONDS)
116-
val isBeforeDecodeWindowEnd = (cyclePositionSeconds <= WSPRTimingConstants.DECODE_WINDOW_END_SECOND)
117+
val isPastDecodeStartDelay = (cyclePositionSeconds >= DECODE_START_DELAY_SECONDS)
118+
val isBeforeDecodeWindowEnd = (cyclePositionSeconds <= DECODE_WINDOW_END_SECOND)
117119

118120
return isEvenMinute && isPastDecodeStartDelay && isBeforeDecodeWindowEnd
119121
}
@@ -131,8 +133,8 @@ class WSPRTimingCoordinator
131133
*/
132134
private fun calculatePositionInCurrentWSPRCycle(currentMinute: Int, currentSecond: Int): Int
133135
{
134-
val minuteInCycle = currentMinute % WSPRTimingConstants.MINUTES_PER_WSPR_CYCLE
135-
return minuteInCycle * WSPRTimingConstants.SECONDS_PER_MINUTE + currentSecond
136+
val minuteInCycle = currentMinute % MINUTES_PER_WSPR_CYCLE
137+
return minuteInCycle * SECONDS_PER_MINUTE + currentSecond
136138
}
137139

138140
/**

AudioCoder/src/main/java/org/operatorfoundation/audiocoder/models/WSPRCycleInformation.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.operatorfoundation.audiocoder.models
22

33
import org.operatorfoundation.audiocoder.WSPRTimingConstants
4+
import org.operatorfoundation.audiocoder.WSPRTimingConstants.WSPR_CYCLE_DURATION_SECONDS
45

56
/**
67
* Real-time information about the current WSPR cycle state.
@@ -36,6 +37,6 @@ data class WSPRCycleInformation(
3637
/**
3738
* Progress through current 2-minute cycle as a percentage (0.0 - 1.0).
3839
*/
39-
val currentCyclePercentage: Float
40-
get() = cyclePositionSeconds.toFloat() / WSPRTimingConstants.WSPR_CYCLE_DURATION_SECONDS.toFloat()
40+
val cycleProgressPercentage: Float
41+
get() = cyclePositionSeconds.toFloat() / WSPR_CYCLE_DURATION_SECONDS.toFloat()
4142
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.operatorfoundation.audiocoder.models
2+
3+
/**
4+
* Represents the current state of WSPR station operation.
5+
* Provides detailed information about what the station is currently doing.
6+
*/
7+
sealed class WSPRStationState
8+
{
9+
/** Station is not running */
10+
object Stopped : WSPRStationState()
11+
12+
/** Station is initializing and starting up */
13+
object Starting : WSPRStationState()
14+
15+
/** Station is running and monitoring for decode opportunities */
16+
object Running : WSPRStationState()
17+
18+
/** Station is shutting down */
19+
object Stopping : WSPRStationState()
20+
21+
/**
22+
* Station is waiting for the next WSPR decode window to begin.
23+
* @param windowInfo Information about the upcoming decode window
24+
*/
25+
data class WaitingForNextWindow(val windowInfo: WSPRDecodeWindowInformation) : WSPRStationState()
26+
27+
/** Station is preparing to collect audio (clearing buffers, etc.) */
28+
object PreparingForCollection : WSPRStationState()
29+
30+
/** Station is actively collecting audio for decode */
31+
object CollectingAudio : WSPRStationState()
32+
33+
/** Station is processing collected audio through the WSPR decoder */
34+
object ProcessingAudio : WSPRStationState()
35+
36+
/**
37+
* Decode cycle completed successfully.
38+
* @param decodedSignalCount Number of WSPR signals found in this cycle
39+
*/
40+
data class DecodeCompleted(val decodedSignalCount: Int) : WSPRStationState()
41+
42+
/**
43+
* Station encountered an error and requires attention.
44+
* @param errorDescription Human-readable description of the error
45+
*/
46+
data class Error(val errorDescription: String) : WSPRStationState()
47+
}

0 commit comments

Comments
 (0)