CCSDS AOS (Advanced Orbiting Systems) Transfer Frame for satellite downlinks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add CCSDS space protocol libraries: aos, clcw, uslp

New libraries for CCSDS (Consultative Committee for Space Data Systems)
protocols:

- ocaml-aos: AOS (Advanced Orbiting Systems) Transfer Frame (CCSDS 732.0-B-4)
- ocaml-clcw: CLCW (Communications Link Control Word) for COP-1
- ocaml-uslp: USLP (Unified Space Link Protocol) Transfer Frame

All use ocamlformat 0.28.1.

+825
+5
.gitignore
··· 1 + _build/ 2 + *.install 3 + .merlin 4 + *.opam.locked 5 + _opam/
+1
.ocamlformat
··· 1 + version = 0.28.1
+21
LICENSE.md
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Thomas Gazagnaire 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+63
README.md
··· 1 + # ocaml-aos 2 + 3 + CCSDS AOS (Advanced Orbiting Systems) Transfer Frame parser and encoder. 4 + 5 + AOS frames are defined in [CCSDS 732.0-B-4](https://public.ccsds.org/Pubs/732x0b4.pdf) 6 + and used for high-rate downlinks from satellites. 7 + 8 + ## Installation 9 + 10 + ``` 11 + opam install aos 12 + ``` 13 + 14 + ## Usage 15 + 16 + ```ocaml 17 + (* Decode an AOS frame *) 18 + let () = 19 + let buf = (* ... frame bytes ... *) in 20 + match Aos.decode buf with 21 + | Error e -> Printf.printf "Error: %a\n" Aos.pp_error e 22 + | Ok frame -> 23 + Printf.printf "SCID: %d, VCID: %d, Data: %d bytes\n" 24 + (Aos.scid_to_int frame.header.scid) 25 + (Aos.vcid_to_int frame.header.vcid) 26 + (String.length frame.data) 27 + 28 + (* Create and encode an AOS frame *) 29 + let () = 30 + let scid = Aos.scid_exn 42 in 31 + let vcid = Aos.vcid_exn 5 in 32 + let frame = Aos.v ~scid ~vcid ~vcfc:12345 "payload data" in 33 + let encoded = Aos.encode frame in 34 + Printf.printf "Encoded: %d bytes\n" (String.length encoded) 35 + 36 + (* Create a frame with CLCW *) 37 + let () = 38 + let scid = Aos.scid_exn 42 in 39 + let vcid = Aos.vcid_exn 5 in 40 + let clcw_vcid = Clcw.vcid_exn 5 in 41 + let clcw = Clcw.v ~vcid:clcw_vcid ~report_value:100 () in 42 + let frame = Aos.with_clcw ~scid ~vcid ~vcfc:1 ~clcw "data" in 43 + let encoded = Aos.encode frame in 44 + Printf.printf "Frame with CLCW: %d bytes\n" (String.length encoded) 45 + ``` 46 + 47 + ## Frame Format 48 + 49 + ``` 50 + Primary header (6 bytes): 51 + Byte 0-1: TFVN(2b) | SCID(8b) | VCID(6b) 52 + Byte 2-4: VC Frame Count (24b) 53 + Byte 5: RF(1b) | SF(1b) | spare(2b) | VFCC(4b) 54 + 55 + Optional insert zone (variable) 56 + Data field (variable) 57 + Optional OCF (4 bytes) - Contains CLCW 58 + Optional FECF (2 bytes) - CRC-16-CCITT 59 + ``` 60 + 61 + ## License 62 + 63 + MIT
+31
aos.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "CCSDS AOS Transfer Frames (CCSDS 732.0-B-4)" 4 + description: 5 + "Parser and encoder for CCSDS Advanced Orbiting Systems (AOS) Transfer Frames. Supports 6-byte primary header, insert zone, data field, Operational Control Field (OCF) with CLCW, and Frame Error Control Field (FECF) with CRC-16-CCITT." 6 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 7 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 8 + license: "MIT" 9 + depends: [ 10 + "dune" {>= "3.0"} 11 + "ocaml" {>= "4.14"} 12 + "clcw" {>= "0.1"} 13 + "alcotest" {with-test} 14 + "crowbar" {with-test} 15 + "odoc" {with-doc} 16 + ] 17 + build: [ 18 + ["dune" "subst"] {dev} 19 + [ 20 + "dune" 21 + "build" 22 + "-p" 23 + name 24 + "-j" 25 + jobs 26 + "@install" 27 + "@runtest" {with-test} 28 + "@doc" {with-doc} 29 + ] 30 + ] 31 + dev-repo: "https://tangled.org/gazagnaire.org/ocaml-aos"
+26
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name aos) 4 + 5 + (generate_opam_files true) 6 + 7 + (license MIT) 8 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 9 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 10 + 11 + (source 12 + (uri https://tangled.org/gazagnaire.org/ocaml-aos)) 13 + 14 + (package 15 + (name aos) 16 + (synopsis "CCSDS AOS Transfer Frames (CCSDS 732.0-B-4)") 17 + (description 18 + "Parser and encoder for CCSDS Advanced Orbiting Systems (AOS) Transfer \ 19 + Frames. Supports 6-byte primary header, insert zone, data field, \ 20 + Operational Control Field (OCF) with CLCW, and Frame Error Control Field \ 21 + (FECF) with CRC-16-CCITT.") 22 + (depends 23 + (ocaml (>= 4.14)) 24 + (clcw (>= 0.1)) 25 + (alcotest :with-test) 26 + (crowbar :with-test)))
+3
fuzz/dune
··· 1 + (test 2 + (name fuzz_aos) 3 + (libraries aos crowbar))
+24
fuzz/fuzz_aos.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + open Crowbar 7 + 8 + let () = 9 + add_test ~name:"aos roundtrip" 10 + [ range 256; range 64; range 0x1000000; bytes ] 11 + (fun scid_val vcid_val vcfc data -> 12 + match (Aos.scid scid_val, Aos.vcid vcid_val) with 13 + | Some scid, Some vcid -> ( 14 + let frame = Aos.v ~scid ~vcid ~vcfc data in 15 + let encoded = Aos.encode frame in 16 + match Aos.decode encoded with 17 + | Error e -> fail (Fmt.str "decode failed: %a" Aos.pp_error e) 18 + | Ok frame' -> check_eq ~pp:Aos.pp ~eq:Aos.equal frame frame') 19 + | _ -> ()) 20 + 21 + let () = 22 + add_test ~name:"aos decode random bytes" [ bytes ] (fun buf -> 23 + (* Just check decode doesn't crash on random input *) 24 + ignore (Aos.decode buf))
+379
lib/aos.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** CCSDS AOS (Advanced Orbiting Systems) Transfer Frame (CCSDS 732.0-B-4). 7 + 8 + {b Primary header (6 bytes)} 9 + {v 10 + Byte 0-1: TFVN(2b) | SCID(8b) | VCID(6b) 11 + Byte 2-4: VC Frame Count (24b) 12 + Byte 5: RF(1b) | SF(1b) | spare(2b) | VFCC(4b) 13 + v} 14 + 15 + {b Invariants} 16 + - TFVN must be 0 or 1 17 + - SCID is 8 bits (0-255) 18 + - VCID is 6 bits (0-63), with 63 reserved for idle frames 19 + - VCFC is 24 bits (0-16,777,215) *) 20 + 21 + (* {1 Security limits} *) 22 + 23 + let max_frame_len = 2048 24 + 25 + (* {1 Spacecraft ID: 8 bits for AOS, 0-255} *) 26 + 27 + type scid = int 28 + 29 + let scid n = if n >= 0 && n <= 255 then Some n else None 30 + 31 + let scid_exn n = 32 + if n >= 0 && n <= 255 then n 33 + else invalid_arg (Printf.sprintf "scid: %d out of range 0-255" n) 34 + 35 + let scid_to_int s = s 36 + 37 + (* {1 Virtual Channel ID: 6 bits for AOS, 0-63} *) 38 + 39 + type vcid = int 40 + 41 + let vcid n = if n >= 0 && n <= 63 then Some n else None 42 + 43 + let vcid_exn n = 44 + if n >= 0 && n <= 63 then n 45 + else invalid_arg (Printf.sprintf "vcid: %d out of range 0-63" n) 46 + 47 + let vcid_to_int v = v 48 + 49 + (* {1 Constants} *) 50 + 51 + let header_len = 6 52 + let ocf_len = 4 53 + let fecf_len = 2 54 + let idle_vcid = 63 55 + 56 + (* {1 Types} *) 57 + 58 + type header = { 59 + version : int; 60 + scid : scid; 61 + vcid : vcid; 62 + vcfc : int; 63 + replay_flag : bool; 64 + vc_count_flag : bool; 65 + spare : int; 66 + vc_count_cycle : int; 67 + } 68 + 69 + type t = { 70 + header : header; 71 + insert_zone : string option; 72 + data : string; 73 + ocf : int option; 74 + fecf : int option; 75 + } 76 + 77 + type error = 78 + | Truncated of { need : int; have : int } 79 + | Invalid_version of int 80 + | Invalid_scid of int 81 + | Invalid_vcid of int 82 + | Fecf_mismatch of { expected : int; actual : int } 83 + 84 + let pp_error ppf = function 85 + | Truncated { need; have } -> 86 + Format.fprintf ppf "Truncated: need %d bytes, have %d" need have 87 + | Invalid_version v -> 88 + Format.fprintf ppf "Invalid version: %d (expected 0 or 1)" v 89 + | Invalid_scid s -> Format.fprintf ppf "Invalid SCID: %d" s 90 + | Invalid_vcid v -> Format.fprintf ppf "Invalid VCID: %d" v 91 + | Fecf_mismatch { expected; actual } -> 92 + Format.fprintf ppf "FECF mismatch: expected 0x%04X, got 0x%04X" expected 93 + actual 94 + 95 + (* {1 Binary helpers} *) 96 + 97 + let get_u8 s i = Char.code (String.get s i) 98 + 99 + let get_u16_be s i = 100 + let b0 = get_u8 s i in 101 + let b1 = get_u8 s (i + 1) in 102 + (b0 lsl 8) lor b1 103 + 104 + let get_u24_be s i = 105 + let b0 = get_u8 s i in 106 + let b1 = get_u8 s (i + 1) in 107 + let b2 = get_u8 s (i + 2) in 108 + (b0 lsl 16) lor (b1 lsl 8) lor b2 109 + 110 + let get_u32_be s i = 111 + let b0 = get_u8 s i in 112 + let b1 = get_u8 s (i + 1) in 113 + let b2 = get_u8 s (i + 2) in 114 + let b3 = get_u8 s (i + 3) in 115 + (b0 lsl 24) lor (b1 lsl 16) lor (b2 lsl 8) lor b3 116 + 117 + let set_u8 b i v = Bytes.set b i (Char.chr (v land 0xFF)) 118 + 119 + let set_u16_be b i v = 120 + set_u8 b i (v lsr 8); 121 + set_u8 b (i + 1) v 122 + 123 + let set_u24_be b i v = 124 + set_u8 b i (v lsr 16); 125 + set_u8 b (i + 1) (v lsr 8); 126 + set_u8 b (i + 2) v 127 + 128 + let set_u32_be b i v = 129 + set_u8 b i (v lsr 24); 130 + set_u8 b (i + 1) (v lsr 16); 131 + set_u8 b (i + 2) (v lsr 8); 132 + set_u8 b (i + 3) v 133 + 134 + (* {1 CRC-16-CCITT} *) 135 + 136 + let crc16_ccitt_table = 137 + let table = Array.make 256 0 in 138 + for i = 0 to 255 do 139 + let crc = ref (i lsl 8) in 140 + for _ = 0 to 7 do 141 + if !crc land 0x8000 <> 0 then crc := (!crc lsl 1) lxor 0x1021 142 + else crc := !crc lsl 1 143 + done; 144 + table.(i) <- !crc land 0xFFFF 145 + done; 146 + table 147 + 148 + let compute_fecf data = 149 + let crc = ref 0xFFFF in 150 + for i = 0 to String.length data - 1 do 151 + let byte = Char.code data.[i] in 152 + let idx = (!crc lsr 8) lxor byte land 0xFF in 153 + crc := (!crc lsl 8) lxor crc16_ccitt_table.(idx) land 0xFFFF 154 + done; 155 + !crc 156 + 157 + (* {1 Header decoding} *) 158 + 159 + let decode_header buf = 160 + let len = String.length buf in 161 + if len < header_len then Error (Truncated { need = header_len; have = len }) 162 + else 163 + let w0 = get_u16_be buf 0 in 164 + let version = (w0 lsr 14) land 0x3 in 165 + if version > 1 then Error (Invalid_version version) 166 + else 167 + let scid_val = (w0 lsr 6) land 0xFF in 168 + let vcid_val = w0 land 0x3F in 169 + let vcfc = get_u24_be buf 2 in 170 + let b5 = get_u8 buf 5 in 171 + let replay_flag = (b5 lsr 7) land 1 = 1 in 172 + let vc_count_flag = (b5 lsr 6) land 1 = 1 in 173 + let spare = (b5 lsr 4) land 0x3 in 174 + let vc_count_cycle = b5 land 0xF in 175 + Ok 176 + { 177 + version; 178 + scid = scid_val; 179 + vcid = vcid_val; 180 + vcfc; 181 + replay_flag; 182 + vc_count_flag; 183 + spare; 184 + vc_count_cycle; 185 + } 186 + 187 + (* {1 Frame decoding} *) 188 + 189 + let decode ?(frame_len = 0) ?(insert_zone_len = 0) ?(expect_ocf = true) 190 + ?(expect_fecf = true) ?(check_fecf = true) buf = 191 + let buf_len = String.length buf in 192 + match decode_header buf with 193 + | Error e -> Error e 194 + | Ok header -> 195 + let ocf_size = if expect_ocf then ocf_len else 0 in 196 + let fecf_size = if expect_fecf then fecf_len else 0 in 197 + let frame_len = if frame_len > 0 then frame_len else buf_len in 198 + if buf_len < frame_len then 199 + Error (Truncated { need = frame_len; have = buf_len }) 200 + else 201 + let data_len = 202 + frame_len - header_len - insert_zone_len - ocf_size - fecf_size 203 + in 204 + if data_len < 0 then 205 + Error 206 + (Truncated 207 + { 208 + need = header_len + insert_zone_len + ocf_size + fecf_size; 209 + have = frame_len; 210 + }) 211 + else 212 + let insert_zone = 213 + if insert_zone_len > 0 then 214 + Some (String.sub buf header_len insert_zone_len) 215 + else None 216 + in 217 + let data_off = header_len + insert_zone_len in 218 + let data = String.sub buf data_off data_len in 219 + let ocf_off = data_off + data_len in 220 + let ocf = 221 + if ocf_size > 0 then Some (get_u32_be buf ocf_off) else None 222 + in 223 + let fecf_off = ocf_off + ocf_size in 224 + if fecf_size > 0 then 225 + let fecf_val = get_u16_be buf fecf_off in 226 + if check_fecf then 227 + let expected = compute_fecf (String.sub buf 0 fecf_off) in 228 + if expected <> fecf_val then 229 + Error (Fecf_mismatch { expected; actual = fecf_val }) 230 + else Ok { header; insert_zone; data; ocf; fecf = Some fecf_val } 231 + else Ok { header; insert_zone; data; ocf; fecf = Some fecf_val } 232 + else Ok { header; insert_zone; data; ocf; fecf = None } 233 + 234 + (* {1 Frame encoding} *) 235 + 236 + let encode_header buf off hdr = 237 + (* Byte 0-1: version(2b) | scid(8b) | vcid(6b) *) 238 + let w0 = 239 + ((hdr.version land 0x3) lsl 14) 240 + lor ((scid_to_int hdr.scid land 0xFF) lsl 6) 241 + lor (vcid_to_int hdr.vcid land 0x3F) 242 + in 243 + set_u16_be buf off w0; 244 + (* Byte 2-4: vcfc (24b) *) 245 + set_u24_be buf (off + 2) (hdr.vcfc land 0xFFFFFF); 246 + (* Byte 5: rf(1b) | sf(1b) | spare(2b) | vfcc(4b) *) 247 + let b5 = 248 + ((if hdr.replay_flag then 1 else 0) lsl 7) 249 + lor ((if hdr.vc_count_flag then 1 else 0) lsl 6) 250 + lor ((hdr.spare land 0x3) lsl 4) 251 + lor (hdr.vc_count_cycle land 0xF) 252 + in 253 + set_u8 buf (off + 5) b5 254 + 255 + let encode ?(insert_zone_len = 0) ?(with_ocf = true) ?(with_fecf = true) frame = 256 + let ocf_size = if with_ocf then ocf_len else 0 in 257 + let fecf_size = if with_fecf then fecf_len else 0 in 258 + let iz_len = insert_zone_len in 259 + let data_len = String.length frame.data in 260 + let total_len = header_len + iz_len + data_len + ocf_size + fecf_size in 261 + let buf = Bytes.make total_len '\000' in 262 + (* Header *) 263 + encode_header buf 0 frame.header; 264 + (* Insert Zone *) 265 + (match frame.insert_zone with 266 + | Some iz when iz_len > 0 -> 267 + let copy_len = min (String.length iz) iz_len in 268 + Bytes.blit_string iz 0 buf header_len copy_len 269 + | _ -> ()); 270 + (* Data *) 271 + Bytes.blit_string frame.data 0 buf (header_len + iz_len) data_len; 272 + (* OCF *) 273 + (if with_ocf then 274 + let ocf_off = header_len + iz_len + data_len in 275 + match frame.ocf with Some ocf -> set_u32_be buf ocf_off ocf | None -> ()); 276 + (* FECF *) 277 + if with_fecf then begin 278 + let fecf_off = header_len + iz_len + data_len + ocf_size in 279 + let crc = compute_fecf (Bytes.sub_string buf 0 fecf_off) in 280 + set_u16_be buf fecf_off crc 281 + end; 282 + Bytes.to_string buf 283 + 284 + let encoded_len ?(insert_zone_len = 0) ?(with_ocf = true) ?(with_fecf = true) 285 + frame = 286 + let ocf_size = if with_ocf then ocf_len else 0 in 287 + let fecf_size = if with_fecf then fecf_len else 0 in 288 + header_len + insert_zone_len + String.length frame.data + ocf_size + fecf_size 289 + 290 + (* {1 Predicates} *) 291 + 292 + let is_idle frame = vcid_to_int frame.header.vcid = idle_vcid 293 + 294 + (* {1 Pretty-printing} *) 295 + 296 + let pp_header ppf hdr = 297 + Format.fprintf ppf 298 + "@[<hv 2>{ version=%d;@ scid=%d;@ vcid=%d;@ vcfc=%d;@ rf=%b;@ sf=%b }@]" 299 + hdr.version hdr.scid hdr.vcid hdr.vcfc hdr.replay_flag hdr.vc_count_flag 300 + 301 + let pp ppf frame = 302 + Format.fprintf ppf "@[<v 2>AOS_frame %a@ data[%d bytes]%a%a%a@]" pp_header 303 + frame.header (String.length frame.data) 304 + (fun ppf -> function 305 + | Some iz -> 306 + Format.fprintf ppf "@ insert_zone[%d bytes]" (String.length iz) 307 + | None -> ()) 308 + frame.insert_zone 309 + (fun ppf -> function 310 + | Some ocf -> Format.fprintf ppf "@ ocf=0x%08X" ocf | None -> ()) 311 + frame.ocf 312 + (fun ppf -> function 313 + | Some f -> Format.fprintf ppf "@ fecf=0x%04X" f | None -> ()) 314 + frame.fecf 315 + 316 + let equal_header a b = 317 + a.version = b.version && a.scid = b.scid && a.vcid = b.vcid && a.vcfc = b.vcfc 318 + && a.replay_flag = b.replay_flag 319 + && a.vc_count_flag = b.vc_count_flag 320 + && a.spare = b.spare 321 + && a.vc_count_cycle = b.vc_count_cycle 322 + 323 + let equal a b = 324 + let ocf_equal o1 o2 = 325 + match (o1, o2) with 326 + | None, None -> true 327 + | Some x, Some y -> x = y 328 + | None, Some 0 | Some 0, None -> true 329 + | _ -> false 330 + in 331 + equal_header a.header b.header 332 + && a.insert_zone = b.insert_zone 333 + && a.data = b.data && ocf_equal a.ocf b.ocf 334 + 335 + (* {1 Constructors} *) 336 + 337 + let v ?(version = 1) ?(replay_flag = false) ?(vc_count_flag = false) 338 + ?(vc_count_cycle = 0) ?(insert_zone = None) ?(ocf = None) ?(fecf = None) 339 + ~scid ~vcid ~vcfc data = 340 + let header = 341 + { 342 + version; 343 + scid; 344 + vcid; 345 + vcfc = vcfc land 0xFFFFFF; 346 + replay_flag; 347 + vc_count_flag; 348 + spare = 0; 349 + vc_count_cycle = vc_count_cycle land 0xF; 350 + } 351 + in 352 + { header; insert_zone; data; ocf; fecf } 353 + 354 + (* {1 CLCW Integration} *) 355 + 356 + let get_clcw frame = 357 + match frame.ocf with None -> None | Some word -> Some (Clcw.decode word) 358 + 359 + let set_clcw frame clcw = 360 + let ocf = Some (Clcw.encode clcw) in 361 + { frame with ocf } 362 + 363 + let with_clcw ?(version = 1) ?(replay_flag = false) ?(vc_count_flag = false) 364 + ?(vc_count_cycle = 0) ?(insert_zone = None) ?(fecf = None) ~scid ~vcid ~vcfc 365 + ~clcw data = 366 + let ocf = Some (Clcw.encode clcw) in 367 + let header = 368 + { 369 + version; 370 + scid; 371 + vcid; 372 + vcfc = vcfc land 0xFFFFFF; 373 + replay_flag; 374 + vc_count_flag; 375 + spare = 0; 376 + vc_count_cycle = vc_count_cycle land 0xF; 377 + } 378 + in 379 + { header; insert_zone; data; ocf; fecf }
+164
lib/aos.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** CCSDS AOS (Advanced Orbiting Systems) Transfer Frame (CCSDS 732.0-B-4). 7 + 8 + AOS frames are used for high-rate downlinks from satellites. They feature a 9 + 6-byte primary header, optional insert zone, variable-length data field, 10 + optional OCF (Operational Control Field with CLCW), and optional FECF (Frame 11 + Error Control Field with CRC-16-CCITT). *) 12 + 13 + (** {1 Constants} *) 14 + 15 + val header_len : int 16 + (** Primary header length (6 bytes). *) 17 + 18 + val ocf_len : int 19 + (** OCF length (4 bytes). *) 20 + 21 + val fecf_len : int 22 + (** FECF length (2 bytes). *) 23 + 24 + val idle_vcid : int 25 + (** Reserved VCID for idle frames (63). *) 26 + 27 + val max_frame_len : int 28 + (** Maximum frame length for security limits. *) 29 + 30 + (** {1 Spacecraft ID} *) 31 + 32 + type scid = private int 33 + (** Spacecraft Identifier (8 bits, 0-255). *) 34 + 35 + val scid : int -> scid option 36 + val scid_exn : int -> scid 37 + val scid_to_int : scid -> int 38 + 39 + (** {1 Virtual Channel ID} *) 40 + 41 + type vcid = private int 42 + (** Virtual Channel Identifier (6 bits, 0-63). *) 43 + 44 + val vcid : int -> vcid option 45 + val vcid_exn : int -> vcid 46 + val vcid_to_int : vcid -> int 47 + 48 + (** {1 Types} *) 49 + 50 + type header = { 51 + version : int; 52 + scid : scid; 53 + vcid : vcid; 54 + vcfc : int; (** Virtual Channel Frame Count (24 bits). *) 55 + replay_flag : bool; (** Replay flag. *) 56 + vc_count_flag : bool; (** VC count usage flag. *) 57 + spare : int; (** Spare bits. *) 58 + vc_count_cycle : int; (** VC count cycle (4 bits). *) 59 + } 60 + (** AOS frame primary header. *) 61 + 62 + type t = { 63 + header : header; 64 + insert_zone : string option; (** Optional insert zone data. *) 65 + data : string; (** Frame data field. *) 66 + ocf : int option; (** Operational Control Field (32 bits). *) 67 + fecf : int option; (** Frame Error Control Field (16 bits). *) 68 + } 69 + (** AOS Transfer Frame. *) 70 + 71 + val equal_header : header -> header -> bool 72 + val equal : t -> t -> bool 73 + val pp_header : Format.formatter -> header -> unit 74 + val pp : Format.formatter -> t -> unit 75 + 76 + (** {1 Errors} *) 77 + 78 + type error = 79 + | Truncated of { need : int; have : int } 80 + | Invalid_version of int 81 + | Invalid_scid of int 82 + | Invalid_vcid of int 83 + | Fecf_mismatch of { expected : int; actual : int } 84 + 85 + val pp_error : Format.formatter -> error -> unit 86 + 87 + (** {1 Encoding/Decoding} *) 88 + 89 + val decode : 90 + ?frame_len:int -> 91 + ?insert_zone_len:int -> 92 + ?expect_ocf:bool -> 93 + ?expect_fecf:bool -> 94 + ?check_fecf:bool -> 95 + string -> 96 + (t, error) result 97 + (** [decode buf] decodes an AOS frame from [buf]. 98 + @param frame_len Expected frame length (default: buffer length) 99 + @param insert_zone_len Insert zone length (default: 0) 100 + @param expect_ocf Whether OCF is present (default: true) 101 + @param expect_fecf Whether FECF is present (default: true) 102 + @param check_fecf Whether to verify FECF (default: true) *) 103 + 104 + val encode : 105 + ?insert_zone_len:int -> ?with_ocf:bool -> ?with_fecf:bool -> t -> string 106 + (** [encode frame] encodes an AOS frame to a string. 107 + @param insert_zone_len Insert zone length to reserve (default: 0) 108 + @param with_ocf Whether to include OCF (default: true) 109 + @param with_fecf Whether to include FECF (default: true) *) 110 + 111 + val encoded_len : 112 + ?insert_zone_len:int -> ?with_ocf:bool -> ?with_fecf:bool -> t -> int 113 + (** [encoded_len frame] returns the encoded length of [frame]. *) 114 + 115 + (** {1 Predicates} *) 116 + 117 + val is_idle : t -> bool 118 + (** [is_idle frame] returns true if frame uses the idle VCID (63). *) 119 + 120 + (** {1 Constructors} *) 121 + 122 + val v : 123 + ?version:int -> 124 + ?replay_flag:bool -> 125 + ?vc_count_flag:bool -> 126 + ?vc_count_cycle:int -> 127 + ?insert_zone:string option -> 128 + ?ocf:int option -> 129 + ?fecf:int option -> 130 + scid:scid -> 131 + vcid:vcid -> 132 + vcfc:int -> 133 + string -> 134 + t 135 + (** [v ~scid ~vcid ~vcfc data] constructs an AOS frame. *) 136 + 137 + (** {1 CLCW Integration} *) 138 + 139 + val get_clcw : t -> (Clcw.t, Clcw.error) result option 140 + (** [get_clcw frame] extracts and decodes the CLCW from the OCF if present. *) 141 + 142 + val set_clcw : t -> Clcw.t -> t 143 + (** [set_clcw frame clcw] sets the OCF to the encoded CLCW. *) 144 + 145 + val with_clcw : 146 + ?version:int -> 147 + ?replay_flag:bool -> 148 + ?vc_count_flag:bool -> 149 + ?vc_count_cycle:int -> 150 + ?insert_zone:string option -> 151 + ?fecf:int option -> 152 + scid:scid -> 153 + vcid:vcid -> 154 + vcfc:int -> 155 + clcw:Clcw.t -> 156 + string -> 157 + t 158 + (** [with_clcw ~scid ~vcid ~vcfc ~clcw data] constructs an AOS frame with CLCW. 159 + *) 160 + 161 + (** {1 CRC} *) 162 + 163 + val compute_fecf : string -> int 164 + (** [compute_fecf data] computes CRC-16-CCITT over [data]. *)
+4
lib/dune
··· 1 + (library 2 + (name aos) 3 + (public_name aos) 4 + (libraries clcw))
+3
test/dune
··· 1 + (test 2 + (name test_aos) 3 + (libraries aos clcw alcotest))
+101
test/test_aos.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + let aos = Alcotest.testable Aos.pp Aos.equal 7 + 8 + let test_roundtrip () = 9 + let scid = Aos.scid_exn 42 in 10 + let vcid = Aos.vcid_exn 5 in 11 + let data = "Hello, AOS!" in 12 + let frame = Aos.v ~scid ~vcid ~vcfc:12345 data in 13 + let encoded = Aos.encode frame in 14 + match Aos.decode encoded with 15 + | Error e -> Alcotest.failf "decode failed: %a" Aos.pp_error e 16 + | Ok frame' -> Alcotest.(check aos) "roundtrip" frame frame' 17 + 18 + let test_roundtrip_no_ocf () = 19 + let scid = Aos.scid_exn 10 in 20 + let vcid = Aos.vcid_exn 0 in 21 + let data = "No OCF" in 22 + let frame = Aos.v ~scid ~vcid ~vcfc:0 ~ocf:None data in 23 + let encoded = Aos.encode ~with_ocf:false frame in 24 + match Aos.decode ~expect_ocf:false encoded with 25 + | Error e -> Alcotest.failf "decode failed: %a" Aos.pp_error e 26 + | Ok frame' -> Alcotest.(check aos) "roundtrip no ocf" frame frame' 27 + 28 + let test_roundtrip_no_fecf () = 29 + let scid = Aos.scid_exn 100 in 30 + let vcid = Aos.vcid_exn 10 in 31 + let data = "No FECF" in 32 + let frame = Aos.v ~scid ~vcid ~vcfc:999 data in 33 + let encoded = Aos.encode ~with_fecf:false frame in 34 + match Aos.decode ~expect_fecf:false encoded with 35 + | Error e -> Alcotest.failf "decode failed: %a" Aos.pp_error e 36 + | Ok frame' -> Alcotest.(check aos) "roundtrip no fecf" frame frame' 37 + 38 + let test_clcw_integration () = 39 + let scid = Aos.scid_exn 50 in 40 + let vcid = Aos.vcid_exn 1 in 41 + let clcw_vcid = Clcw.vcid_exn 1 in 42 + let clcw = Clcw.v ~vcid:clcw_vcid ~report_value:42 () in 43 + let frame = Aos.with_clcw ~scid ~vcid ~vcfc:100 ~clcw "CLCW test" in 44 + let encoded = Aos.encode frame in 45 + match Aos.decode encoded with 46 + | Error e -> Alcotest.failf "decode failed: %a" Aos.pp_error e 47 + | Ok frame' -> ( 48 + match Aos.get_clcw frame' with 49 + | None -> Alcotest.fail "no CLCW" 50 + | Some (Error e) -> 51 + Alcotest.failf "CLCW decode failed: %a" Clcw.pp_error e 52 + | Some (Ok clcw') -> 53 + Alcotest.(check int) "report_value" 42 clcw'.report_value) 54 + 55 + let test_idle_frame () = 56 + let scid = Aos.scid_exn 0 in 57 + let vcid = Aos.vcid_exn 63 in 58 + let frame = Aos.v ~scid ~vcid ~vcfc:0 "" in 59 + Alcotest.(check bool) "is_idle" true (Aos.is_idle frame) 60 + 61 + let test_fecf_check () = 62 + let scid = Aos.scid_exn 1 in 63 + let vcid = Aos.vcid_exn 1 in 64 + let frame = Aos.v ~scid ~vcid ~vcfc:1 "data" in 65 + let encoded = Aos.encode frame in 66 + (* Corrupt the FECF *) 67 + let corrupted = Bytes.of_string encoded in 68 + let len = Bytes.length corrupted in 69 + Bytes.set corrupted (len - 1) '\xFF'; 70 + match Aos.decode (Bytes.to_string corrupted) with 71 + | Error (Aos.Fecf_mismatch _) -> () 72 + | Error e -> Alcotest.failf "expected Fecf_mismatch, got: %a" Aos.pp_error e 73 + | Ok _ -> Alcotest.fail "expected error" 74 + 75 + let test_vcid_bounds () = 76 + Alcotest.(check bool) "vcid 0 valid" true (Option.is_some (Aos.vcid 0)); 77 + Alcotest.(check bool) "vcid 63 valid" true (Option.is_some (Aos.vcid 63)); 78 + Alcotest.(check bool) "vcid -1 invalid" true (Option.is_none (Aos.vcid (-1))); 79 + Alcotest.(check bool) "vcid 64 invalid" true (Option.is_none (Aos.vcid 64)) 80 + 81 + let test_scid_bounds () = 82 + Alcotest.(check bool) "scid 0 valid" true (Option.is_some (Aos.scid 0)); 83 + Alcotest.(check bool) "scid 255 valid" true (Option.is_some (Aos.scid 255)); 84 + Alcotest.(check bool) "scid -1 invalid" true (Option.is_none (Aos.scid (-1))); 85 + Alcotest.(check bool) "scid 256 invalid" true (Option.is_none (Aos.scid 256)) 86 + 87 + let () = 88 + Alcotest.run "aos" 89 + [ 90 + ( "aos", 91 + [ 92 + Alcotest.test_case "roundtrip" `Quick test_roundtrip; 93 + Alcotest.test_case "roundtrip_no_ocf" `Quick test_roundtrip_no_ocf; 94 + Alcotest.test_case "roundtrip_no_fecf" `Quick test_roundtrip_no_fecf; 95 + Alcotest.test_case "clcw_integration" `Quick test_clcw_integration; 96 + Alcotest.test_case "idle_frame" `Quick test_idle_frame; 97 + Alcotest.test_case "fecf_check" `Quick test_fecf_check; 98 + Alcotest.test_case "vcid_bounds" `Quick test_vcid_bounds; 99 + Alcotest.test_case "scid_bounds" `Quick test_scid_bounds; 100 + ] ); 101 + ]