import math import numpy as np from datetime import datetime, timezone, timedelta speedOfLight_c = 299792458.0 uplinkFreq = 1646652500 downlinkFreq = 3615152500 btoBias = -495679 BFO_bias = 150 elip_a = 6378137.0 elip_f = 1.0 / 298.257223563 elip_b = elip_a * (1.0 - elip_f) elip_e2 = (2 * elip_f) - (elip_f * elip_f) def degToRad(d): return d * math.pi / 180.0 def radToDeg(r): return r * 180.0 / math.pi def feetToM(ft): return ft * 0.3048 satLoc = ( (("18:25:00"), (18136.7, 38071.8, 1148.5), (0.00188, -0.00117, 0.02690), 11, 142, 12520), (("18:40:00"), (18138.21, 38070.79, 1170.29), (0.001892134, -0.001142665, 0.02137832), 8, 88, None), (("19:40:00"), (18145.1, 38067.0, 1206.3), (0.00189, -0.00092, -0.00148), -1, 111, 11500), (("20:40:00"), (18152.1, 38064.0, 1159.7), (0.00200, -0.00077, -0.02422), -1, 141, 11740), (("21:40:00"), (18159.5, 38061.3, 1033.8), (0.00212, -0.00076, -0.04531), -18, 168, 12780), (("22:40:00"), (18167.2, 38058.3, 837.2), (0.00211, -0.00096, -0.06331), -29, 204, 14540), (("24:10:00"), (18177.5, 38051.7, 440.0), (0.00160, -0.00151, -0.08188), -38, 252, 18040), (("24:20:00"), (18178.4, 38050.8, 390.5), (0.00150, -0.00158, -0.08321), -38, 182, 18400) ) gsPerth = (-2368.8, 4881.1, -3342.0) nominalSatLoc = (0, 64.5, 36000000) def N_phi(lat_rad): sinp = math.sin(lat_rad) return elip_a / math.sqrt(1.0 - elip_e2 * sinp * sinp) def latLonToECEF(lat_deg, lon_deg, h_m): lat = degToRad(lat_deg) lon = degToRad(lon_deg) Np = N_phi(lat) x = (Np + h_m) * math.cos(lat) * math.cos(lon) y = (Np + h_m) * math.cos(lat) * math.sin(lon) z = (Np * (1 - elip_e2) + h_m) * math.sin(lat) return np.array([x, y, z], dtype=float) def distBetECEF(p1, p2): d = p1 - p2 return float(np.sqrt(np.dot(d, d))) def ENU2ECEFvelo(vE, vN, vU, lat_rad, lon_rad): sL, cL = (math.sin(lat_rad), math.cos(lat_rad)) sO, cO = (math.sin(lon_rad), math.cos(lon_rad)) R = np.array([[-sO, -sL * cO, cL * cO], [cO, -sL * sO, cL * sO], [0.0, cL, sL]], dtype=float) return R @ np.array([vE, vN, vU], dtype=float) def toECEFunitVector(p_from, p_to): v = p_to - p_from n = np.linalg.norm(v) return v / n if n != 0 else v def rhumb_direct(start_latlon, bearing_deg, distance_m): lat1 = degToRad(start_latlon[0]) lon1 = degToRad(start_latlon[1]) brg = degToRad(bearing_deg) R = elip_a d = distance_m / R dphi = d * math.cos(brg) lat2 = lat1 + dphi if abs(lat2) > math.pi / 2 - 1e-12: lat2 = math.copysign(math.pi / 2 - 1e-12, lat2) dpsi = math.log(math.tan(lat2 / 2 + math.pi / 4) / math.tan(lat1 / 2 + math.pi / 4)) q = dphi / dpsi if abs(dpsi) > 1e-12 else math.cos(lat1) dlon = d * math.sin(brg) / q lon2 = lon1 + dlon lon2 = (lon2 + math.pi) % (2 * math.pi) - math.pi return (radToDeg(lat2), radToDeg(lon2)) def vincenty_direct(latLon, alpha1_deg, s): lat1 = degToRad(latLon[0]) lon1 = degToRad(latLon[1]) alpha1 = degToRad(alpha1_deg) U1 = math.atan((1 - elip_f) * math.tan(lat1)) sigma1 = math.atan2(math.tan(U1), math.cos(alpha1)) sin_alpha = math.cos(U1) * math.sin(alpha1) cos2_alpha = 1 - sin_alpha ** 2 elip_b = elip_a * (1.0 - elip_f) u2 = cos2_alpha * (elip_a ** 2 - elip_b ** 2) / elip_b ** 2 A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2))) B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2))) sigma = s / (elip_b * A) tol = 1e-12 for _ in range(200): cos2sigma_m = math.cos(2 * sigma1 + sigma) sin_sigma = math.sin(sigma) cos_sigma = math.cos(sigma) delta_sigma = B * sin_sigma * (cos2sigma_m + B / 4 * (cos_sigma * (-1 + 2 * cos2sigma_m ** 2) - B / 6 * cos2sigma_m * (-3 + 4 * sin_sigma ** 2) * (-3 + 4 * cos2sigma_m ** 2))) sigma_new = s / (elip_b * A) + delta_sigma if abs(sigma_new - sigma) < tol: sigma = sigma_new break sigma = sigma_new lat2 = math.atan2(math.sin(U1) * cos_sigma + math.cos(U1) * sin_sigma * math.cos(alpha1), (1 - elip_f) * math.sqrt(sin_alpha ** 2 + (math.sin(U1) * sin_sigma - math.cos(U1) * cos_sigma * math.cos(alpha1)) ** 2)) lam = math.atan2(sin_sigma * math.sin(alpha1), math.cos(U1) * cos_sigma - math.sin(U1) * sin_sigma * math.cos(alpha1)) C = elip_f / 16 * cos2_alpha * (4 + elip_f * (4 - 3 * cos2_alpha)) L = lam - (1 - C) * elip_f * sin_alpha * (sigma + C * sin_sigma * (cos2sigma_m + C * cos_sigma * (-1 + 2 * cos2sigma_m ** 2))) lon2 = lon1 + L alpha2 = math.atan2(sin_alpha, -math.sin(U1) * sin_sigma + math.cos(U1) * cos_sigma * math.cos(alpha1)) return ([radToDeg(lat2), radToDeg(lon2)], (radToDeg(alpha2) + 360) % 360) SIDEREAL_DAY_S = 86164.0 OMEGA = 2.0 * math.pi / SIDEREAL_DAY_S SAT_EPOCH_UTC = datetime(2014, 3, 7, 16, 30, 0, tzinfo=timezone.utc) from datetime import datetime, timedelta, timezone def _safe_float(x, default=0.0): try: return float(x) except (TypeError, ValueError): return default def _extract_time_string(label_field): if isinstance(label_field, (tuple, list)): label = label_field[0] if isinstance(label, (tuple, list)): return str(label[0]) return str(label) return str(label_field) def _time_label_to_dt(label: str, epoch: datetime) -> datetime: hh, mm, ss = map(int, label.split(":")) day_offset = hh // 24 hh = hh % 24 dt = datetime(epoch.year, epoch.month, epoch.day, hh, mm, ss, tzinfo=epoch.tzinfo) if day_offset: dt += timedelta(days=day_offset) return dt def _extract_time_string(tfield): if isinstance(tfield, (tuple, list)): return str(tfield[0]) return str(tfield) def _build_sat_time_seconds(): times_sec = [] for entry in satLoc: tstr = _extract_time_string(entry[0]) try: h, m, s = map(int, tstr.split(':')) except Exception: continue add_day = 0 if h == 24: h = 0 add_day = 1 try: dt = datetime(SAT_EPOCH_UTC.year, SAT_EPOCH_UTC.month, SAT_EPOCH_UTC.day, h, m, s, tzinfo=timezone.utc) except ValueError: continue sec = (dt - SAT_EPOCH_UTC).total_seconds() + add_day * 86400.0 times_sec.append(sec) return np.array(times_sec, dtype=float) _sat_time_seconds = _build_sat_time_seconds() def seconds_since_sat_epoch(t_utc): return (t_utc - SAT_EPOCH_UTC).total_seconds() def calculateBTO_at_time(acLat, acLon, acAlt, time_utc): """ Compute BTO (microseconds) using the *same* satellite geometry as BFO: nearest satLoc entry to time_utc, with positions in km converted to meters. """ # Aircraft ECEF (meters) acECEF = latLonToECEF(acLat, acLon, acAlt) # Nearest satLoc satellite ECEF (meters) idx_nearest = nearest_measured_for_time(time_utc)[0] satECEF = 1000.0 * np.array(satLoc[idx_nearest][1], float) # Perth ground station ECEF (meters) gsECEF = 1000.0 * np.array(gsPerth, float) # One-way distances (meters) d1 = distBetECEF(acECEF, satECEF) # AC -> SAT d2 = distBetECEF(satECEF, gsECEF) # SAT -> GS # Round-trip signal time (microseconds), plus constant system bias bto_us = (2.0 * (d1 + d2) / speedOfLight_c) * 1e6 + btoBias return bto_us def calculateBFO_at_time(flightV_mps, track_deg, acLat, acLon, acAlt, acVS_mps, time_utc): """ Predict BFO using the satellite state from satLoc that is NEAREST to time_utc. Includes δF_sat + δF_AFC from satLoc[idx][3] and BFO_bias. """ # --- aircraft velocity in ENU and ECEF --- vE = flightV_mps * math.sin(degToRad(track_deg)) vN = flightV_mps * math.cos(degToRad(track_deg)) vU = acVS_mps acECEF = latLonToECEF(acLat, acLon, acAlt) # meters vECEF = ENU2ECEFvelo(vE, vN, vU, degToRad(acLat), degToRad(acLon)) # m/s # --- find nearest satLoc entry for this UTC --- idx_nearest = nearest_measured_for_time(time_utc)[0] # --- satellite state from satLoc (convert km → m, km/s → m/s) --- sat_pos_km = np.array(satLoc[idx_nearest][1], float) sat_vel_km_s = np.array(satLoc[idx_nearest][2], float) satECEF = 1000.0 * sat_pos_km # m satV_ECEF = 1000.0 * sat_vel_km_s # m/s # --- ground station (Perth) ECEF in meters --- gsECEF = 1000.0 * np.array(gsPerth, float) # --- line-of-sight unit vectors --- u_ac_sat = toECEFunitVector(acECEF, satECEF) u_sat_gs = toECEFunitVector(satECEF, gsECEF) # --- relative LOS velocities --- v_rel_ac_sat = float(np.dot(vECEF - satV_ECEF, u_ac_sat)) # m/s v_rel_sat_gs = float(np.dot(satV_ECEF, u_sat_gs)) # m/s (GS static in ECEF) # --- Doppler terms --- Fup = uplinkFreq * (v_rel_ac_sat / speedOfLight_c) Fdown = downlinkFreq * (v_rel_sat_gs / speedOfLight_c) # --- aircraft frequency compensation toward nominal sat location --- nomECEF = latLonToECEF(nominalSatLoc[0], nominalSatLoc[1], nominalSatLoc[2]) u_ac_nom = toECEFunitVector(acECEF, nomECEF) v_rel_ac_nom = float(np.dot(vECEF, u_ac_nom)) FcompAC = uplinkFreq * (v_rel_ac_nom / speedOfLight_c) # --- δF_sat + δF_AFC from satLoc (index 3), safe-cast in case of None --- fsat_afc = float(satLoc[idx_nearest][3]) if satLoc[idx_nearest][3] is not None else 0.0 # --- final BFO --- return Fup + Fdown - FcompAC + fsat_afc + BFO_bias def nearest_measured_for_time(t_utc): """ Return (idx, label_str, meas_bfo_hz, meas_bto_us) for the satLoc entry whose time label is nearest to t_utc. Guard against None in measured fields. """ times = [] for i, row in enumerate(satLoc): label_str = _extract_time_string(row[0]) try: dt = _time_label_to_dt(label_str, SAT_EPOCH_UTC) except Exception: continue times.append((i, dt, label_str)) if not times: raise RuntimeError("No valid satLoc times to compare.") idx, dt_near, label = min(times, key=lambda it: abs((t_utc - it[1]).total_seconds())) meas_bfo = _safe_float(satLoc[idx][4], float("nan")) meas_bto = _safe_float(satLoc[idx][5], float("nan")) return (idx, label, meas_bfo, meas_bto) def nearest_measured_with_bto_for_time(t_utc): """Like nearest_measured_for_time, but guarantees meas_bto is not None if possible.""" best = None best_dt = None for i, row in enumerate(satLoc): # row[0] is a time label like "19:40:00" row_dt = SAT_EPOCH_UTC + timedelta(seconds=_sat_time_seconds[i]) dt = abs((t_utc - row_dt).total_seconds()) meas_bto = row[5] if meas_bto is None: continue if best is None or dt < best_dt: best = (i, row[0], row[4], meas_bto) best_dt = dt if best is not None: return best # Fall back: return nearest even if BTO is None idx, meas_time_str, meas_bfo, meas_bto = nearest_measured_for_time(t_utc) return (idx, meas_time_str, meas_bfo, meas_bto) def auto_tune_heading_to_bto(current_lat, current_lon, alt_m, current_time, base_heading_deg, speed_knots, leg_minutes, model_sel, search_half_width_deg=5.0, heading_step_deg=0.1): """Search around base_heading_deg to minimize |measured_bto - calculated_bto| at leg end. Returns dict with best_heading, best_lat, best_lon, best_time, best_bto, best_delta_bto, meas_time_str. """ speed_mps = speed_knots * 0.514444 distance_m = speed_mps * (leg_minutes * 60.0) target_time = current_time + timedelta(minutes=leg_minutes) # get measurement reference (prefer one with non-None BTO) meas_idx, meas_time_str, meas_bfo, meas_bto = nearest_measured_with_bto_for_time(target_time) # If measurement BTO is still None (should be rare), we cannot tune. if meas_bto is None: return { "ok": False, "reason": "No measured BTO available near target time", "best_heading": base_heading_deg, "meas_time_str": meas_time_str, } def wrap(h): h = h % 360.0 return h + 360.0 if h < 0 else h best = None # number of steps on each side n = int(round(search_half_width_deg / heading_step_deg)) for k in range(-n, n + 1): hdg = wrap(base_heading_deg + k * heading_step_deg) if model_sel == '1': (lat2, lon2), _ = vincenty_direct((current_lat, current_lon), hdg, distance_m) else: lat2, lon2 = rhumb_direct((current_lat, current_lon), hdg, distance_m) bto_val = calculateBTO_at_time(lat2, lon2, alt_m, target_time) delta_bto = meas_bto - bto_val score = abs(delta_bto) if best is None or score < best["score"]: best = { "ok": True, "score": score, "best_heading": hdg, "best_lat": lat2, "best_lon": lon2, "best_time": target_time, "best_bto": bto_val, "best_delta_bto": delta_bto, "meas_time_str": meas_time_str, "meas_bto": meas_bto, } return best def auto_tune_heading_speed_to_bto(current_lat, current_lon, alt_m, current_time, base_heading_deg, base_speed_knots, leg_minutes, model_sel, heading_half_width_deg=8.0, heading_step_deg=0.2, speed_half_width_knots=80.0, speed_step_knots=2.0, refine_heading_half_width_deg=1.0, refine_heading_step_deg=0.05, refine_speed_half_width_knots=10.0, refine_speed_step_knots=0.5): """Search around (base_heading_deg, base_speed_knots) to minimize |ΔBTO| at leg end. Two-stage search: 1) coarse grid over heading±heading_half_width_deg and speed±speed_half_width_knots 2) refine around best using smaller steps Returns dict with best_heading, best_speed_knots, best_lat, best_lon, best_time, best_bto, best_delta_bto, meas_time_str, meas_bto. """ target_time = current_time + timedelta(minutes=leg_minutes) meas_idx, meas_time_str, meas_bfo, meas_bto = nearest_measured_with_bto_for_time(target_time) if meas_bto is None: return { "ok": False, "reason": "No measured BTO available near target time.", "best_heading": base_heading_deg, "best_speed_knots": base_speed_knots, "best_lat": None, "best_lon": None, "best_time": target_time, "best_bto": None, "best_delta_bto": None, "meas_time_str": meas_time_str, "meas_bto": None, } def eval_candidate(hdg_deg: float, spd_knots: float): speed_mps = spd_knots * 0.514444 distance_m = speed_mps * (leg_minutes * 60.0) if model_sel == '1': (latlon2, _az2) = vincenty_direct((current_lat, current_lon), hdg_deg, distance_m) lat2, lon2 = latlon2 else: lat2, lon2 = rhumb_direct((current_lat, current_lon), hdg_deg, distance_m) bto_val = calculateBTO_at_time(lat2, lon2, alt_m, target_time) delta_bto = meas_bto - bto_val return lat2, lon2, bto_val, delta_bto def wrap_heading(h): h = h % 360.0 return h + 360.0 if h < 0 else h best = None # ---- stage 1: coarse grid ---- h0 = base_heading_deg s0 = base_speed_knots h_min = h0 - heading_half_width_deg h_max = h0 + heading_half_width_deg s_min = max(0.0, s0 - speed_half_width_knots) s_max = s0 + speed_half_width_knots # Build grids (inclusive ends) headings = np.arange(h_min, h_max + 1e-9, heading_step_deg) speeds = np.arange(s_min, s_max + 1e-9, speed_step_knots) for spd in speeds: for hdg in headings: hdg_w = wrap_heading(float(hdg)) lat2, lon2, bto_val, delta_bto = eval_candidate(hdg_w, float(spd)) score = abs(delta_bto) if best is None or score < best["score"]: best = { "ok": True, "score": score, "best_heading": hdg_w, "best_speed_knots": float(spd), "best_lat": lat2, "best_lon": lon2, "best_time": target_time, "best_bto": bto_val, "best_delta_bto": delta_bto, "meas_time_str": meas_time_str, "meas_bto": meas_bto, } # ---- stage 2: refine around the best ---- if best is None: return { "ok": False, "reason": "Search failed.", "best_heading": base_heading_deg, "best_speed_knots": base_speed_knots, "best_lat": None, "best_lon": None, "best_time": target_time, "best_bto": None, "best_delta_bto": None, "meas_time_str": meas_time_str, "meas_bto": meas_bto, } h1 = best["best_heading"] s1 = best["best_speed_knots"] headings2 = np.arange(h1 - refine_heading_half_width_deg, h1 + refine_heading_half_width_deg + 1e-9, refine_heading_step_deg) speeds2 = np.arange(max(0.0, s1 - refine_speed_half_width_knots), s1 + refine_speed_half_width_knots + 1e-9, refine_speed_step_knots) for spd in speeds2: for hdg in headings2: hdg_w = wrap_heading(float(hdg)) lat2, lon2, bto_val, delta_bto = eval_candidate(hdg_w, float(spd)) score = abs(delta_bto) if score < best["score"]: best.update({ "score": score, "best_heading": hdg_w, "best_speed_knots": float(spd), "best_lat": lat2, "best_lon": lon2, "best_bto": bto_val, "best_delta_bto": delta_bto, }) return best def repl_fly_from_radar_fix(): print('=== MH370 REPL (radar fix) ===') print('Start: 2014-03-07 18:22:12Z @ 6.578°N, 96.341°E, FL350') current_lat = 6.578 current_lon = 96.341 current_alt_m = feetToM(35000) current_time = datetime(2014, 3, 7, 18, 22, 12, tzinfo=timezone.utc) path_points = [(current_lat, current_lon, current_time.isoformat())] while True: try: heading = float(input('HEADING (deg): ').strip()) speed_knots = float(input('SPEED (knots): ').strip()) model_sel = input('Enter 1 for VINCENTY or 2 for RHUMB: ').strip() model_sel = '1' if model_sel not in ('1', '2') else model_sel leg_str = input('Leg duration in minutes (default 10): ').strip() leg_minutes = float(leg_str) if leg_str else 10.0 except Exception as e: print('Invalid input, try again.', e) continue auto_sel = input('Auto-tune: (h) heading, (b) heading+speed, Enter = none: ').strip().lower() speed_mps = speed_knots * 0.514444 distance_m = speed_mps * (leg_minutes * 60.0) if auto_sel in ('b', 'both'): tuned = auto_tune_heading_speed_to_bto( current_lat=current_lat, current_lon=current_lon, alt_m=current_alt_m, current_time=current_time, base_heading_deg=heading, base_speed_knots=speed_knots, leg_minutes=leg_minutes, model_sel=model_sel, ) if tuned.get('ok'): heading = tuned['best_heading'] speed_knots = tuned['best_speed_knots'] # update distance for subsequent printing/metrics speed_mps = speed_knots * 0.514444 distance_m = speed_mps * (leg_minutes * 60.0) lat2 = tuned['best_lat'] lon2 = tuned['best_lon'] new_time = tuned['best_time'] print(f"[Auto-tune] Best heading: {heading:.2f}° | Best speed: {speed_knots:.2f} kt | Expected ΔBTO vs {tuned['meas_time_str']}: {tuned['best_delta_bto']:+.3f} µs") else: if model_sel == '1': (lat2, lon2), _ = vincenty_direct((current_lat, current_lon), heading, distance_m) else: lat2, lon2 = rhumb_direct((current_lat, current_lon), heading, distance_m) new_time = current_time + timedelta(minutes=leg_minutes) elif auto_sel in ('h', 'heading', 'y', 'yes'): tuned = auto_tune_heading_to_bto( current_lat=current_lat, current_lon=current_lon, alt_m=current_alt_m, current_time=current_time, base_heading_deg=heading, speed_knots=speed_knots, leg_minutes=leg_minutes, model_sel=model_sel, search_half_width_deg=5.0, heading_step_deg=0.1, ) if tuned.get('ok'): heading = tuned['best_heading'] lat2 = tuned['best_lat'] lon2 = tuned['best_lon'] new_time = tuned['best_time'] print(f"[Auto-tune] Best heading: {heading:.2f}° | Expected ΔBTO vs {tuned['meas_time_str']}: {tuned['best_delta_bto']:+.3f} µs") else: if model_sel == '1': (lat2, lon2), _ = vincenty_direct((current_lat, current_lon), heading, distance_m) else: lat2, lon2 = rhumb_direct((current_lat, current_lon), heading, distance_m) new_time = current_time + timedelta(minutes=leg_minutes) else: if model_sel == '1': (lat2, lon2), _ = vincenty_direct((current_lat, current_lon), heading, distance_m) else: lat2, lon2 = rhumb_direct((current_lat, current_lon), heading, distance_m) new_time = current_time + timedelta(minutes=leg_minutes) bto_val = calculateBTO_at_time(lat2, lon2, current_alt_m, new_time) tsec = seconds_since_sat_epoch(new_time) bfo_val = calculateBFO_at_time(speed_mps, heading, lat2, lon2, current_alt_m, 0.0, new_time) idx, meas_time_str, meas_bfo, meas_bto = nearest_measured_for_time(new_time) delta_bto = meas_bto - bto_val delta_bfo = meas_bfo - bfo_val print() print(f'New coordinates: {lat2:.6f}, {lon2:.6f} | Calculated BTO: {bto_val:.3f} (Δ vs nearest@{meas_time_str}: {delta_bto:.3f}) | Calculated BFO: {bfo_val:.1f} (Δ vs nearest@{meas_time_str}: {delta_bfo:.1f}) | Distance travelled: {distance_m / 1000.0:.2f} km | Timing (UTC): {new_time.isoformat()}') print() path_points.append((lat2, lon2, new_time.isoformat())) print('Would you like to:') print('A) Output the flight path generated so far') print('B) Input a new heading and ground speed for calculating the flight path to handshake no. 3') print('C) Restart calculations from an earlier handshake (Handshake 1 to 2)') print('D) Proceed to calculate a flight path to handshake no. 4') print('E) Manually input coordinates and speed for BTO & BFO calculations at handshake 3') print('F) Exit the program') selection = input('Selection: ').strip().upper() # --- Menu actions (dispatch table) --- advance_after_menu = True # if False, do not overwrite current_* with the just-computed leg def action_A(): print('\nFlight path so far (index: lat, lon, utc):') for i, (la, lo, ts) in enumerate(path_points): print(f'{i:02d}: {la:.6f}, {lo:.6f}, {ts}') def action_B(): # Placeholder: keep behavior as a no-op (you can wire this to a heading/speed editor later). pass def action_C(): idx_str = input(f'Restart from index (0..{len(path_points) - 1}): ').strip() try: i_idx = int(idx_str) if 0 <= i_idx < len(path_points): la, lo, ts = path_points[i_idx] return ('restart', i_idx, la, lo, ts) else: print('Index out of range.') except Exception as e: print('Invalid index.', e) return None def action_D(): return 'continue' def action_E(): try: la = float(input('Latitude (deg): ')) lo = float(input('Longitude (deg): ')) spd_kn = float(input('Speed (knots): ')) hdg2 = float(input('Heading (deg): ')) t_str = input('UTC time (YYYY-MM-DDTHH:MM:SSZ, blank = nearest handshake to current time): ').strip() if t_str: if t_str.endswith('Z'): t_str = t_str[:-1] + '+00:00' t_utc = datetime.fromisoformat(t_str) else: nearest_idx, meas_ts, *_ = nearest_measured_for_time(current_time) t_utc = SAT_EPOCH_UTC + timedelta(seconds=_sat_time_seconds[nearest_idx]) spd_mps = spd_kn * 0.514444 bto_v = calculateBTO_at_time(la, lo, current_alt_m, t_utc) bfo_v = calculateBFO_at_time(spd_mps, hdg2, la, lo, current_alt_m, 0.0, t_utc) idxn, m_ts, m_bfo, m_bto = nearest_measured_for_time(t_utc) print(f'Manual point BTO: {bto_v:.3f} (meas@{m_ts}: {m_bto}, Δ: {m_bto - bto_v:.3f})') print(f'Manual point BFO: {bfo_v:.1f} (meas@{m_ts}: {m_bfo}, Δ: {m_bfo - bfo_v:.1f})') except Exception as e: print('Bad manual input.', e) def action_F(): return 'exit' actions = { 'A': action_A, 'B': action_B, 'C': action_C, 'D': action_D, 'E': action_E, 'F': action_F, } action = actions.get(selection) if action is None: # Unknown / blank input: do nothing. pass else: result = action() if result == 'exit': print('Exiting.') break if result == 'continue': current_lat, current_lon, current_time = (lat2, lon2, new_time) continue if isinstance(result, tuple) and result and result[0] == 'restart': _, i_idx, la, lo, ts = result current_lat, current_lon = (la, lo) current_time = datetime.fromisoformat(ts.replace('Z', '+00:00')) path_points = path_points[:i_idx + 1] print(f'Restarted from index {i_idx}.') advance_after_menu = False if advance_after_menu: current_lat, current_lon, current_time = (lat2, lon2, new_time) if __name__ == '__main__': repl_fly_from_radar_fix() def satloc_labels(): return [_extract_time_string(r[0]) for r in satLoc]