fix(wavelog): send real ADIF band + sat_mode, not the illegal "SAT"
Satellite QSOs uploaded with BAND=SAT, which is not a legal ADIF Band
enumeration value (the legal values are concrete bands: 160M/80M/.../
2M/70CM/23CM...). Loggers that fail to parse an unknown band fall back
to a default — observed as QSOs landing in 160m. SAT is only legal as
PROP_MODE (propagation mode), which is already sent for v1.
Changes (WaveLogApi):
- bandFromHz(): map TX frequency to the real ADIF band (2M for VHF,
70CM for UHF, etc.)
- satModeFrom(): derive the ADIF SAT_MODE convention string from TX/RX
bands ("V/U" = VHF up / UHF down, "U/V", "V/S", "U/S"...; empty for
same-band links)
- v2 JSON: band=<real band>, add sat_mode when non-empty
- v1 ADIF: <band:> real band, add <sat_mode:> when non-empty;
PROP_MODE=SAT kept
Verification:
- New tests: SO-50 (145.850 up / 436.795 down) -> band 2M, sat_mode V/U;
AO-73 (435.150 up / 145.950 down) -> band 70CM, sat_mode U/V;
same-band -> empty sat_mode; satellite freqs never map to 160M.
- All wavelog payload tests + full domain suite green.
This commit is contained in:
1 parent
09ebf1f39a
commit
c42e1d7b4e
2 files changed
+143
-2
No files matched your search
@@ -106,6 +106,51 @@ object WaveLogApi {
|
||||
WavelogResult.Success("")
|
||||
}
|
||||
|
||||
/**
|
||||
* ADIF band code from a frequency in Hz. "SAT" is NOT a legal ADIF band
|
||||
* value (the Band enumeration is 160M/80M/.../2M/70CM/23CM...); a logger
|
||||
* that fails to parse an illegal band falls back to a default such as
|
||||
* 160m. Satellite QSOs must carry the real band of the TX frequency.
|
||||
*/
|
||||
fun bandFromHz(freqHz: Long): String = when {
|
||||
freqHz >= 1240_000_000 -> "23CM"
|
||||
freqHz >= 902_000_000 -> "33CM"
|
||||
freqHz >= 420_000_000 -> "70CM"
|
||||
freqHz >= 222_000_000 -> "1.25M"
|
||||
freqHz >= 144_000_000 -> "2M"
|
||||
freqHz >= 50_000_000 -> "6M"
|
||||
freqHz >= 28_000_000 -> "10M"
|
||||
freqHz >= 24_890_000 -> "12M"
|
||||
freqHz >= 21_000_000 -> "15M"
|
||||
freqHz >= 18_068_000 -> "17M"
|
||||
freqHz >= 14_000_000 -> "20M"
|
||||
freqHz >= 10_000_000 -> "30M"
|
||||
freqHz >= 7_000_000 -> "40M"
|
||||
freqHz >= 5_102_000 -> "60M"
|
||||
freqHz >= 3_500_000 -> "80M"
|
||||
freqHz >= 1_800_000 -> "160M"
|
||||
else -> "160M"
|
||||
}
|
||||
|
||||
/** Band class letter for satellite mode derivation: VHF=V, UHF=U, SHF=S. */
|
||||
private fun bandLetter(freqHz: Long): String = when {
|
||||
freqHz >= 1_240_000_000 -> "S"
|
||||
freqHz >= 420_000_000 -> "U"
|
||||
freqHz >= 144_000_000 -> "V"
|
||||
else -> "V"
|
||||
}
|
||||
|
||||
/**
|
||||
* ADIF SAT_MODE (free text, satellite convention): "V/U" = VHF up /
|
||||
* UHF down, "U/V", "V/S", "U/S"... Derived from the actual TX/RX bands.
|
||||
*/
|
||||
fun satModeFrom(txFreqHz: Long, rxFreqHz: Long): String {
|
||||
if (rxFreqHz <= 0) return ""
|
||||
val up = bandLetter(txFreqHz)
|
||||
val down = bandLetter(rxFreqHz)
|
||||
return if (up == down) "" else "$up/$down"
|
||||
}
|
||||
|
||||
/** LoTW-recognized satellite name: main name before parentheses, uppercased (ISS special case) */
|
||||
fun normalizeSatName(raw: String): String {
|
||||
val main = raw.substringBefore('(').trim()
|
||||
@@ -142,10 +187,11 @@ object WaveLogApi {
|
||||
val satName = normalizeSatName(qso.satName)
|
||||
|
||||
// v2: POST /index.php/api/v2/qso (JSON fields)
|
||||
val satMode = satModeFrom(qso.freqTxHz, qso.freqRxHz)
|
||||
val v2Body = JSONObject().apply {
|
||||
put("station_profile_id", stationProfileId.toIntOrNull() ?: 0)
|
||||
put("call", qso.call)
|
||||
put("band", "SAT")
|
||||
put("band", bandFromHz(qso.freqTxHz))
|
||||
put("mode", qso.mode)
|
||||
put("qso_date", utcDate(qso.timeUtcMs))
|
||||
put("time_on", utcTime(qso.timeUtcMs))
|
||||
@@ -155,6 +201,7 @@ object WaveLogApi {
|
||||
put("rst_sent", "59")
|
||||
put("rst_rcvd", "59")
|
||||
put("sat_name", satName)
|
||||
if (satMode.isNotBlank()) put("sat_mode", satMode)
|
||||
}
|
||||
val (code, resp) = httpRequest("$base/index.php/api/v2/qso", "POST", apiKey, v2Body.toString())
|
||||
if (code in 200..299) return@withContext WavelogResult.Success("已上传 (v2)")
|
||||
@@ -183,9 +230,10 @@ object WaveLogApi {
|
||||
val bytes = value.toByteArray(Charsets.UTF_8).size
|
||||
return "<$name:$bytes>$value"
|
||||
}
|
||||
val satMode = satModeFrom(qso.freqTxHz, qso.freqRxHz)
|
||||
return buildString {
|
||||
append(field("call", qso.call))
|
||||
append(field("band", "SAT"))
|
||||
append(field("band", bandFromHz(qso.freqTxHz)))
|
||||
append(field("mode", qso.mode))
|
||||
append(field("freq", String.format(Locale.ENGLISH, "%.6f", qso.freqTxHz / 1_000_000.0)))
|
||||
if (qso.freqRxHz > 0) {
|
||||
@@ -198,6 +246,7 @@ object WaveLogApi {
|
||||
if (gridsquare.isNotBlank()) append(field("gridsquare", gridsquare.take(4)))
|
||||
if (satName.isNotBlank()) {
|
||||
append(field("sat_name", satName))
|
||||
if (satMode.isNotBlank()) append(field("sat_mode", satMode))
|
||||
append(field("prop_mode", "SAT"))
|
||||
}
|
||||
append("<eor>")
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.rtbishop.look4sat.core.domain.wavelog
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Verifies the WaveLog upload payload frequency/band fields.
|
||||
*
|
||||
* Regression: user reported QSOs landing in the 160m band. The v2 JSON
|
||||
* envelope must carry freq as a MHz string with an "M" suffix so WaveLog's
|
||||
* parse_frequency() reads it as Hz internally; a bare integer or bare MHz
|
||||
* value corrupts band derivation.
|
||||
*/
|
||||
class WaveLogApiPayloadTest {
|
||||
|
||||
// SO-50: uplink 145.850 MHz, downlink 436.795 MHz
|
||||
private val uplinkHz = 145_850_000L
|
||||
private val downlinkHz = 436_795_000L
|
||||
|
||||
@Test
|
||||
fun v2_freq_usesMhzStringWithMSuffix() {
|
||||
val freq = String.format(Locale.ENGLISH, "%.6fM", uplinkHz / 1_000_000.0)
|
||||
val freqRx = String.format(Locale.ENGLISH, "%.6fM", downlinkHz / 1_000_000.0)
|
||||
assertEquals("145.850000M", freq)
|
||||
assertEquals("436.795000M", freqRx)
|
||||
// WaveLog parse_frequency: "145.850000M" -> 145850000 Hz
|
||||
val parsedHz = parseLikeWaveLog(freq)
|
||||
assertEquals(uplinkHz, parsedHz)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun v1_adif_freq_isBareMhzNumber() {
|
||||
// v1 ADIF <FREQ> is a bare MHz number per ADIF spec (no unit suffix)
|
||||
val freq = String.format(Locale.ENGLISH, "%.6f", uplinkHz / 1_000_000.0)
|
||||
assertEquals("145.850000", freq)
|
||||
val adifFreq = freq.toDouble() * 1_000_000
|
||||
assertEquals(uplinkHz.toDouble(), adifFreq, 1.0)
|
||||
}
|
||||
|
||||
/** Mirrors WaveLog Logbook_model::parse_frequency: int = Hz, "12.3M" suffix = MHz. */
|
||||
private fun parseLikeWaveLog(raw: String): Long {
|
||||
val s = raw.trim()
|
||||
return if (s.endsWith("M", ignoreCase = true)) {
|
||||
(s.dropLast(1).toDouble() * 1_000_000).toLong()
|
||||
} else if (s.endsWith("k", ignoreCase = true)) {
|
||||
(s.dropLast(1).toDouble() * 1_000).toLong()
|
||||
} else {
|
||||
s.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun qsoFreqs_stayInSatelliteBands_afterDoppler() {
|
||||
// Doppler-corrected values must remain near the base frequency.
|
||||
// SPEED_OF_LIGHT = 299792458 m/s; distanceRate is km/s (x1000 -> m/s).
|
||||
val dopplerRate = 7.0 // km/s approaching
|
||||
val corrected = uplinkHz * (299_792_458.0 + dopplerRate * 1000.0) / 299_792_458.0
|
||||
assertTrue(
|
||||
"corrected within +-20kHz, got ${corrected - uplinkHz} Hz",
|
||||
kotlin.math.abs(corrected - uplinkHz) < 20_000
|
||||
)
|
||||
// band derivation: 145.x MHz -> 2m, never 160m (1.8-2.0 MHz)
|
||||
val mhz = corrected / 1_000_000.0
|
||||
assertTrue("145.x MHz stays in 2m, got $mhz MHz", mhz in 144.0..148.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun band_isRealBand_notSAT() {
|
||||
// SO-50: TX 145.850 MHz (VHF) -> band 2M, sat mode V/U
|
||||
assertEquals("2M", WaveLogApi.bandFromHz(145_850_000))
|
||||
assertEquals("V/U", WaveLogApi.satModeFrom(145_850_000, 436_795_000))
|
||||
// AO-73: TX 435.150 MHz (UHF up), RX 145.950 MHz (VHF down) -> band 70CM, U/V
|
||||
assertEquals("70CM", WaveLogApi.bandFromHz(435_150_000))
|
||||
assertEquals("U/V", WaveLogApi.satModeFrom(435_150_000, 145_950_000))
|
||||
// Same-band (e.g. simplex) -> empty sat mode
|
||||
assertEquals("", WaveLogApi.satModeFrom(145_850_000, 145_950_000))
|
||||
// Never 160m for satellite frequencies
|
||||
assertTrue(WaveLogApi.bandFromHz(145_850_000) != "160M")
|
||||
assertTrue(WaveLogApi.bandFromHz(436_795_000) != "160M")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun adif_containsRealBandAndSatMode() {
|
||||
// v2 payload fields (mirror postQso construction)
|
||||
val band = WaveLogApi.bandFromHz(uplinkHz)
|
||||
val satMode = WaveLogApi.satModeFrom(uplinkHz, downlinkHz)
|
||||
assertEquals("2M", band)
|
||||
assertEquals("V/U", satMode)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user