CCSDS USLP (Unified Space Link Protocol) Transfer Frame- unified TM/TC/AOS
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.

+1062
+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.
+73
README.md
··· 1 + # ocaml-uslp 2 + 3 + CCSDS USLP (Unified Space Link Protocol) Transfer Frame parser and encoder. 4 + 5 + USLP frames are defined in [CCSDS 732.1-B-2](https://public.ccsds.org/Pubs/732x1b2.pdf) 6 + and unify TM, TC, and AOS protocols with additional features. 7 + 8 + ## Installation 9 + 10 + ``` 11 + opam install uslp 12 + ``` 13 + 14 + ## Usage 15 + 16 + ```ocaml 17 + (* Decode a USLP frame *) 18 + let () = 19 + let buf = (* ... frame bytes ... *) in 20 + match Uslp.decode buf with 21 + | Error e -> Printf.printf "Error: %a\n" Uslp.pp_error e 22 + | Ok frame -> 23 + Printf.printf "SCID: %d, VCID: %d, MAP: %d\n" 24 + (Uslp.scid_to_int frame.header.scid) 25 + (Uslp.vcid_to_int frame.header.vcid) 26 + (Uslp.map_id_to_int frame.header.map_id) 27 + 28 + (* Create and encode a USLP frame *) 29 + let () = 30 + let scid = Uslp.scid_exn 1000 in 31 + let vcid = Uslp.vcid_exn 5 in 32 + let map_id = Uslp.map_id_exn 3 in 33 + let frame = Uslp.v ~scid ~vcid ~map_id ~vcfc:12345 ~vcfc_len:2 "payload" in 34 + let encoded = Uslp.encode ~fecf:Uslp.Crc16 frame in 35 + Printf.printf "Encoded: %d bytes\n" (String.length encoded) 36 + 37 + (* Decode with VCFC and CRC-32 *) 38 + let () = 39 + let buf = (* ... *) in 40 + match Uslp.decode ~vcfc_len:2 ~expect_fecf:Uslp.Crc32 buf with 41 + | Error e -> Printf.printf "Error: %a\n" Uslp.pp_error e 42 + | Ok frame -> Printf.printf "VCFC: %d\n" frame.header.vcfc 43 + ``` 44 + 45 + ## Frame Format 46 + 47 + ``` 48 + Primary header (7+ bytes): 49 + Byte 0: TFVN(4b) | SCID[15:12](4b) 50 + Byte 1: SCID[11:4](8b) 51 + Byte 2: SCID[3:0](4b) | src_dest(1b) | VCID[5:3](3b) 52 + Byte 3: VCID[2:0](3b) | MAP_ID(4b) | EOFPH(1b) 53 + Byte 4-5: Frame Length - 1 (16b) 54 + Byte 6: Bypass(1b) | PCC(1b) | Rsvd(2b) | OCF(1b) | VCFC_len(3b) 55 + Byte 7+: VCFC (0-7 bytes based on vcfc_len) 56 + 57 + Optional insert zone (variable) 58 + Data field (variable) 59 + Optional OCF (4 bytes) - Contains CLCW 60 + Optional FECF (2 or 4 bytes) - CRC-16-CCITT or CRC-32 61 + ``` 62 + 63 + ## Features 64 + 65 + - Variable-length VCFC (0-7 bytes) 66 + - 16-bit SCID (vs 8-bit in AOS, 10-bit in TM) 67 + - MAP IDs for multiplexed access points 68 + - Source/Destination indicator 69 + - CRC-16-CCITT or CRC-32 FECF 70 + 71 + ## License 72 + 73 + MIT
+25
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name uslp) 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-uslp)) 13 + 14 + (package 15 + (name uslp) 16 + (synopsis "CCSDS USLP Transfer Frames (CCSDS 732.1-B-2)") 17 + (description 18 + "Parser and encoder for CCSDS Unified Space Link Protocol (USLP) Transfer \ 19 + Frames. Supports variable-length VCFC, MAP IDs, source/destination flags, \ 20 + optional OCF, and FECF with CRC-16 or CRC-32.") 21 + (depends 22 + (ocaml (>= 4.14)) 23 + (clcw (>= 0.1)) 24 + (alcotest :with-test) 25 + (crowbar :with-test)))
+3
fuzz/dune
··· 1 + (test 2 + (name fuzz_uslp) 3 + (libraries uslp crowbar))
+26
fuzz/fuzz_uslp.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:"uslp roundtrip" 10 + [ range 0x10000; range 64; range 16; range 0x10000; bytes ] 11 + (fun scid_val vcid_val map_id_val vcfc data -> 12 + match 13 + (Uslp.scid scid_val, Uslp.vcid vcid_val, Uslp.map_id map_id_val) 14 + with 15 + | Some scid, Some vcid, Some map_id -> ( 16 + let frame = Uslp.v ~scid ~vcid ~map_id ~vcfc ~vcfc_len:0 data in 17 + let encoded = Uslp.encode frame in 18 + match Uslp.decode encoded with 19 + | Error e -> fail (Fmt.str "decode failed: %a" Uslp.pp_error e) 20 + | Ok frame' -> check_eq ~pp:Uslp.pp ~eq:Uslp.equal frame frame') 21 + | _ -> ()) 22 + 23 + let () = 24 + add_test ~name:"uslp decode random bytes" [ bytes ] (fun buf -> 25 + (* Just check decode doesn't crash on random input *) 26 + ignore (Uslp.decode buf))
+4
lib/dune
··· 1 + (library 2 + (name uslp) 3 + (public_name uslp) 4 + (libraries clcw))
+541
lib/uslp.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** CCSDS USLP (Unified Space Data Link Protocol) Transfer Frame (CCSDS 7 + 732.1-B-2). 8 + 9 + {b Primary header layout} 10 + {v 11 + Byte 0: TFVN(4b) | SCID[15:12](4b) 12 + Byte 1: SCID[11:4](8b) 13 + Byte 2: SCID[3:0](4b) | src_dest(1b) | VCID[5:3](3b) 14 + Byte 3: VCID[2:0](3b) | MAP_ID(4b) | EOFPH(1b) 15 + Byte 4-5: Frame Length (16b) 16 + Byte 6: Bypass(1b) | PCC(1b) | Rsvd(2b) | OCF(1b) | VCFC_len(3b) 17 + Byte 7+: VCFC (0-7 bytes based on vcfc_len) 18 + v} 19 + 20 + {b Invariants} 21 + - TFVN must be 0xC (12) for USLP 22 + - SCID is 16 bits (0-65535) 23 + - VCID is 6 bits (0-63), with 63 reserved for idle frames 24 + - MAP_ID is 4 bits (0-15) 25 + - VCFC length is 0-7 bytes *) 26 + 27 + (* {1 Security limits} *) 28 + 29 + let max_frame_len = 65536 30 + 31 + (* {1 Constants} *) 32 + 33 + let min_header_len = 7 34 + let ocf_len = 4 35 + let tfvn_uslp = 0xC 36 + let idle_vcid = 63 37 + 38 + (* {1 Types} *) 39 + 40 + type scid = int 41 + type vcid = int 42 + type map_id = int 43 + 44 + let scid n = if n >= 0 && n <= 0xFFFF then Some n else None 45 + 46 + let scid_exn n = 47 + if n >= 0 && n <= 0xFFFF then n 48 + else invalid_arg (Printf.sprintf "scid: %d out of range 0-65535" n) 49 + 50 + let scid_to_int s = s 51 + let vcid n = if n >= 0 && n <= 63 then Some n else None 52 + 53 + let vcid_exn n = 54 + if n >= 0 && n <= 63 then n 55 + else invalid_arg (Printf.sprintf "vcid: %d out of range 0-63" n) 56 + 57 + let vcid_to_int v = v 58 + let map_id n = if n >= 0 && n <= 15 then Some n else None 59 + 60 + let map_id_exn n = 61 + if n >= 0 && n <= 15 then n 62 + else invalid_arg (Printf.sprintf "map_id: %d out of range 0-15" n) 63 + 64 + let map_id_to_int m = m 65 + 66 + type src_or_dest = Source | Dest 67 + type fecf_type = No_fecf | Crc16 | Crc32 68 + 69 + type header = { 70 + tfvn : int; 71 + scid : scid; 72 + src_or_dest : src_or_dest; 73 + vcid : vcid; 74 + map_id : map_id; 75 + eofph : bool; 76 + frame_len : int; 77 + bypass_flag : bool; 78 + prot_ctrl_cmd : bool; 79 + ocf_flag : bool; 80 + vcfc_len : int; 81 + vcfc : int; 82 + } 83 + 84 + type t = { 85 + header : header; 86 + insert_zone : string option; 87 + data : string; 88 + ocf : int option; 89 + fecf : int64 option; 90 + } 91 + 92 + type error = 93 + | Truncated of { need : int; have : int } 94 + | Invalid_version of int 95 + | Invalid_scid of int 96 + | Invalid_vcid of int 97 + | Invalid_map_id of int 98 + | Fecf_mismatch of { expected : int64; actual : int64 } 99 + | Invalid_vcfc_len of int 100 + 101 + let pp_error ppf = function 102 + | Truncated { need; have } -> 103 + Format.fprintf ppf "Truncated: need %d bytes, have %d" need have 104 + | Invalid_version v -> 105 + Format.fprintf ppf "Invalid version: %d (expected 12 for USLP)" v 106 + | Invalid_scid s -> Format.fprintf ppf "Invalid SCID: %d" s 107 + | Invalid_vcid v -> Format.fprintf ppf "Invalid VCID: %d" v 108 + | Invalid_map_id m -> Format.fprintf ppf "Invalid MAP_ID: %d" m 109 + | Fecf_mismatch { expected; actual } -> 110 + Format.fprintf ppf "FECF mismatch: expected 0x%Lx, got 0x%Lx" expected 111 + actual 112 + | Invalid_vcfc_len l -> Format.fprintf ppf "Invalid VCFC length: %d" l 113 + 114 + (* {1 Binary helpers} *) 115 + 116 + let get_u8 s i = Char.code (String.get s i) 117 + 118 + let get_u16_be s i = 119 + let b0 = get_u8 s i in 120 + let b1 = get_u8 s (i + 1) in 121 + (b0 lsl 8) lor b1 122 + 123 + let get_u32_be s i = 124 + let b0 = get_u8 s i in 125 + let b1 = get_u8 s (i + 1) in 126 + let b2 = get_u8 s (i + 2) in 127 + let b3 = get_u8 s (i + 3) in 128 + (b0 lsl 24) lor (b1 lsl 16) lor (b2 lsl 8) lor b3 129 + 130 + let get_var_uint_be s off len = 131 + let rec aux acc i = 132 + if i >= len then acc else aux ((acc lsl 8) lor get_u8 s (off + i)) (i + 1) 133 + in 134 + aux 0 0 135 + 136 + let set_u8 b i v = Bytes.set b i (Char.chr (v land 0xFF)) 137 + 138 + let set_u16_be b i v = 139 + set_u8 b i (v lsr 8); 140 + set_u8 b (i + 1) v 141 + 142 + let set_u32_be b i v = 143 + set_u8 b i (v lsr 24); 144 + set_u8 b (i + 1) (v lsr 16); 145 + set_u8 b (i + 2) (v lsr 8); 146 + set_u8 b (i + 3) v 147 + 148 + let set_var_uint_be b off len v = 149 + for i = 0 to len - 1 do 150 + set_u8 b (off + i) (v lsr (8 * (len - 1 - i))) 151 + done 152 + 153 + (* {1 CRC-16-CCITT} *) 154 + 155 + let crc16_ccitt_table = 156 + let table = Array.make 256 0 in 157 + for i = 0 to 255 do 158 + let crc = ref (i lsl 8) in 159 + for _ = 0 to 7 do 160 + if !crc land 0x8000 <> 0 then crc := (!crc lsl 1) lxor 0x1021 161 + else crc := !crc lsl 1 162 + done; 163 + table.(i) <- !crc land 0xFFFF 164 + done; 165 + table 166 + 167 + let compute_crc16 data = 168 + let crc = ref 0xFFFF in 169 + for i = 0 to String.length data - 1 do 170 + let byte = Char.code data.[i] in 171 + let idx = (!crc lsr 8) lxor byte land 0xFF in 172 + crc := (!crc lsl 8) lxor crc16_ccitt_table.(idx) land 0xFFFF 173 + done; 174 + !crc 175 + 176 + (* {1 CRC-32 (ISO 3309)} *) 177 + 178 + let crc32_table = 179 + let table = Array.make 256 0l in 180 + for i = 0 to 255 do 181 + let crc = ref (Int32.of_int i) in 182 + for _ = 0 to 7 do 183 + if Int32.logand !crc 1l <> 0l then 184 + crc := Int32.logxor (Int32.shift_right_logical !crc 1) 0xEDB88320l 185 + else crc := Int32.shift_right_logical !crc 1 186 + done; 187 + table.(i) <- !crc 188 + done; 189 + table 190 + 191 + let compute_crc32 data = 192 + let crc = ref 0xFFFFFFFFl in 193 + for i = 0 to String.length data - 1 do 194 + let byte = Char.code data.[i] in 195 + let idx = 196 + Int32.to_int (Int32.logand (Int32.logxor !crc (Int32.of_int byte)) 0xFFl) 197 + in 198 + crc := Int32.logxor (Int32.shift_right_logical !crc 8) crc32_table.(idx) 199 + done; 200 + Int32.to_int (Int32.logxor !crc 0xFFFFFFFFl) land 0xFFFFFFFF 201 + 202 + (* {1 Header decoding} *) 203 + 204 + let decode_header ~vcfc_len buf = 205 + let len = String.length buf in 206 + let hdr_len = min_header_len + vcfc_len in 207 + if len < hdr_len then Error (Truncated { need = hdr_len; have = len }) 208 + else 209 + let b0 = get_u8 buf 0 in 210 + let tfvn = (b0 lsr 4) land 0xF in 211 + if tfvn <> tfvn_uslp then Error (Invalid_version tfvn) 212 + else 213 + let scid_hi = b0 land 0xF in 214 + let b1 = get_u8 buf 1 in 215 + let b2 = get_u8 buf 2 in 216 + let scid_mid = b1 in 217 + let scid_lo = (b2 lsr 4) land 0xF in 218 + let scid_val = (scid_hi lsl 12) lor (scid_mid lsl 4) lor scid_lo in 219 + let src_or_dest = if (b2 lsr 3) land 1 = 0 then Source else Dest in 220 + let vcid_hi = b2 land 0x7 in 221 + let b3 = get_u8 buf 3 in 222 + let vcid_lo = (b3 lsr 5) land 0x7 in 223 + let vcid_val = (vcid_hi lsl 3) lor vcid_lo in 224 + if vcid_val > 63 then Error (Invalid_vcid vcid_val) 225 + else 226 + let map_id_val = (b3 lsr 1) land 0xF in 227 + if map_id_val > 15 then Error (Invalid_map_id map_id_val) 228 + else 229 + let eofph = b3 land 1 = 1 in 230 + let frame_len = get_u16_be buf 4 in 231 + let b6 = get_u8 buf 6 in 232 + let bypass_flag = (b6 lsr 7) land 1 = 1 in 233 + let prot_ctrl_cmd = (b6 lsr 6) land 1 = 1 in 234 + let ocf_flag = (b6 lsr 3) land 1 = 1 in 235 + let vcfc_len_field = b6 land 0x7 in 236 + let vcfc = 237 + if vcfc_len > 0 then get_var_uint_be buf 7 vcfc_len else 0 238 + in 239 + Ok 240 + { 241 + tfvn; 242 + scid = scid_val; 243 + src_or_dest; 244 + vcid = vcid_val; 245 + map_id = map_id_val; 246 + eofph; 247 + frame_len; 248 + bypass_flag; 249 + prot_ctrl_cmd; 250 + ocf_flag; 251 + vcfc_len = vcfc_len_field; 252 + vcfc; 253 + } 254 + 255 + (* {1 Frame decoding} *) 256 + 257 + let decode ?(vcfc_len = 0) ?(insert_zone_len = 0) ?(expect_ocf = false) 258 + ?(expect_fecf = No_fecf) ?(check_fecf = true) buf = 259 + let buf_len = String.length buf in 260 + match decode_header ~vcfc_len buf with 261 + | Error e -> Error e 262 + | Ok header -> ( 263 + let hdr_len = min_header_len + vcfc_len in 264 + let ocf_size = if expect_ocf then ocf_len else 0 in 265 + let fecf_size = 266 + match expect_fecf with No_fecf -> 0 | Crc16 -> 2 | Crc32 -> 4 267 + in 268 + (* Frame length field is total length - 1 *) 269 + let frame_len = header.frame_len + 1 in 270 + if buf_len < frame_len then 271 + Error (Truncated { need = frame_len; have = buf_len }) 272 + else 273 + let data_len = 274 + frame_len - hdr_len - insert_zone_len - ocf_size - fecf_size 275 + in 276 + if data_len < 0 then 277 + Error 278 + (Truncated 279 + { 280 + need = hdr_len + insert_zone_len + ocf_size + fecf_size; 281 + have = frame_len; 282 + }) 283 + else 284 + let insert_zone = 285 + if insert_zone_len > 0 then 286 + Some (String.sub buf hdr_len insert_zone_len) 287 + else None 288 + in 289 + let data_off = hdr_len + insert_zone_len in 290 + let data = String.sub buf data_off data_len in 291 + let ocf_off = data_off + data_len in 292 + let ocf = 293 + if ocf_size > 0 then Some (get_u32_be buf ocf_off) else None 294 + in 295 + let fecf_off = ocf_off + ocf_size in 296 + match expect_fecf with 297 + | No_fecf -> Ok { header; insert_zone; data; ocf; fecf = None } 298 + | Crc16 -> 299 + let fecf_val = get_u16_be buf fecf_off in 300 + if check_fecf then 301 + let expected = compute_crc16 (String.sub buf 0 fecf_off) in 302 + if expected <> fecf_val then 303 + Error 304 + (Fecf_mismatch 305 + { 306 + expected = Int64.of_int expected; 307 + actual = Int64.of_int fecf_val; 308 + }) 309 + else 310 + Ok 311 + { 312 + header; 313 + insert_zone; 314 + data; 315 + ocf; 316 + fecf = Some (Int64.of_int fecf_val); 317 + } 318 + else 319 + Ok 320 + { 321 + header; 322 + insert_zone; 323 + data; 324 + ocf; 325 + fecf = Some (Int64.of_int fecf_val); 326 + } 327 + | Crc32 -> 328 + let fecf_val = get_u32_be buf fecf_off in 329 + if check_fecf then 330 + let expected = compute_crc32 (String.sub buf 0 fecf_off) in 331 + if expected <> fecf_val then 332 + Error 333 + (Fecf_mismatch 334 + { 335 + expected = Int64.of_int expected; 336 + actual = Int64.of_int fecf_val; 337 + }) 338 + else 339 + Ok 340 + { 341 + header; 342 + insert_zone; 343 + data; 344 + ocf; 345 + fecf = Some (Int64.of_int fecf_val); 346 + } 347 + else 348 + Ok 349 + { 350 + header; 351 + insert_zone; 352 + data; 353 + ocf; 354 + fecf = Some (Int64.of_int fecf_val); 355 + }) 356 + 357 + (* {1 Frame encoding} *) 358 + 359 + let encode_header buf off hdr = 360 + (* Byte 0: TFVN(4b) | SCID[15:12](4b) *) 361 + let b0 = ((hdr.tfvn land 0xF) lsl 4) lor ((hdr.scid lsr 12) land 0xF) in 362 + set_u8 buf off b0; 363 + (* Byte 1: SCID[11:4](8b) *) 364 + set_u8 buf (off + 1) ((hdr.scid lsr 4) land 0xFF); 365 + (* Byte 2: SCID[3:0](4b) | src_dest(1b) | VCID[5:3](3b) *) 366 + let src_dest_bit = match hdr.src_or_dest with Source -> 0 | Dest -> 1 in 367 + let b2 = 368 + ((hdr.scid land 0xF) lsl 4) 369 + lor (src_dest_bit lsl 3) 370 + lor ((hdr.vcid lsr 3) land 0x7) 371 + in 372 + set_u8 buf (off + 2) b2; 373 + (* Byte 3: VCID[2:0](3b) | MAP_ID(4b) | EOFPH(1b) *) 374 + let b3 = 375 + ((hdr.vcid land 0x7) lsl 5) 376 + lor ((hdr.map_id land 0xF) lsl 1) 377 + lor if hdr.eofph then 1 else 0 378 + in 379 + set_u8 buf (off + 3) b3; 380 + (* Byte 4-5: Frame Length (16b) *) 381 + set_u16_be buf (off + 4) hdr.frame_len; 382 + (* Byte 6: Bypass(1b) | PCC(1b) | Rsvd(2b) | OCF(1b) | VCFC_len(3b) *) 383 + let b6 = 384 + ((if hdr.bypass_flag then 1 else 0) lsl 7) 385 + lor ((if hdr.prot_ctrl_cmd then 1 else 0) lsl 6) 386 + lor ((if hdr.ocf_flag then 1 else 0) lsl 3) 387 + lor (hdr.vcfc_len land 0x7) 388 + in 389 + set_u8 buf (off + 6) b6; 390 + (* Bytes 7+: VCFC *) 391 + if hdr.vcfc_len > 0 then set_var_uint_be buf (off + 7) hdr.vcfc_len hdr.vcfc 392 + 393 + let encode ?(insert_zone_len = 0) ?(with_ocf = false) ?(fecf = No_fecf) frame = 394 + let ocf_size = if with_ocf then ocf_len else 0 in 395 + let fecf_size = match fecf with No_fecf -> 0 | Crc16 -> 2 | Crc32 -> 4 in 396 + let hdr_len = min_header_len + frame.header.vcfc_len in 397 + let data_len = String.length frame.data in 398 + let total_len = hdr_len + insert_zone_len + data_len + ocf_size + fecf_size in 399 + let buf = Bytes.make total_len '\000' in 400 + (* Update frame_len in header (total - 1) *) 401 + let header = 402 + { frame.header with frame_len = total_len - 1; ocf_flag = with_ocf } 403 + in 404 + (* Header *) 405 + encode_header buf 0 header; 406 + (* Insert Zone *) 407 + (match frame.insert_zone with 408 + | Some iz when insert_zone_len > 0 -> 409 + let copy_len = min (String.length iz) insert_zone_len in 410 + Bytes.blit_string iz 0 buf hdr_len copy_len 411 + | _ -> ()); 412 + (* Data *) 413 + Bytes.blit_string frame.data 0 buf (hdr_len + insert_zone_len) data_len; 414 + (* OCF *) 415 + (if with_ocf then 416 + let ocf_off = hdr_len + insert_zone_len + data_len in 417 + match frame.ocf with Some ocf -> set_u32_be buf ocf_off ocf | None -> ()); 418 + (* FECF *) 419 + let fecf_off = hdr_len + insert_zone_len + data_len + ocf_size in 420 + (match fecf with 421 + | No_fecf -> () 422 + | Crc16 -> 423 + let crc = compute_crc16 (Bytes.sub_string buf 0 fecf_off) in 424 + set_u16_be buf fecf_off crc 425 + | Crc32 -> 426 + let crc = compute_crc32 (Bytes.sub_string buf 0 fecf_off) in 427 + set_u32_be buf fecf_off crc); 428 + Bytes.to_string buf 429 + 430 + let encoded_len ?(insert_zone_len = 0) ?(with_ocf = false) ?(fecf = No_fecf) 431 + frame = 432 + let ocf_size = if with_ocf then ocf_len else 0 in 433 + let fecf_size = match fecf with No_fecf -> 0 | Crc16 -> 2 | Crc32 -> 4 in 434 + let hdr_len = min_header_len + frame.header.vcfc_len in 435 + hdr_len + insert_zone_len + String.length frame.data + ocf_size + fecf_size 436 + 437 + (* {1 Predicates} *) 438 + 439 + let is_idle frame = vcid_to_int frame.header.vcid = idle_vcid 440 + 441 + (* {1 Pretty-printing} *) 442 + 443 + let pp_src_or_dest ppf = function 444 + | Source -> Format.fprintf ppf "Source" 445 + | Dest -> Format.fprintf ppf "Dest" 446 + 447 + let pp_header ppf hdr = 448 + Format.fprintf ppf 449 + "@[<hv 2>{ tfvn=%d;@ scid=%d;@ %a;@ vcid=%d;@ map_id=%d;@ vcfc=%d }@]" 450 + hdr.tfvn hdr.scid pp_src_or_dest hdr.src_or_dest hdr.vcid hdr.map_id 451 + hdr.vcfc 452 + 453 + let pp ppf frame = 454 + Format.fprintf ppf "@[<v 2>USLP_frame %a@ data[%d bytes]%a%a%a@]" pp_header 455 + frame.header (String.length frame.data) 456 + (fun ppf -> function 457 + | Some iz -> 458 + Format.fprintf ppf "@ insert_zone[%d bytes]" (String.length iz) 459 + | None -> ()) 460 + frame.insert_zone 461 + (fun ppf -> function 462 + | Some ocf -> Format.fprintf ppf "@ ocf=0x%08X" ocf | None -> ()) 463 + frame.ocf 464 + (fun ppf -> function 465 + | Some f -> Format.fprintf ppf "@ fecf=0x%Lx" f | None -> ()) 466 + frame.fecf 467 + 468 + let equal_header a b = 469 + a.tfvn = b.tfvn && a.scid = b.scid 470 + && a.src_or_dest = b.src_or_dest 471 + && a.vcid = b.vcid && a.map_id = b.map_id && a.eofph = b.eofph 472 + && a.bypass_flag = b.bypass_flag 473 + && a.prot_ctrl_cmd = b.prot_ctrl_cmd 474 + && a.vcfc_len = b.vcfc_len && a.vcfc = b.vcfc 475 + 476 + let equal a b = 477 + let ocf_equal o1 o2 = 478 + match (o1, o2) with 479 + | None, None -> true 480 + | Some x, Some y -> x = y 481 + | None, Some 0 | Some 0, None -> true 482 + | _ -> false 483 + in 484 + equal_header a.header b.header 485 + && a.insert_zone = b.insert_zone 486 + && a.data = b.data && ocf_equal a.ocf b.ocf 487 + 488 + (* {1 Constructors} *) 489 + 490 + let v ?(tfvn = tfvn_uslp) ?(src_or_dest = Source) ?(eofph = false) 491 + ?(bypass_flag = false) ?(prot_ctrl_cmd = false) ?(insert_zone = None) 492 + ?(ocf = None) ?(fecf = None) ~scid ~vcid ~map_id ~vcfc ~vcfc_len data = 493 + let header = 494 + { 495 + tfvn; 496 + scid = scid land 0xFFFF; 497 + src_or_dest; 498 + vcid = vcid land 0x3F; 499 + map_id = map_id land 0xF; 500 + eofph; 501 + frame_len = 0; 502 + (* Computed during encode *) 503 + bypass_flag; 504 + prot_ctrl_cmd; 505 + ocf_flag = Option.is_some ocf; 506 + vcfc_len = vcfc_len land 0x7; 507 + vcfc; 508 + } 509 + in 510 + { header; insert_zone; data; ocf; fecf } 511 + 512 + (* {1 CLCW Integration} *) 513 + 514 + let get_clcw frame = 515 + match frame.ocf with None -> None | Some word -> Some (Clcw.decode word) 516 + 517 + let set_clcw frame clcw = 518 + let ocf = Some (Clcw.encode clcw) in 519 + { frame with ocf } 520 + 521 + let with_clcw ?(tfvn = tfvn_uslp) ?(src_or_dest = Source) ?(eofph = false) 522 + ?(bypass_flag = false) ?(prot_ctrl_cmd = false) ?(insert_zone = None) 523 + ?(fecf = None) ~scid ~vcid ~map_id ~vcfc ~vcfc_len ~clcw data = 524 + let ocf = Some (Clcw.encode clcw) in 525 + let header = 526 + { 527 + tfvn; 528 + scid = scid land 0xFFFF; 529 + src_or_dest; 530 + vcid = vcid land 0x3F; 531 + map_id = map_id land 0xF; 532 + eofph; 533 + frame_len = 0; 534 + bypass_flag; 535 + prot_ctrl_cmd; 536 + ocf_flag = true; 537 + vcfc_len = vcfc_len land 0x7; 538 + vcfc; 539 + } 540 + in 541 + { header; insert_zone; data; ocf; fecf }
+190
lib/uslp.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** CCSDS USLP (Unified Space Link Protocol) Transfer Frame (CCSDS 732.1-B-2). 7 + 8 + USLP is a newer protocol that unifies TM, TC, and AOS with additional 9 + features like variable-length VCFC, MAP IDs, and flexible CRC options. *) 10 + 11 + (** {1 Constants} *) 12 + 13 + val min_header_len : int 14 + (** Minimum header length (7 bytes, without VCFC). *) 15 + 16 + val ocf_len : int 17 + (** OCF length (4 bytes). *) 18 + 19 + val tfvn_uslp : int 20 + (** Transfer Frame Version Number for USLP (12). *) 21 + 22 + val idle_vcid : int 23 + (** Reserved VCID for idle frames (63). *) 24 + 25 + val max_frame_len : int 26 + (** Maximum frame length for security limits. *) 27 + 28 + (** {1 Identifiers} *) 29 + 30 + type scid = private int 31 + (** Spacecraft Identifier (16 bits, 0-65535). *) 32 + 33 + val scid : int -> scid option 34 + val scid_exn : int -> scid 35 + val scid_to_int : scid -> int 36 + 37 + type vcid = private int 38 + (** Virtual Channel Identifier (6 bits, 0-63). *) 39 + 40 + val vcid : int -> vcid option 41 + val vcid_exn : int -> vcid 42 + val vcid_to_int : vcid -> int 43 + 44 + type map_id = private int 45 + (** Multiplexer Access Point Identifier (4 bits, 0-15). *) 46 + 47 + val map_id : int -> map_id option 48 + val map_id_exn : int -> map_id 49 + val map_id_to_int : map_id -> int 50 + 51 + (** {1 Types} *) 52 + 53 + type src_or_dest = Source | Dest (** Source or destination indicator. *) 54 + 55 + type fecf_type = 56 + | No_fecf 57 + | Crc16 58 + | Crc32 (** FECF type: none, CRC-16-CCITT, or CRC-32. *) 59 + 60 + type header = { 61 + tfvn : int; (** Transfer Frame Version Number. *) 62 + scid : scid; (** Spacecraft ID. *) 63 + src_or_dest : src_or_dest; (** Source or destination. *) 64 + vcid : vcid; (** Virtual Channel ID. *) 65 + map_id : map_id; (** MAP ID. *) 66 + eofph : bool; (** End of Frame Primary Header. *) 67 + frame_len : int; (** Frame length - 1. *) 68 + bypass_flag : bool; (** Bypass flag. *) 69 + prot_ctrl_cmd : bool; (** Protocol Control Command flag. *) 70 + ocf_flag : bool; (** OCF present flag. *) 71 + vcfc_len : int; (** VCFC length in bytes (0-7). *) 72 + vcfc : int; (** Virtual Channel Frame Count. *) 73 + } 74 + (** USLP frame primary header. *) 75 + 76 + type t = { 77 + header : header; 78 + insert_zone : string option; (** Optional insert zone data. *) 79 + data : string; (** Frame data field. *) 80 + ocf : int option; (** Operational Control Field (32 bits). *) 81 + fecf : int64 option; (** Frame Error Control Field (16 or 32 bits). *) 82 + } 83 + (** USLP Transfer Frame. *) 84 + 85 + val equal_header : header -> header -> bool 86 + val equal : t -> t -> bool 87 + val pp_header : Format.formatter -> header -> unit 88 + val pp : Format.formatter -> t -> unit 89 + val pp_src_or_dest : Format.formatter -> src_or_dest -> unit 90 + 91 + (** {1 Errors} *) 92 + 93 + type error = 94 + | Truncated of { need : int; have : int } 95 + | Invalid_version of int 96 + | Invalid_scid of int 97 + | Invalid_vcid of int 98 + | Invalid_map_id of int 99 + | Fecf_mismatch of { expected : int64; actual : int64 } 100 + | Invalid_vcfc_len of int 101 + 102 + val pp_error : Format.formatter -> error -> unit 103 + 104 + (** {1 Encoding/Decoding} *) 105 + 106 + val decode : 107 + ?vcfc_len:int -> 108 + ?insert_zone_len:int -> 109 + ?expect_ocf:bool -> 110 + ?expect_fecf:fecf_type -> 111 + ?check_fecf:bool -> 112 + string -> 113 + (t, error) result 114 + (** [decode buf] decodes a USLP frame from [buf]. 115 + @param vcfc_len VCFC length in bytes (default: 0) 116 + @param insert_zone_len Insert zone length (default: 0) 117 + @param expect_ocf Whether OCF is present (default: false) 118 + @param expect_fecf FECF type (default: No_fecf) 119 + @param check_fecf Whether to verify FECF (default: true) *) 120 + 121 + val encode : 122 + ?insert_zone_len:int -> ?with_ocf:bool -> ?fecf:fecf_type -> t -> string 123 + (** [encode frame] encodes a USLP frame to a string. 124 + @param insert_zone_len Insert zone length to reserve (default: 0) 125 + @param with_ocf Whether to include OCF (default: false) 126 + @param fecf FECF type (default: No_fecf) *) 127 + 128 + val encoded_len : 129 + ?insert_zone_len:int -> ?with_ocf:bool -> ?fecf:fecf_type -> t -> int 130 + (** [encoded_len frame] returns the encoded length of [frame]. *) 131 + 132 + (** {1 CRC} *) 133 + 134 + val compute_crc16 : string -> int 135 + (** [compute_crc16 data] computes CRC-16-CCITT over [data]. *) 136 + 137 + val compute_crc32 : string -> int 138 + (** [compute_crc32 data] computes CRC-32 (ISO 3309) over [data]. *) 139 + 140 + (** {1 Predicates} *) 141 + 142 + val is_idle : t -> bool 143 + (** [is_idle frame] returns true if frame uses the idle VCID (63). *) 144 + 145 + (** {1 Constructors} *) 146 + 147 + val v : 148 + ?tfvn:int -> 149 + ?src_or_dest:src_or_dest -> 150 + ?eofph:bool -> 151 + ?bypass_flag:bool -> 152 + ?prot_ctrl_cmd:bool -> 153 + ?insert_zone:string option -> 154 + ?ocf:int option -> 155 + ?fecf:int64 option -> 156 + scid:scid -> 157 + vcid:vcid -> 158 + map_id:map_id -> 159 + vcfc:int -> 160 + vcfc_len:int -> 161 + string -> 162 + t 163 + (** [v ~scid ~vcid ~map_id ~vcfc ~vcfc_len data] constructs a USLP frame. *) 164 + 165 + (** {1 CLCW Integration} *) 166 + 167 + val get_clcw : t -> (Clcw.t, Clcw.error) result option 168 + (** [get_clcw frame] extracts and decodes the CLCW from the OCF if present. *) 169 + 170 + val set_clcw : t -> Clcw.t -> t 171 + (** [set_clcw frame clcw] sets the OCF to the encoded CLCW. *) 172 + 173 + val with_clcw : 174 + ?tfvn:int -> 175 + ?src_or_dest:src_or_dest -> 176 + ?eofph:bool -> 177 + ?bypass_flag:bool -> 178 + ?prot_ctrl_cmd:bool -> 179 + ?insert_zone:string option -> 180 + ?fecf:int64 option -> 181 + scid:scid -> 182 + vcid:vcid -> 183 + map_id:map_id -> 184 + vcfc:int -> 185 + vcfc_len:int -> 186 + clcw:Clcw.t -> 187 + string -> 188 + t 189 + (** [with_clcw ~scid ~vcid ~map_id ~vcfc ~vcfc_len ~clcw data] constructs a USLP 190 + frame with CLCW. *)
+3
test/dune
··· 1 + (test 2 + (name test_uslp) 3 + (libraries uslp clcw alcotest))
+139
test/test_uslp.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + let uslp = Alcotest.testable Uslp.pp Uslp.equal 7 + 8 + let test_roundtrip () = 9 + let scid = Uslp.scid_exn 1000 in 10 + let vcid = Uslp.vcid_exn 5 in 11 + let map_id = Uslp.map_id_exn 3 in 12 + let data = "Hello, USLP!" in 13 + let frame = Uslp.v ~scid ~vcid ~map_id ~vcfc:12345 ~vcfc_len:0 data in 14 + let encoded = Uslp.encode frame in 15 + match Uslp.decode encoded with 16 + | Error e -> Alcotest.failf "decode failed: %a" Uslp.pp_error e 17 + | Ok frame' -> Alcotest.(check uslp) "roundtrip" frame frame' 18 + 19 + let test_roundtrip_with_vcfc () = 20 + let scid = Uslp.scid_exn 500 in 21 + let vcid = Uslp.vcid_exn 10 in 22 + let map_id = Uslp.map_id_exn 0 in 23 + let data = "With VCFC" in 24 + let frame = Uslp.v ~scid ~vcid ~map_id ~vcfc:0xABCD ~vcfc_len:2 data in 25 + let encoded = Uslp.encode frame in 26 + match Uslp.decode ~vcfc_len:2 encoded with 27 + | Error e -> Alcotest.failf "decode failed: %a" Uslp.pp_error e 28 + | Ok frame' -> Alcotest.(check uslp) "roundtrip with vcfc" frame frame' 29 + 30 + let test_roundtrip_with_crc16 () = 31 + let scid = Uslp.scid_exn 100 in 32 + let vcid = Uslp.vcid_exn 1 in 33 + let map_id = Uslp.map_id_exn 1 in 34 + let data = "CRC16 test" in 35 + let frame = Uslp.v ~scid ~vcid ~map_id ~vcfc:0 ~vcfc_len:0 data in 36 + let encoded = Uslp.encode ~fecf:Uslp.Crc16 frame in 37 + match Uslp.decode ~expect_fecf:Uslp.Crc16 encoded with 38 + | Error e -> Alcotest.failf "decode failed: %a" Uslp.pp_error e 39 + | Ok frame' -> Alcotest.(check uslp) "roundtrip crc16" frame frame' 40 + 41 + let test_roundtrip_with_crc32 () = 42 + let scid = Uslp.scid_exn 200 in 43 + let vcid = Uslp.vcid_exn 2 in 44 + let map_id = Uslp.map_id_exn 2 in 45 + let data = "CRC32 test" in 46 + let frame = Uslp.v ~scid ~vcid ~map_id ~vcfc:0 ~vcfc_len:0 data in 47 + let encoded = Uslp.encode ~fecf:Uslp.Crc32 frame in 48 + match Uslp.decode ~expect_fecf:Uslp.Crc32 encoded with 49 + | Error e -> Alcotest.failf "decode failed: %a" Uslp.pp_error e 50 + | Ok frame' -> Alcotest.(check uslp) "roundtrip crc32" frame frame' 51 + 52 + let test_clcw_integration () = 53 + let scid = Uslp.scid_exn 300 in 54 + let vcid = Uslp.vcid_exn 3 in 55 + let map_id = Uslp.map_id_exn 0 in 56 + let clcw_vcid = Clcw.vcid_exn 3 in 57 + let clcw = Clcw.v ~vcid:clcw_vcid ~report_value:99 () in 58 + let frame = 59 + Uslp.with_clcw ~scid ~vcid ~map_id ~vcfc:0 ~vcfc_len:0 ~clcw "CLCW test" 60 + in 61 + let encoded = Uslp.encode ~with_ocf:true frame in 62 + match Uslp.decode ~expect_ocf:true encoded with 63 + | Error e -> Alcotest.failf "decode failed: %a" Uslp.pp_error e 64 + | Ok frame' -> ( 65 + match Uslp.get_clcw frame' with 66 + | None -> Alcotest.fail "no CLCW" 67 + | Some (Error e) -> 68 + Alcotest.failf "CLCW decode failed: %a" Clcw.pp_error e 69 + | Some (Ok clcw') -> 70 + Alcotest.(check int) "report_value" 99 clcw'.report_value) 71 + 72 + let test_idle_frame () = 73 + let scid = Uslp.scid_exn 0 in 74 + let vcid = Uslp.vcid_exn 63 in 75 + let map_id = Uslp.map_id_exn 0 in 76 + let frame = Uslp.v ~scid ~vcid ~map_id ~vcfc:0 ~vcfc_len:0 "" in 77 + Alcotest.(check bool) "is_idle" true (Uslp.is_idle frame) 78 + 79 + let test_src_dest () = 80 + let scid = Uslp.scid_exn 400 in 81 + let vcid = Uslp.vcid_exn 4 in 82 + let map_id = Uslp.map_id_exn 4 in 83 + let frame = 84 + Uslp.v ~scid ~vcid ~map_id ~vcfc:0 ~vcfc_len:0 ~src_or_dest:Uslp.Dest "dest" 85 + in 86 + let encoded = Uslp.encode frame in 87 + match Uslp.decode encoded with 88 + | Error e -> Alcotest.failf "decode failed: %a" Uslp.pp_error e 89 + | Ok frame' -> 90 + Alcotest.(check bool) 91 + "is dest" true 92 + (frame'.header.src_or_dest = Uslp.Dest) 93 + 94 + let test_vcid_bounds () = 95 + Alcotest.(check bool) "vcid 0 valid" true (Option.is_some (Uslp.vcid 0)); 96 + Alcotest.(check bool) "vcid 63 valid" true (Option.is_some (Uslp.vcid 63)); 97 + Alcotest.(check bool) "vcid -1 invalid" true (Option.is_none (Uslp.vcid (-1))); 98 + Alcotest.(check bool) "vcid 64 invalid" true (Option.is_none (Uslp.vcid 64)) 99 + 100 + let test_scid_bounds () = 101 + Alcotest.(check bool) "scid 0 valid" true (Option.is_some (Uslp.scid 0)); 102 + Alcotest.(check bool) 103 + "scid 65535 valid" true 104 + (Option.is_some (Uslp.scid 65535)); 105 + Alcotest.(check bool) "scid -1 invalid" true (Option.is_none (Uslp.scid (-1))); 106 + Alcotest.(check bool) 107 + "scid 65536 invalid" true 108 + (Option.is_none (Uslp.scid 65536)) 109 + 110 + let test_map_id_bounds () = 111 + Alcotest.(check bool) "map_id 0 valid" true (Option.is_some (Uslp.map_id 0)); 112 + Alcotest.(check bool) "map_id 15 valid" true (Option.is_some (Uslp.map_id 15)); 113 + Alcotest.(check bool) 114 + "map_id -1 invalid" true 115 + (Option.is_none (Uslp.map_id (-1))); 116 + Alcotest.(check bool) 117 + "map_id 16 invalid" true 118 + (Option.is_none (Uslp.map_id 16)) 119 + 120 + let () = 121 + Alcotest.run "uslp" 122 + [ 123 + ( "uslp", 124 + [ 125 + Alcotest.test_case "roundtrip" `Quick test_roundtrip; 126 + Alcotest.test_case "roundtrip_with_vcfc" `Quick 127 + test_roundtrip_with_vcfc; 128 + Alcotest.test_case "roundtrip_with_crc16" `Quick 129 + test_roundtrip_with_crc16; 130 + Alcotest.test_case "roundtrip_with_crc32" `Quick 131 + test_roundtrip_with_crc32; 132 + Alcotest.test_case "clcw_integration" `Quick test_clcw_integration; 133 + Alcotest.test_case "idle_frame" `Quick test_idle_frame; 134 + Alcotest.test_case "src_dest" `Quick test_src_dest; 135 + Alcotest.test_case "vcid_bounds" `Quick test_vcid_bounds; 136 + Alcotest.test_case "scid_bounds" `Quick test_scid_bounds; 137 + Alcotest.test_case "map_id_bounds" `Quick test_map_id_bounds; 138 + ] ); 139 + ]
+31
uslp.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "CCSDS USLP Transfer Frames (CCSDS 732.1-B-2)" 4 + description: 5 + "Parser and encoder for CCSDS Unified Space Link Protocol (USLP) Transfer Frames. Supports variable-length VCFC, MAP IDs, source/destination flags, optional OCF, and FECF with CRC-16 or CRC-32." 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-uslp"