SpaceOS flight simulator with ISS orbit and telemetry

feat(space-sim,sgp4): continuous SGP4 orbit telemetry

Replace the fixed 7-frame-then-poweroff flight sim with a continuous
loop that propagates ISS orbit via SGP4 and streams real lat/lon/alt/velocity
telemetry interleaved with drifting sensor readings, health, and EVR frames.

Expose gsto (Greenwich Sidereal Time at epoch) from Sgp4.state so
consumers can call to_geodetic without replicating the GMST computation.

+100 -21
+1 -1
bin/dune
··· 2 2 (name main) 3 3 (public_name space-sim) 4 4 (package space-sim) 5 - (libraries pid1 zephyr space-wire wire eio eio_main fmt)) 5 + (libraries pid1 zephyr space-wire wire eio eio_main fmt sgp4))
+99 -20
bin/main.ml
··· 6 6 (** P0 flight simulator — minimal init process for the flight partition. 7 7 8 8 1. Mount devtmpfs, proc, sysfs 2. Open /dev/virtio-ports/ipc for writing 3. 9 - Send TM, HEALTH, EVR and ERROR frames 4. Poweroff *) 9 + Propagate ISS orbit via SGP4 and stream continuous telemetry *) 10 10 11 11 let banner = 12 12 {| ··· 30 30 let frame = Space_wire.Msg.v kind ~apid payload in 31 31 write_frame port frame 32 32 33 + (* ISS TLE (epoch 2024) — good enough for a continuous demo. *) 34 + let iss_tle = 35 + {|ISS (ZARYA) 36 + 1 25544U 98067A 24001.00000000 .00016717 00000-0 10270-3 0 9003 37 + 2 25544 51.6400 100.0000 0007417 50.0000 310.1280 15.49560000000017|} 38 + 39 + let rand_float lo hi = lo +. Random.float (hi -. lo) 40 + let rand_pick a = a.(Random.int (Array.length a)) 41 + 42 + (* Simulate slowly-varying onboard sensors *) 43 + let temp = ref 22.0 44 + let voltage = ref 3.3 45 + let cpu = ref 15 46 + 47 + let drift_sensor () = 48 + temp := !temp +. rand_float (-0.3) 0.3; 49 + temp := Float.max 10.0 (Float.min 40.0 !temp); 50 + voltage := !voltage +. rand_float (-0.02) 0.02; 51 + voltage := Float.max 3.0 (Float.min 3.6 !voltage); 52 + cpu := !cpu + (Random.int 5 - 2); 53 + cpu := max 5 (min 95 !cpu) 54 + 55 + let run_sim port = 56 + let tle = 57 + match Sgp4.parse_tle_string iss_tle with 58 + | Ok t -> t 59 + | Error e -> failwith (Fmt.str "TLE parse: %a" Sgp4.pp_error e) 60 + in 61 + let state = 62 + match Sgp4.sgp4_init tle with 63 + | Ok s -> s 64 + | Error e -> failwith (Fmt.str "SGP4 init: %a" Sgp4.pp_error e) 65 + in 66 + let gsto = Sgp4.gsto state in 67 + pr "SGP4 initialized: %s (epoch=%.1f)" tle.name (Sgp4.epoch_unix tle); 68 + (* Initial boot burst *) 69 + send port TM ~apid:0x100 "heartbeat:ok"; 70 + Unix.sleepf 0.2; 71 + send port EVR ~apid:0x200 "IPC_LINK_UP:p1"; 72 + Unix.sleepf 0.2; 73 + send port EVR ~apid:0x200 "SGP4_INIT:ok"; 74 + Unix.sleepf 0.2; 75 + (* Continuous telemetry loop — 1 Hz orbit + mixed housekeeping *) 76 + pr "Entering continuous telemetry loop..."; 77 + let seq = ref 0 in 78 + while true do 79 + let tsince = float_of_int !seq *. 0.1 in 80 + (* 0.1 min = 6s per tick *) 81 + incr seq; 82 + (* Orbit propagation *) 83 + (match Sgp4.propagate state tle tsince with 84 + | Ok (pos, vel) -> 85 + let speed = 86 + sqrt ((vel.vx *. vel.vx) +. (vel.vy *. vel.vy) +. (vel.vz *. vel.vz)) 87 + in 88 + let alt = 89 + sqrt ((pos.x *. pos.x) +. (pos.y *. pos.y) +. (pos.z *. pos.z)) 90 + -. 6378.135 91 + in 92 + let lat, lon, _alt_geo = Sgp4.to_geodetic pos gsto tsince in 93 + send port TM ~apid:0x101 94 + (Fmt.str "orbit:lat=%.2f,lon=%.2f,alt=%.1f,v=%.2f" lat lon alt speed) 95 + | Error _ -> send port ERROR ~apid:0x1FF "sgp4:propagation_error"); 96 + Unix.sleepf 0.3; 97 + (* Housekeeping — rotate through different frame types *) 98 + drift_sensor (); 99 + (match !seq mod 5 with 100 + | 0 -> send port TM ~apid:0x100 "heartbeat:ok" 101 + | 1 -> 102 + send port TM ~apid:0x102 103 + (Fmt.str "sensor:temp=%.1f,voltage=%.2f" !temp !voltage) 104 + | 2 -> 105 + send port HEALTH ~apid:0x100 106 + (Fmt.str "cpu=%d%%,mem=%dMB,uptime=%d" !cpu 107 + (48 + Random.int 20) 108 + (!seq * 6)) 109 + | 3 -> 110 + let evt = 111 + rand_pick 112 + [| 113 + "ORBIT_UPDATE:epoch"; 114 + "CMD_ACK:0x42"; 115 + "BEACON_SENT"; 116 + "WATCHDOG_PET"; 117 + "SENSOR_CAL:temp:done"; 118 + |] 119 + in 120 + send port EVR ~apid:0x200 evt 121 + | _ -> 122 + let mode = rand_pick [| "nominal"; "nominal"; "nominal"; "safe" |] in 123 + send port TM ~apid:0x103 (Fmt.str "status:%s" mode)); 124 + Unix.sleepf 0.7 125 + done 126 + 33 127 let () = 128 + Random.self_init (); 34 129 Eio_main.run @@ fun env -> 35 130 Eio.Switch.run @@ fun _sw -> 36 131 let fs = Eio.Stdenv.fs env in ··· 49 144 let port = Zephyr.Virtio_port.open_write ipc_name in 50 145 Fun.protect 51 146 ~finally:(fun () -> Zephyr.Virtio_port.close port) 52 - (fun () -> 53 - send port TM ~apid:0x100 "heartbeat:ok"; 54 - Unix.sleepf 0.5; 55 - send port TM ~apid:0x101 "sensor:temp=22.5"; 56 - Unix.sleepf 0.5; 57 - send port HEALTH ~apid:0x100 "cpu=12%,mem=48MB,uptime=3"; 58 - Unix.sleepf 0.5; 59 - send port EVR ~apid:0x200 "IPC_LINK_UP:p1"; 60 - Unix.sleepf 0.5; 61 - send port TM ~apid:0x102 "status:nominal"; 62 - Unix.sleepf 0.5; 63 - send port EVR ~apid:0x200 "SENSOR_CAL:temp:done"; 64 - Unix.sleepf 0.5; 65 - send port ERROR ~apid:0x1FF "watchdog:timeout:recovered"; 66 - Unix.sleepf 0.5); 67 - pr "Init complete. Powering off..."; 68 - Pid1.poweroff () 147 + (fun () -> run_sim port) 69 148 end 70 149 else begin 71 150 pr "Not running as PID 1 (demo mode)"; ··· 73 152 pr "In a real VM, this would:"; 74 153 pr " 1. Mount /proc, /sys, /dev"; 75 154 pr " 2. Open /dev/virtio-ports/ipc"; 76 - pr " 3. Send TM, HEALTH, EVR, and ERROR frames"; 77 - pr " 4. Poweroff" 155 + pr " 3. Propagate ISS orbit via SGP4"; 156 + pr " 4. Stream continuous TM, HEALTH, EVR frames" 78 157 end