SpaceOS flight simulator with ISS orbit and telemetry
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** P0 flight simulator — minimal init process for the flight partition.
7
8 1. Mount devtmpfs, proc, sysfs 2. Open /dev/virtio-ports/ipc for writing 3.
9 Propagate ISS orbit via SGP4 and stream continuous telemetry *)
10
11let banner =
12 {|
13 ____ ___
14| _ \ / _ \
15| |_) | | | |
16| __/| |_| |
17|_| \___/ Flight Simulator
18|}
19
20let pr fmt = Fmt.pr (fmt ^^ "@.")
21let ipc_name = "ipc"
22
23let write_frame port (frame : Space_wire.Msg.t) =
24 let buf = Bytes.make Space_wire.Msg.frame_size '\x00' in
25 Wire.Codec.encode Space_wire.Msg.codec frame buf 0;
26 Zephyr.Virtio_port.write_all port (Bytes.unsafe_to_string buf)
27
28let send port kind ~apid payload =
29 pr "Sending %a: %s" Space_wire.Msg.pp_kind kind payload;
30 let frame = Space_wire.Msg.v kind ~apid payload in
31 write_frame port frame
32
33(* ISS TLE (epoch 2024) — good enough for a continuous demo. *)
34let iss_tle =
35 {|ISS (ZARYA)
361 25544U 98067A 24001.00000000 .00016717 00000-0 10270-3 0 9003
372 25544 51.6400 100.0000 0007417 50.0000 310.1280 15.49560000000017|}
38
39let rand_float lo hi = lo +. Random.float (hi -. lo)
40let rand_pick a = a.(Random.int (Array.length a))
41
42(* Simulate slowly-varying onboard sensors *)
43let temp = ref 22.0
44let voltage = ref 3.3
45let cpu = ref 15
46
47let 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
55let send_housekeeping port seq =
56 drift_sensor ();
57 match seq mod 5 with
58 | 0 -> send port TM ~apid:0x100 "heartbeat:ok"
59 | 1 ->
60 send port TM ~apid:0x102
61 (Fmt.str "sensor:temp=%.1f,voltage=%.2f" !temp !voltage)
62 | 2 ->
63 send port HEALTH ~apid:0x100
64 (Fmt.str "cpu=%d%%,mem=%dMB,uptime=%d" !cpu
65 (48 + Random.int 20)
66 (seq * 6))
67 | 3 ->
68 let evt =
69 rand_pick
70 [|
71 "ORBIT_UPDATE:epoch";
72 "CMD_ACK:0x42";
73 "BEACON_SENT";
74 "WATCHDOG_PET";
75 "SENSOR_CAL:temp:done";
76 |]
77 in
78 send port EVR ~apid:0x200 evt
79 | _ ->
80 let mode = rand_pick [| "nominal"; "nominal"; "nominal"; "safe" |] in
81 send port TM ~apid:0x103 (Fmt.str "status:%s" mode)
82
83let run_sim port =
84 let tle =
85 match Sgp4.parse_tle_string iss_tle with
86 | Ok t -> t
87 | Error e -> Fmt.failwith "TLE parse: %a" Sgp4.pp_error e
88 in
89 let state =
90 match Sgp4.init tle with
91 | Ok s -> s
92 | Error e -> Fmt.failwith "SGP4 init: %a" Sgp4.pp_error e
93 in
94 let gsto = Sgp4.gsto state in
95 pr "SGP4 initialized: %s (epoch=%.1f)" tle.name (Sgp4.epoch_unix tle);
96 (* Initial boot burst *)
97 send port TM ~apid:0x100 "heartbeat:ok";
98 Unix.sleepf 0.2;
99 send port EVR ~apid:0x200 "IPC_LINK_UP:p1";
100 Unix.sleepf 0.2;
101 send port EVR ~apid:0x200 "SGP4_INIT:ok";
102 Unix.sleepf 0.2;
103 (* Continuous telemetry loop — 1 Hz orbit + mixed housekeeping *)
104 pr "Entering continuous telemetry loop...";
105 let seq = ref 0 in
106 while true do
107 let tsince = float_of_int !seq *. 0.1 in
108 (* 0.1 min = 6s per tick *)
109 incr seq;
110 (* Orbit propagation *)
111 (match Sgp4.propagate state tle tsince with
112 | Ok (pos, vel) ->
113 let speed =
114 sqrt ((vel.vx *. vel.vx) +. (vel.vy *. vel.vy) +. (vel.vz *. vel.vz))
115 in
116 let alt =
117 sqrt ((pos.x *. pos.x) +. (pos.y *. pos.y) +. (pos.z *. pos.z))
118 -. 6378.135
119 in
120 let lat, lon, _alt_geo = Sgp4.to_geodetic pos gsto tsince in
121 send port TM ~apid:0x101
122 (Fmt.str "orbit:lat=%.2f,lon=%.2f,alt=%.1f,v=%.2f" lat lon alt speed)
123 | Error _ -> send port ERROR ~apid:0x1FF "sgp4:propagation_error");
124 Unix.sleepf 0.3;
125 send_housekeeping port !seq;
126 Unix.sleepf 0.7
127 done
128
129let () =
130 Random.self_init ();
131 Eio_main.run @@ fun env ->
132 Eio.Switch.run @@ fun _sw ->
133 let fs = Eio.Stdenv.fs env in
134 let pid = Unix.getpid () in
135 Fmt.pr "%s@." banner;
136 pr "P0 Flight Simulator (PID %d)" pid;
137 if pid = 1 then begin
138 pr "Setting up filesystems...";
139 Pid1.setup_filesystems ~fs;
140 Pid1.setup_signals ();
141 pr "Waiting for IPC device...";
142 if not (Zephyr.Virtio_port.wait ipc_name ~timeout:5.0) then begin
143 pr "IPC device not found after 5s, powering off.";
144 Pid1.poweroff ()
145 end;
146 let port = Zephyr.Virtio_port.open_write ipc_name in
147 Fun.protect
148 ~finally:(fun () -> Zephyr.Virtio_port.close port)
149 (fun () -> run_sim port)
150 end
151 else begin
152 pr "Not running as PID 1 (demo mode)";
153 pr "";
154 pr "In a real VM, this would:";
155 pr " 1. Mount /proc, /sys, /dev";
156 pr " 2. Open /dev/virtio-ports/ipc";
157 pr " 3. Propagate ISS orbit via SGP4";
158 pr " 4. Stream continuous TM, HEALTH, EVR frames"
159 end