[feat](trx-rs): add GetSatPasses protocol command for server-side TLE management
TLE refresh now happens only on trx-server (once at startup, then every 24h). Client fetches satellite predictions from server via new GetSatPasses fast-path command and caches them locally, refreshing every 5 minutes. Removes spawn_tle_refresh_task from trx-client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -260,6 +260,8 @@ pub struct FrontendRuntimeContext {
|
||||
pub remote_active_rig_id: Arc<Mutex<Option<String>>>,
|
||||
/// Cached remote rig list from GetRigs polling.
|
||||
pub remote_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
|
||||
/// Cached satellite pass predictions from the server (GetSatPasses).
|
||||
pub sat_passes: Arc<RwLock<Option<trx_core::geo::PassPredictionResult>>>,
|
||||
/// Per-rig state watch channels, keyed by rig_id.
|
||||
/// Populated by the remote client poll loop so each SSE session can
|
||||
/// subscribe to a specific rig's state independently.
|
||||
@@ -391,6 +393,7 @@ impl FrontendRuntimeContext {
|
||||
http_decode_history_retention_min_by_rig: HashMap::new(),
|
||||
remote_active_rig_id: Arc::new(Mutex::new(None)),
|
||||
remote_rigs: Arc::new(Mutex::new(Vec::new())),
|
||||
sat_passes: Arc::new(RwLock::new(None)),
|
||||
rig_states: Arc::new(RwLock::new(HashMap::new())),
|
||||
owner_callsign: None,
|
||||
owner_website_url: None,
|
||||
|
||||
@@ -107,6 +107,7 @@ async fn handle_client(
|
||||
rig_id: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: Some(format!("Invalid JSON: {}", e)),
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
@@ -120,6 +121,7 @@ async fn handle_client(
|
||||
rig_id: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: Some(err),
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
@@ -138,6 +140,7 @@ async fn handle_client(
|
||||
rig_id: Some("client".to_string()),
|
||||
state: None,
|
||||
rigs: Some(snapshot_remote_rigs(context.as_ref())),
|
||||
sat_passes: None,
|
||||
error: None,
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
@@ -168,6 +171,7 @@ async fn handle_client(
|
||||
rig_id: active_rig_id.clone(),
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: Some("Internal error: rig task not available".into()),
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
@@ -179,6 +183,7 @@ async fn handle_client(
|
||||
rig_id: active_rig_id.clone(),
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: Some("Internal error: request queue timeout".into()),
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
@@ -193,6 +198,7 @@ async fn handle_client(
|
||||
rig_id: active_rig_id.clone(),
|
||||
state: Some(snapshot),
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: None,
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
@@ -203,6 +209,7 @@ async fn handle_client(
|
||||
rig_id: active_rig_id.clone(),
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: Some(err.message),
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
@@ -214,6 +221,7 @@ async fn handle_client(
|
||||
rig_id: active_rig_id.clone(),
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: Some("Internal error waiting for rig response".into()),
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
@@ -224,6 +232,7 @@ async fn handle_client(
|
||||
rig_id: active_rig_id.clone(),
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: Some("Request timed out waiting for rig response".into()),
|
||||
};
|
||||
send_response(&mut writer, &resp).await?;
|
||||
|
||||
@@ -1384,43 +1384,34 @@ struct SatPassesResponse {
|
||||
|
||||
/// Return predicted passes for all known satellites over the next 24 h.
|
||||
///
|
||||
/// Requires the server station location to be configured. Returns an empty
|
||||
/// `passes` array with an `error` field if the location is missing or TLE
|
||||
/// data has not been fetched yet.
|
||||
/// Reads cached predictions from the server (fetched via GetSatPasses).
|
||||
/// Returns an empty `passes` array with an `error` field if predictions
|
||||
/// are not yet available.
|
||||
#[get("/sat_passes")]
|
||||
pub async fn sat_passes(state: web::Data<watch::Receiver<RigState>>) -> impl Responder {
|
||||
let rig_state = state.get_ref().borrow().clone();
|
||||
let lat = rig_state.server_latitude;
|
||||
let lon = rig_state.server_longitude;
|
||||
|
||||
let (Some(lat), Some(lon)) = (lat, lon) else {
|
||||
return web::Json(SatPassesResponse {
|
||||
pub async fn sat_passes(context: web::Data<Arc<FrontendRuntimeContext>>) -> impl Responder {
|
||||
let cached = context.sat_passes.read().ok().and_then(|g| g.clone());
|
||||
match cached {
|
||||
Some(result) => {
|
||||
let error = match result.tle_source {
|
||||
trx_core::geo::TleSource::Unavailable => {
|
||||
Some("TLE data not yet available — waiting for CelesTrak fetch".to_string())
|
||||
}
|
||||
trx_core::geo::TleSource::Celestrak => None,
|
||||
};
|
||||
web::Json(SatPassesResponse {
|
||||
passes: result.passes,
|
||||
error,
|
||||
satellite_count: result.satellite_count,
|
||||
tle_source: result.tle_source,
|
||||
})
|
||||
}
|
||||
None => web::Json(SatPassesResponse {
|
||||
passes: vec![],
|
||||
error: Some("No station location configured".to_string()),
|
||||
error: Some("Satellite predictions not yet available from server".to_string()),
|
||||
satellite_count: 0,
|
||||
tle_source: trx_core::geo::TleSource::Unavailable,
|
||||
});
|
||||
};
|
||||
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
let window_ms = 24 * 60 * 60 * 1000_i64;
|
||||
|
||||
let result = trx_core::geo::compute_upcoming_passes(lat, lon, now_ms, window_ms);
|
||||
let error = match result.tle_source {
|
||||
trx_core::geo::TleSource::Unavailable => {
|
||||
Some("TLE data not yet available — waiting for CelesTrak fetch".to_string())
|
||||
}
|
||||
trx_core::geo::TleSource::Celestrak => None,
|
||||
};
|
||||
web::Json(SatPassesResponse {
|
||||
passes: result.passes,
|
||||
error,
|
||||
satellite_count: result.satellite_count,
|
||||
tle_source: result.tle_source,
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/clear_ft8_decode")]
|
||||
@@ -2400,6 +2391,7 @@ async fn send_command(
|
||||
rig_id: None,
|
||||
state: Some(snapshot),
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: None,
|
||||
})),
|
||||
Ok(Err(err)) => Ok(HttpResponse::BadRequest().json(ClientResponse {
|
||||
@@ -2407,6 +2399,7 @@ async fn send_command(
|
||||
rig_id: None,
|
||||
state: None,
|
||||
rigs: None,
|
||||
sat_passes: None,
|
||||
error: Some(err.message),
|
||||
})),
|
||||
Err(e) => Err(actix_web::error::ErrorInternalServerError(format!(
|
||||
|
||||
Reference in New Issue
Block a user