mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-06 12:37:26 +00:00
fix(wavelog,radar): 4.5.2 second overwrite - v1 API support, error dialog with copy, frequency sync
Background: user's WaveLog server has NO v2 API (all /api/v2/* return
404; /api/qso v1 works - verified with curl). First fix only tried v2
paths, so uploads still failed with 404. Also: Log tab frequencies did
not match the Doppler panel, linear transponders showed a single
frequency instead of the passband range, and errors were toast-only
(no copy).
Changes:
- WaveLogApi: full v1 support with auto fallback
- v2 first (Bearer header + JSON fields), on 404 fall back to v1
(key inside JSON body + ADIF string) - both with and without
index.php prefix
- test connection: v2 GET api/v2/token -> v1 POST
api/get_contacts_adif (validates key + station id)
- station gridsquare is v2-only; on v1 the uploader falls back to
the user's QTH grid (grid check skipped/equal)
- v1 ADIF: call/band=SAT/mode/freq+freq_rx (MHz)/qso_date/time_on
(UTC, compact)/gridsquare(4)/sat_name/prop_mode=SAT, byte-length
field prefixes
- 409 duplicate counts as success in both versions
- Error dialog with copy: test/upload failures now open an AlertDialog
with the full error (incl. actual URL + HTTP code), Copy button
(ClipboardManager) and Cancel; uploader collects the first failure
message
- Log tab frequency sync: LogTab receives txBaseFrequencyHz from the
radar page (the tuned frequency shown in the transceiver panel);
RX is computed through the same Doppler mapping as the Doppler
panel; linear transponders show the full uplink/downlink range
(Doppler-corrected low-high) instead of a single frequency
- Restored uploadWavelogQueue (lost in an earlier patch)
Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL.
This commit is contained in:
1 parent
5351afe508
commit
9d437a90c9
10 files changed
+250
-147
No files matched your search
@@ -65,6 +65,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQso
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
@@ -86,6 +87,7 @@ fun LogTab(
|
||||
queue: WavelogQueue,
|
||||
wavelogConfigured: Boolean,
|
||||
showToast: (String) -> Unit,
|
||||
txBaseFrequencyHz: Long? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var selectedUuid by remember { mutableStateOf<String?>(null) }
|
||||
@@ -155,6 +157,7 @@ fun LogTab(
|
||||
satelliteName = satelliteName,
|
||||
queue = queue,
|
||||
showToast = showToast,
|
||||
txBaseFrequencyHz = txBaseFrequencyHz,
|
||||
onSaved = { refreshTick++ }
|
||||
)
|
||||
}
|
||||
@@ -220,17 +223,23 @@ private fun ExpandedLogInput(
|
||||
satelliteName: String,
|
||||
queue: WavelogQueue,
|
||||
showToast: (String) -> Unit,
|
||||
txBaseFrequencyHz: Long? = null,
|
||||
onSaved: () -> Unit
|
||||
) {
|
||||
var callsign by remember { mutableStateOf("") }
|
||||
var mode by remember { mutableStateOf(radio.uplinkMode ?: "FM") }
|
||||
|
||||
// 回车那一刻的多普勒修正频率(UTC 时间一并采样)
|
||||
// 回车那一刻的频率(与雷达页 Doppler 面板一致): 优先用当前调谐 TX 频率
|
||||
fun currentFreqs(): Pair<Long, Long> {
|
||||
val uplinkBase = radio.uplinkLow ?: radio.downlinkLow ?: 0L
|
||||
val downlinkBase = radio.downlinkLow ?: radio.uplinkLow ?: 0L
|
||||
val tx = if (orbitalPos != null) orbitalPos.getUplinkFreq(uplinkBase) else uplinkBase
|
||||
val rx = if (orbitalPos != null) orbitalPos.getDownlinkFreq(downlinkBase) else downlinkBase
|
||||
// 调谐基准(用户在当前转发器上调的)优先; 否则转发器标称下限
|
||||
val tx = txBaseFrequencyHz ?: (if (orbitalPos != null) orbitalPos.getUplinkFreq(uplinkBase) else uplinkBase)
|
||||
val rx = if (orbitalPos != null) {
|
||||
// 线性转发器: 按映射算下行; FM: 标称下行加多普勒
|
||||
DopplerFrequencyCalculator.computeDownlinkFromUplink(tx, radio, orbitalPos)
|
||||
?: orbitalPos.getDownlinkFreq(downlinkBase)
|
||||
} else downlinkBase
|
||||
return tx to rx
|
||||
}
|
||||
|
||||
@@ -257,15 +266,31 @@ private fun ExpandedLogInput(
|
||||
}
|
||||
|
||||
val (txHz, rxHz) = currentFreqs()
|
||||
// 线性转发器: 显示频段区间(多普勒修正); FM: 单频
|
||||
val isLinear = DopplerFrequencyCalculator.isLinearTransponder(radio)
|
||||
val uplinkLow = radio.uplinkLow
|
||||
val uplinkHigh = radio.uplinkHigh
|
||||
val downlinkLow = radio.downlinkLow
|
||||
val downlinkHigh = radio.downlinkHigh
|
||||
val txRange = if (isLinear && uplinkLow != null && uplinkHigh != null && orbitalPos != null) {
|
||||
val lo = orbitalPos.getUplinkFreq(uplinkLow)
|
||||
val hi = orbitalPos.getUplinkFreq(uplinkHigh)
|
||||
"${formatFrequency(minOf(lo, hi))}–${formatFrequency(maxOf(lo, hi))}"
|
||||
} else null
|
||||
val rxRange = if (isLinear && downlinkLow != null && downlinkHigh != null && orbitalPos != null) {
|
||||
val lo = orbitalPos.getDownlinkFreq(downlinkLow)
|
||||
val hi = orbitalPos.getDownlinkFreq(downlinkHigh)
|
||||
"${formatFrequency(minOf(lo, hi))}–${formatFrequency(maxOf(lo, hi))}"
|
||||
} else null
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
text = "TX ${formatFrequency(txHz)}",
|
||||
text = "TX ${txRange ?: formatFrequency(txHz)}",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "RX ${formatFrequency(rxHz)}",
|
||||
text = "RX ${rxRange ?: formatFrequency(rxHz)}",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
@@ -275,7 +275,8 @@ private fun PagerCard(
|
||||
satelliteName = uiState.currentPass?.name ?: "",
|
||||
queue = wavelogQueue,
|
||||
wavelogConfigured = wavelogConfigured,
|
||||
showToast = showToast
|
||||
showToast = showToast,
|
||||
txBaseFrequencyHz = uiState.radioControl.txBaseFrequencyHz
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -44,6 +44,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.AlertDialog
|
||||
@@ -144,6 +145,35 @@ fun SettingsDestination() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// WaveLog 错误详情弹窗(可一键复制)
|
||||
val wavelogError = viewModel.wavelogError
|
||||
if (wavelogError != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.wavelogError = null },
|
||||
title = { Text(text = stringResource(id = R.string.wavelog_title)) },
|
||||
text = {
|
||||
Text(
|
||||
text = wavelogError,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("WaveLog error", wavelogError))
|
||||
viewModel.wavelogError = null
|
||||
}) {
|
||||
Text(text = stringResource(id = R.string.wavelog_copy))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.wavelogError = null }) {
|
||||
Text(text = stringResource(id = R.string.btn_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+21
-14
@@ -232,32 +232,39 @@ class SettingsViewModel(
|
||||
|
||||
// region WaveLog(4.5.2)
|
||||
|
||||
private fun testWavelogConnection() {
|
||||
viewModelScope.launch {
|
||||
val s = settingsRepo.otherSettings.value
|
||||
val result = WaveLogApi.testToken(s.wavelogUrl, s.wavelogApiKey)
|
||||
showToast(
|
||||
when (result) {
|
||||
is WavelogResult.Success -> "WaveLog: 连接成功"
|
||||
is WavelogResult.Failure -> "WaveLog: ${result.message}"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 由 SettingsScreen 注入的共享 uploader(容器级单例)
|
||||
var pendingUploader: WavelogUploader? = null
|
||||
|
||||
// 网格不一致确认弹窗状态(UI 观察后弹 AlertDialog)
|
||||
var gridConfirm by mutableStateOf<GridConfirmData?>(null)
|
||||
|
||||
// WaveLog 错误详情弹窗(UI 观察后弹 AlertDialog, 可一键复制)
|
||||
var wavelogError by mutableStateOf<String?>(null)
|
||||
|
||||
data class GridConfirmData(val stationGrid: String, val userGrid: String)
|
||||
|
||||
private fun testWavelogConnection() {
|
||||
viewModelScope.launch {
|
||||
val s = settingsRepo.otherSettings.value
|
||||
val result = WaveLogApi.testToken(s.wavelogUrl, s.wavelogApiKey, s.wavelogStationId)
|
||||
when (result) {
|
||||
is WavelogResult.Success -> showToast("WaveLog: ${result.message}")
|
||||
is WavelogResult.Failure -> wavelogError = result.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadWavelogQueue() {
|
||||
viewModelScope.launch {
|
||||
val result = pendingUploader?.uploadQueue()
|
||||
when (result) {
|
||||
is UploadOutcome.Done -> showToast("WaveLog: ${result.message}")
|
||||
is UploadOutcome.Done -> {
|
||||
if (result.failedCount > 0 && result.firstError.isNotBlank()) {
|
||||
wavelogError = result.firstError
|
||||
} else {
|
||||
showToast("WaveLog: ${result.message}")
|
||||
}
|
||||
}
|
||||
is UploadOutcome.NeedConfirm -> {
|
||||
gridConfirm = GridConfirmData(result.stationGrid, result.userGrid)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user