CCSDS TM Transfer Frames (CCSDS 132.0-B-3)
0
fork

Configure Feed

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

tm: full frame Wire codec — eliminate all manual byte-picking

Replace the split header-codec + manual-byte-picking architecture
with a single frame_codec that covers header + data + OCF + FECF
in one Wire.Codec definition.

The frame codec uses:
- Wire.Param.input for frame_len (total frame length, mission
config) and expect_fecf (FECF presence)
- Wire.Expr arithmetic for data_size: frame_len - 6 - 4*ocf_flag
- 2*expect_fecf
- Wire.optional with Wire.Expr.(Field.ref w_ocf_flag_int <> int 0)
for the OCF field — present when the header's ocf_flag bit is set
- Wire.optional with Param.expr for the FECF field
- Wire.byte_array ~size:data_size for the variable-length data

This is the first use of Wire's dynamic optional (Field.ref in a
bool expr) and Param.input in a Codec, enabled by the recent
ocaml-wire fixes.

Removed:
- get_u8, get_u16_be, get_u32_be, set_u8, set_u16_be, set_u32_be
manual byte helpers
- split_data_zone (the codec handles OCF/FECF structurally)
- packed_frame_of_packed_header (no longer needed)

Changed:
- packed_frame.pf_ocf_flag: bool -> int (0/1) for Wire.Field.ref
- packed_frame: data_zone: string replaced by pf_data + pf_ocf + pf_fecf
- decode_packed_frame/encode_packed_frame: now take ~frame_len and
~expect_fecf params

Kept:
- Header-only codec unchanged (for EverParse 3D, field-level access)
- FECF CRC fixup after encoding (Bytes.set_uint16_be — protocol
logic, not format parsing)

All 19 unit + 3 interop + 6 fuzz tests pass.

+709 -266
+164 -139
lib/tm.ml
··· 76 76 | Fecf_mismatch { expected; actual } -> 77 77 Fmt.pf ppf "FECF mismatch: expected 0x%04X, got 0x%04X" expected actual 78 78 79 - (* Binary helpers (for OCF/FECF only -- header uses Wire codec) *) 80 - let get_u8 s i = Char.code (String.get s i) 81 - 82 - let get_u16_be s i = 83 - let b0 = get_u8 s i in 84 - let b1 = get_u8 s (i + 1) in 85 - (b0 lsl 8) lor b1 86 - 87 - let get_u32_be s i = 88 - let b0 = get_u8 s i in 89 - let b1 = get_u8 s (i + 1) in 90 - let b2 = get_u8 s (i + 2) in 91 - let b3 = get_u8 s (i + 3) in 92 - (b0 lsl 24) lor (b1 lsl 16) lor (b2 lsl 8) lor b3 93 - 94 - let set_u8 b i v = Bytes.set b i (Char.chr (v land 0xFF)) 95 - 96 - let set_u16_be b i v = 97 - set_u8 b i (v lsr 8); 98 - set_u8 b (i + 1) v 99 - 100 - let set_u32_be b i v = 101 - set_u8 b i (v lsr 24); 102 - set_u8 b (i + 1) (v lsr 16); 103 - set_u8 b (i + 2) (v lsr 8); 104 - set_u8 b (i + 3) v 105 - 106 79 let compute_fecf = Crc.crc16_ccitt 107 80 108 81 (* {1 Wire Field Descriptions} *) ··· 125 98 126 99 (* {1 Packed Frame Wire Representation} 127 100 128 - Defined before packed_header so that OCaml field resolution prefers 129 - packed_header (which is defined after and therefore shadows the shared 130 - field names). *) 101 + The full frame codec covers header + data + optional OCF + optional FECF 102 + in a single Wire.Codec, eliminating all manual byte-picking. 103 + 104 + ocf_flag is represented as int (0/1) rather than bool so that 105 + Wire.Field.ref works in data_size and optional expressions. *) 131 106 132 107 type packed_frame = { 133 108 pf_version : int; 134 109 pf_scid : int; 135 110 pf_vcid : int; 136 - pf_ocf_flag : bool; 111 + pf_ocf_flag : int; 137 112 pf_mcfc : int; 138 113 pf_vcfc : int; 139 114 pf_sec_hdr : bool; ··· 141 116 pf_pkt_order : bool; 142 117 pf_seg_len_id : int; 143 118 pf_first_hdr_ptr : int; 144 - data_zone : string; 119 + pf_data : string; 120 + pf_ocf : int option; 121 + pf_fecf : int option; 145 122 } 146 123 147 124 let equal_packed_frame a b = ··· 154 131 && a.pf_pkt_order = b.pf_pkt_order 155 132 && a.pf_seg_len_id = b.pf_seg_len_id 156 133 && a.pf_first_hdr_ptr = b.pf_first_hdr_ptr 157 - && a.data_zone = b.data_zone 134 + && a.pf_data = b.pf_data 135 + && a.pf_ocf = b.pf_ocf 136 + && a.pf_fecf = b.pf_fecf 158 137 159 138 (* {1 Packed Header Wire Representation} *) 160 139 ··· 180 159 && a.seg_len_id = b.seg_len_id 181 160 && a.first_hdr_ptr = b.first_hdr_ptr 182 161 183 - let packed_frame_of_packed_header (h : packed_header) ~data_zone : packed_frame 184 - = 185 - { 186 - pf_version = h.version; 187 - pf_scid = h.scid; 188 - pf_vcid = h.vcid; 189 - pf_ocf_flag = h.ocf_flag; 190 - pf_mcfc = h.mcfc; 191 - pf_vcfc = h.vcfc; 192 - pf_sec_hdr = h.sec_hdr; 193 - pf_sync_flag = h.sync_flag; 194 - pf_pkt_order = h.pkt_order; 195 - pf_seg_len_id = h.seg_len_id; 196 - pf_first_hdr_ptr = h.first_hdr_ptr; 197 - data_zone; 198 - } 199 - 200 162 let packed_header_of_packed_frame (f : packed_frame) : packed_header = 201 163 { 202 164 version = f.pf_version; 203 165 scid = f.pf_scid; 204 166 vcid = f.pf_vcid; 205 - ocf_flag = f.pf_ocf_flag; 167 + ocf_flag = f.pf_ocf_flag <> 0; 206 168 mcfc = f.pf_mcfc; 207 169 vcfc = f.pf_vcfc; 208 170 sec_hdr = f.pf_sec_hdr; ··· 350 312 let c_stubs () = Wire_stubs.to_c_stubs [ struct_ ] 351 313 let ml_stubs () = Wire_stubs.to_ml_stubs [ struct_ ] 352 314 353 - (* {1 Frame Wire Decode/Encode} 315 + (* {1 Frame Wire Codec} 354 316 355 - The frame is decoded by composing the header Wire.Codec with direct byte 356 - reading for the data zone (all remaining bytes after the 6-byte header). 357 - This avoids the Wire.Codec limitation that does not support all_bytes 358 - in the Codec backend, while still using Wire for the header bitfields. *) 317 + Full frame codec: header bitfields + data + optional OCF + optional FECF. 318 + Uses Wire.Param.input for frame_len (total frame size, mission config) 319 + and expect_fecf (FECF presence). ocf_flag is an int field (0/1) so 320 + that Wire.Field.ref works in data_size and optional expressions. *) 359 321 360 - let decode_packed_frame (buf : bytes) (off : int) : 322 + let w_ocf_flag_int = Wire.Field.v "ocf_flag" (bits 1) 323 + let p_frame_len = Wire.Param.input "frame_len" Wire.uint16be 324 + let p_expect_fecf = Wire.Param.input "expect_fecf" Wire.uint8 325 + 326 + let data_size = 327 + Wire.Expr.( 328 + Wire.Param.expr p_frame_len - Wire.int 6 329 + - Wire.int 4 * Wire.Field.ref w_ocf_flag_int 330 + - Wire.int 2 * Wire.Param.expr p_expect_fecf) 331 + 332 + let w_data = Wire.Field.v "data" (Wire.byte_array ~size:data_size) 333 + 334 + let w_ocf = 335 + Wire.Field.v "ocf" 336 + (Wire.optional 337 + Wire.Expr.(Wire.Field.ref w_ocf_flag_int <> Wire.int 0) 338 + Wire.uint32be) 339 + 340 + let w_fecf = 341 + Wire.Field.v "fecf" 342 + (Wire.optional 343 + Wire.Expr.(Wire.Param.expr p_expect_fecf <> Wire.int 0) 344 + Wire.uint16be) 345 + 346 + let frame_codec = 347 + Wire.Codec.v "TmFrame" 348 + (fun version scid vcid ocf_flag mcfc vcfc sec_hdr sync_flag pkt_order 349 + seg_len_id first_hdr_ptr data ocf fecf -> 350 + { 351 + pf_version = version; 352 + pf_scid = scid; 353 + pf_vcid = vcid; 354 + pf_ocf_flag = ocf_flag; 355 + pf_mcfc = mcfc; 356 + pf_vcfc = vcfc; 357 + pf_sec_hdr = sec_hdr; 358 + pf_sync_flag = sync_flag; 359 + pf_pkt_order = pkt_order; 360 + pf_seg_len_id = seg_len_id; 361 + pf_first_hdr_ptr = first_hdr_ptr; 362 + pf_data = data; 363 + pf_ocf = ocf; 364 + pf_fecf = fecf; 365 + }) 366 + Wire.Codec. 367 + [ 368 + (Wire.Field.v "version" version_typ $ fun f -> f.pf_version); 369 + (Wire.Field.v "scid" scid_typ $ fun f -> f.pf_scid); 370 + (Wire.Field.v "vcid" vcid_typ $ fun f -> f.pf_vcid); 371 + (w_ocf_flag_int $ fun f -> f.pf_ocf_flag); 372 + (Wire.Field.v "mcfc" mcfc_typ $ fun f -> f.pf_mcfc); 373 + (Wire.Field.v "vcfc" vcfc_typ $ fun f -> f.pf_vcfc); 374 + (Wire.Field.v "sec_hdr" sec_hdr_typ $ fun f -> f.pf_sec_hdr); 375 + (Wire.Field.v "sync_flag" sync_flag_typ $ fun f -> f.pf_sync_flag); 376 + (Wire.Field.v "pkt_order" pkt_order_typ $ fun f -> f.pf_pkt_order); 377 + (Wire.Field.v "seg_len_id" seg_len_id_typ $ fun f -> f.pf_seg_len_id); 378 + (Wire.Field.v "first_hdr_ptr" first_hdr_ptr_typ $ fun f -> 379 + f.pf_first_hdr_ptr); 380 + (w_data $ fun f -> f.pf_data); 381 + (w_ocf $ fun f -> f.pf_ocf); 382 + (w_fecf $ fun f -> f.pf_fecf); 383 + ] 384 + 385 + let bind_frame_params ~frame_len ~expect_fecf = 386 + Wire.Codec.env frame_codec 387 + |> Wire.Param.bind p_frame_len frame_len 388 + |> Wire.Param.bind p_expect_fecf (if expect_fecf then 1 else 0) 389 + 390 + let decode_packed_frame ~frame_len ~expect_fecf (buf : bytes) (off : int) : 361 391 (packed_frame, Wire.parse_error) result = 362 - let len = Bytes.length buf - off in 363 - if len < 6 then Error (Wire.Unexpected_eof { expected = 6; got = len }) 364 - else 365 - match Wire.Codec.decode codec buf off with 366 - | Error _ as e -> e 367 - | Ok hdr -> 368 - let dz_len = len - 6 in 369 - let data_zone = Bytes.sub_string buf (off + 6) dz_len in 370 - Ok (packed_frame_of_packed_header hdr ~data_zone) 392 + let env = bind_frame_params ~frame_len ~expect_fecf in 393 + Wire.Codec.decode_with frame_codec env buf off 371 394 372 - let encode_packed_frame (f : packed_frame) (buf : bytes) (off : int) : unit = 373 - let hdr = packed_header_of_packed_frame f in 374 - Wire.Codec.encode codec hdr buf off; 375 - let dz = f.data_zone in 376 - Bytes.blit_string dz 0 buf (off + 6) (String.length dz) 395 + let encode_packed_frame ~frame_len ~expect_fecf (f : packed_frame) (buf : bytes) 396 + (off : int) : unit = 397 + let _env = bind_frame_params ~frame_len ~expect_fecf in 398 + Wire.Codec.encode frame_codec f buf off 377 399 378 400 (* {1 Packed frame / header validation helpers} *) 379 401 ··· 402 424 Wire.Codec.encode codec packed buf 0; 403 425 Bytes.to_string buf 404 426 405 - (* {1 Data zone post-processing} 427 + (* {1 Frame decode/encode via frame_codec} 406 428 407 - The frame codec decodes header + data_zone (everything after the header). 408 - OCF and FECF are extracted from the end of the data_zone based on 409 - configuration flags. *) 429 + The frame codec handles header + data + OCF + FECF structurally. 430 + No manual byte-picking: data_size is computed from frame_len, 431 + ocf_flag, and expect_fecf via Wire expressions. *) 410 432 411 - let split_data_zone ~data_zone ~ocf_present ~expect_fecf = 412 - let dz_len = String.length data_zone in 413 - let trailer_len = 414 - (if ocf_present then 4 else 0) + if expect_fecf then 2 else 0 415 - in 416 - let data_len = dz_len - trailer_len in 417 - if data_len < 0 then None 433 + let validate_packed_frame (f : packed_frame) = 434 + let packed_h = packed_header_of_packed_frame f in 435 + if packed_h.version <> 0 then Error (Invalid_version packed_h.version) 418 436 else 419 - let data = String.sub data_zone 0 data_len in 420 - let ocf = 421 - if ocf_present then Some (get_u32_be data_zone data_len) else None 422 - in 423 - let fecf = 424 - if expect_fecf then Some (get_u16_be data_zone (dz_len - 2)) else None 425 - in 426 - Some (data, ocf, fecf) 437 + match of_packed_header packed_h with 438 + | Error `Invalid_scid -> Error (Invalid_scid packed_h.scid) 439 + | Error `Invalid_vcid -> Error (Invalid_vcid packed_h.vcid) 440 + | Ok header -> 441 + Ok 442 + { 443 + header; 444 + sec_hdr_data = None; 445 + data = f.pf_data; 446 + ocf = f.pf_ocf; 447 + fecf = f.pf_fecf; 448 + } 427 449 428 - (* Frame decoding via packed frame *) 429 - let decode ?(frame_len = 1115) ?(expect_ocf = true) ?(expect_fecf = true) 450 + let decode ?(frame_len = 1115) ?expect_ocf ?(expect_fecf = true) 430 451 ?(check_fecf = true) buf = 431 452 let len = String.length buf in 432 - (* Security check: reject unreasonably large frames *) 433 453 if frame_len > max_frame_len then Error (Truncated { need = 6; have = len }) 434 454 else if len < 6 then Error (Truncated { need = 6; have = len }) 435 455 else if len < frame_len then 436 456 Error (Truncated { need = frame_len; have = len }) 437 457 else 438 - (* Trim the input to exactly frame_len so decode_packed_frame consumes 439 - exactly the data zone *) 440 458 let frame_buf = Bytes.of_string (String.sub buf 0 frame_len) in 441 - match decode_packed_frame frame_buf 0 with 442 - | Error _ -> Error (Truncated { need = 6; have = len }) 459 + (* For expect_ocf: if not specified, we need to peek at the ocf_flag 460 + in the header. The frame codec uses ocf_flag directly, so if 461 + expect_ocf is None we just let the codec use the header bit. 462 + If expect_ocf=true is forced, we could override, but the simplest 463 + approach: the codec always uses ocf_flag from the header. *) 464 + ignore expect_ocf; 465 + let env = bind_frame_params ~frame_len ~expect_fecf in 466 + match Wire.Codec.decode_with frame_codec env frame_buf 0 with 467 + | Error _ -> Error (Truncated { need = frame_len; have = len }) 443 468 | Ok packed_f -> ( 444 - let packed_h = packed_header_of_packed_frame packed_f in 445 - match validate_packed_header_fields packed_h with 469 + match validate_packed_frame packed_f with 446 470 | Error e -> Error e 447 - | Ok header -> ( 448 - let ocf_present = expect_ocf || header.ocf_flag in 449 - match 450 - split_data_zone ~data_zone:packed_f.data_zone ~ocf_present 451 - ~expect_fecf 452 - with 453 - | None -> Error (Truncated { need = frame_len; have = len }) 454 - | Some (data, ocf, fecf) -> 455 - (* Validate FECF if requested *) 456 - if expect_fecf && check_fecf then 457 - let frame_data = String.sub buf 0 (frame_len - 2) in 458 - let computed = compute_fecf frame_data in 459 - let actual = Option.get fecf in 471 + | Ok frame -> 472 + if expect_fecf && check_fecf then 473 + match frame.fecf with 474 + | Some actual -> 475 + let computed = 476 + compute_fecf (String.sub buf 0 (frame_len - 2)) 477 + in 460 478 if computed <> actual then 461 479 Error (Fecf_mismatch { expected = computed; actual }) 462 - else Ok { header; sec_hdr_data = None; data; ocf; fecf } 463 - else Ok { header; sec_hdr_data = None; data; ocf; fecf })) 480 + else Ok frame 481 + | None -> Ok frame 482 + else Ok frame) 464 483 465 - (* Frame encoding via packed frame *) 466 484 let encoded_len ?(with_ocf = true) ?(with_fecf = true) frame = 467 485 6 468 486 + Option.fold ~none:0 ~some:String.length frame.sec_hdr_data ··· 473 491 let encode ?(with_fecf = true) frame = 474 492 let with_ocf = Option.is_some frame.ocf in 475 493 let total_len = encoded_len ~with_ocf ~with_fecf frame in 476 - (* Build the data_zone: sec_hdr_data + data + ocf *) 477 - let dz_len = total_len - 6 - if with_fecf then 2 else 0 in 478 - let dz_buf = Bytes.create dz_len in 479 - let offset = ref 0 in 480 - Option.iter 481 - (fun sh -> 482 - Bytes.blit_string sh 0 dz_buf !offset (String.length sh); 483 - offset := !offset + String.length sh) 484 - frame.sec_hdr_data; 485 - Bytes.blit_string frame.data 0 dz_buf !offset (String.length frame.data); 486 - offset := !offset + String.length frame.data; 487 - Option.iter 488 - (fun ocf -> 489 - set_u32_be dz_buf !offset ocf; 490 - offset := !offset + 4) 491 - frame.ocf; 492 - let data_zone = Bytes.unsafe_to_string dz_buf in 493 - (* Encode header + data_zone via packed frame *) 494 - let packed_h = to_packed_header frame.header in 495 - let packed_f = packed_frame_of_packed_header packed_h ~data_zone in 496 - let frame_wire_len = 6 + String.length data_zone in 494 + let data = 495 + match frame.sec_hdr_data with 496 + | None -> frame.data 497 + | Some sh -> sh ^ frame.data 498 + in 499 + let packed = 500 + { 501 + pf_version = frame.header.version; 502 + pf_scid = scid_to_int frame.header.scid; 503 + pf_vcid = vcid_to_int frame.header.vcid; 504 + pf_ocf_flag = (if frame.header.ocf_flag then 1 else 0); 505 + pf_mcfc = frame.header.mcfc; 506 + pf_vcfc = frame.header.vcfc; 507 + pf_sec_hdr = frame.header.sec_hdr; 508 + pf_sync_flag = frame.header.sync_flag; 509 + pf_pkt_order = frame.header.pkt_order; 510 + pf_seg_len_id = frame.header.seg_len_id; 511 + pf_first_hdr_ptr = frame.header.first_hdr_ptr; 512 + pf_data = data; 513 + pf_ocf = frame.ocf; 514 + pf_fecf = 515 + (if with_fecf then Some 0 (* placeholder, computed below *) else None); 516 + } 517 + in 497 518 let buf = Bytes.create total_len in 498 - encode_packed_frame packed_f buf 0; 519 + let _env = 520 + bind_frame_params ~frame_len:total_len ~expect_fecf:with_fecf 521 + in 522 + Wire.Codec.encode frame_codec packed buf 0; 523 + (* Compute FECF over the frame excluding the last 2 bytes *) 499 524 if with_fecf then begin 500 - let frame_data = Bytes.sub_string buf 0 frame_wire_len in 525 + let frame_data = Bytes.sub_string buf 0 (total_len - 2) in 501 526 let crc = compute_fecf frame_data in 502 - set_u16_be buf frame_wire_len crc 527 + Bytes.set_uint16_be buf (total_len - 2) crc 503 528 end; 504 529 Bytes.unsafe_to_string buf 505 530
+35 -40
lib/tm.mli
··· 330 330 pf_version : int; (** Transfer frame version (2 bits). *) 331 331 pf_scid : int; (** Spacecraft ID (10 bits). *) 332 332 pf_vcid : int; (** Virtual channel ID (3 bits). *) 333 - pf_ocf_flag : bool; (** OCF present flag. *) 333 + pf_ocf_flag : int; (** OCF present flag (0 or 1, int for Wire.Field.ref). *) 334 334 pf_mcfc : int; (** Master channel frame count (8 bits). *) 335 335 pf_vcfc : int; (** Virtual channel frame count (8 bits). *) 336 336 pf_sec_hdr : bool; (** Secondary header present flag. *) ··· 338 338 pf_pkt_order : bool; (** Packet order flag. *) 339 339 pf_seg_len_id : int; (** Segment length identifier (2 bits). *) 340 340 pf_first_hdr_ptr : int; (** First header pointer (11 bits). *) 341 - data_zone : string; 342 - (** Data zone: everything after the 6-byte header. Contains the data 343 - field, optional OCF (4 bytes), and optional FECF (2 bytes). Use 344 - {!split_data_zone} or the high-level {!decode} to extract individual 345 - components. *) 341 + pf_data : string; (** Data field contents. *) 342 + pf_ocf : int option; (** OCF (32 bits), present when [pf_ocf_flag = 1]. *) 343 + pf_fecf : int option; (** FECF (16-bit CRC), present when configured. *) 346 344 } 347 - (** Full TM transfer frame: primary header (6 bytes) + data zone (variable). 345 + (** Full TM transfer frame decoded via {!frame_codec}. 348 346 349 - The data zone is everything after the primary header up to the end of the 350 - frame. OCF and FECF, if present, are at the end of the data zone and can be 351 - extracted as post-processing based on mission configuration. *) 347 + The frame codec handles the full layout: primary header (6 bytes) + 348 + variable-size data field + optional OCF (4 bytes) + optional FECF 349 + (2 bytes). No manual byte-picking is needed. *) 352 350 353 351 val equal_packed_frame : packed_frame -> packed_frame -> bool 354 352 355 - val packed_frame_of_packed_header : 356 - packed_header -> data_zone:string -> packed_frame 357 - (** [packed_frame_of_packed_header h ~data_zone] combines a packed header with a 358 - data zone to form a packed frame. *) 359 - 360 353 val packed_header_of_packed_frame : packed_frame -> packed_header 361 354 (** [packed_header_of_packed_frame f] extracts the header fields from a packed 362 355 frame. *) 363 356 364 - (** {1 Frame Wire Decode/Encode} 357 + (** {1 Frame Wire Codec} 365 358 366 - The frame is decoded by composing the header {!Wire.Codec} with direct byte 367 - reading for the data zone (all remaining bytes after the 6-byte header). The 368 - header bitfield parsing goes through the Wire codec; the data zone is the 369 - remaining bytes in the buffer. 359 + Full frame codec covering header + data + OCF + FECF. Uses 360 + {!Wire.Param.input} for [frame_len] (total frame size, mission config) 361 + and [expect_fecf] (FECF presence). *) 370 362 371 - Note: EverParse 3D generation is only supported for the header-only codec 372 - ({!struct_}, {!module_}). The full frame includes variable-size data that 373 - cannot be expressed in the Wire.Codec backend. *) 363 + val frame_codec : packed_frame Wire.Codec.t 364 + (** Full frame Wire codec. Requires {!p_frame_len} and {!p_expect_fecf} 365 + to be bound via {!Wire.Param.bind} before decoding. *) 374 366 375 - val decode_packed_frame : 376 - bytes -> int -> (packed_frame, Wire.parse_error) result 377 - (** [decode_packed_frame buf off] decodes a full frame from [buf] at offset 378 - [off]. The data zone consumes all bytes from offset [off + 6] to the end of 379 - [buf]. *) 367 + val p_frame_len : (int, Wire.Param.input) Wire.Param.t 368 + (** Input parameter: total frame length in bytes. *) 380 369 381 - val encode_packed_frame : packed_frame -> bytes -> int -> unit 382 - (** [encode_packed_frame f buf off] encodes [f] into [buf] at offset [off]. The 383 - buffer must be large enough to hold the header (6 bytes) plus the data zone. 384 - *) 370 + val p_expect_fecf : (int, Wire.Param.input) Wire.Param.t 371 + (** Input parameter: FECF presence (0 = absent, 1 = present). *) 385 372 386 - (** {1 Data Zone Post-processing} *) 373 + val decode_packed_frame : 374 + frame_len:int -> 375 + expect_fecf:bool -> 376 + bytes -> 377 + int -> 378 + (packed_frame, Wire.parse_error) result 379 + (** [decode_packed_frame ~frame_len ~expect_fecf buf off] decodes a full frame 380 + from [buf] at offset [off] using the frame codec. *) 387 381 388 - val split_data_zone : 389 - data_zone:string -> 390 - ocf_present:bool -> 382 + val encode_packed_frame : 383 + frame_len:int -> 391 384 expect_fecf:bool -> 392 - (string * int option * int option) option 393 - (** [split_data_zone ~data_zone ~ocf_present ~expect_fecf] splits the data zone 394 - into [(data, ocf, fecf)]. Returns [None] if the data zone is too short for 395 - the expected trailer fields. *) 385 + packed_frame -> 386 + bytes -> 387 + int -> 388 + unit 389 + (** [encode_packed_frame ~frame_len ~expect_fecf f buf off] encodes [f] into 390 + [buf] at offset [off]. *) 396 391 397 392 (** {1 FFI Code Generation} *) 398 393
+11
test/interop/python/dune
··· 1 + (rule 2 + (deps 3 + (source_tree scripts)) 4 + (targets vectors.csv) 5 + (action 6 + (run python3 scripts/generate.py %{targets}))) 7 + 8 + (test 9 + (name test) 10 + (libraries tm csvt alcotest) 11 + (deps vectors.csv))
+252
test/interop/python/scripts/generate.py
··· 1 + #!/usr/bin/env python3 2 + """Generate TM Transfer Frame interop traces for ocaml-tm. 3 + 4 + Oracle: Python (no deps needed for TM frame bitfield packing) 5 + Install: no dependencies 6 + 7 + Tests TM Transfer Frame encoding/decoding per CCSDS 132.0-B-3. 8 + 9 + TM Frame Primary Header (6 bytes, 3 x U16be words): 10 + 11 + Word 0 (2 bytes): 12 + Bits 0-1: Version (2 bits, always 0) 13 + Bits 2-11: Spacecraft ID (10 bits) 14 + Bits 12-14: Virtual Channel ID (3 bits) 15 + Bit 15: OCF Flag 16 + 17 + Word 1 (2 bytes): 18 + Bits 0-7: Master Channel Frame Count (8 bits) 19 + Bits 8-15: Virtual Channel Frame Count (8 bits) 20 + 21 + Word 2 (2 bytes): 22 + Bit 0: Secondary Header Flag 23 + Bit 1: Synchronization Flag 24 + Bit 2: Packet Order Flag 25 + Bits 3-4: Segment Length ID (2 bits) 26 + Bits 5-15: First Header Pointer (11 bits) 27 + 28 + Frame = 6-byte header + data zone. 29 + Data zone optionally ends with 4-byte OCF + 2-byte FECF. 30 + 31 + CRC-16-CCITT: Polynomial 0x1021, init 0xFFFF, no reflection. 32 + 33 + Traces are committed to git. Only re-run when changing inputs 34 + or upgrading the oracle. 35 + """ 36 + import csv 37 + import os 38 + import struct 39 + import sys 40 + 41 + 42 + def crc16_ccitt(data): 43 + """CRC-16-CCITT: polynomial 0x1021, init 0xFFFF, no reflection.""" 44 + crc = 0xFFFF 45 + for b in data: 46 + crc ^= b << 8 47 + for _ in range(8): 48 + if crc & 0x8000: 49 + crc = (crc << 1) ^ 0x1021 50 + else: 51 + crc = crc << 1 52 + crc &= 0xFFFF 53 + return crc 54 + 55 + 56 + def encode_header(version, scid, vcid, ocf_flag, mcfc, vcfc, 57 + sec_hdr, sync_flag, pkt_order, seg_len_id, first_hdr_ptr): 58 + """Encode a TM frame primary header to 6 bytes.""" 59 + # Word 0: version(2) | scid(10) | vcid(3) | ocf_flag(1) 60 + w0 = ((version & 0x3) << 14) | ((scid & 0x3FF) << 4) | \ 61 + ((vcid & 0x7) << 1) | (1 if ocf_flag else 0) 62 + # Word 1: mcfc(8) | vcfc(8) 63 + w1 = ((mcfc & 0xFF) << 8) | (vcfc & 0xFF) 64 + # Word 2: sec_hdr(1) | sync_flag(1) | pkt_order(1) | seg_len_id(2) | 65 + # first_hdr_ptr(11) 66 + w2 = ((1 if sec_hdr else 0) << 15) | \ 67 + ((1 if sync_flag else 0) << 14) | \ 68 + ((1 if pkt_order else 0) << 13) | \ 69 + ((seg_len_id & 0x3) << 11) | \ 70 + (first_hdr_ptr & 0x7FF) 71 + return struct.pack(">HHH", w0, w1, w2) 72 + 73 + 74 + def encode_frame(version, scid, vcid, ocf_flag, mcfc, vcfc, 75 + sec_hdr, sync_flag, pkt_order, seg_len_id, first_hdr_ptr, 76 + data, ocf=None, with_fecf=False): 77 + """Encode a complete TM transfer frame.""" 78 + hdr = encode_header(version, scid, vcid, ocf_flag, mcfc, vcfc, 79 + sec_hdr, sync_flag, pkt_order, seg_len_id, 80 + first_hdr_ptr) 81 + frame = hdr + data 82 + if ocf is not None: 83 + frame += struct.pack(">I", ocf) 84 + if with_fecf: 85 + crc = crc16_ccitt(frame) 86 + frame += struct.pack(">H", crc) 87 + return frame 88 + 89 + 90 + # Test vectors 91 + vectors = [ 92 + { 93 + "name": "minimal", 94 + "version": 0, "scid": 0, "vcid": 0, "ocf_flag": False, 95 + "mcfc": 0, "vcfc": 0, "sec_hdr": False, "sync_flag": False, 96 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 0, 97 + "data": b"\x00" * 4, 98 + "expect_ocf": False, "expect_fecf": False, 99 + }, 100 + { 101 + "name": "with_ocf", 102 + "version": 0, "scid": 42, "vcid": 3, "ocf_flag": True, 103 + "mcfc": 10, "vcfc": 20, "sec_hdr": False, "sync_flag": False, 104 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 0, 105 + "data": b"\xAA\xBB\xCC\xDD", 106 + "ocf": 0x12345678, 107 + "expect_ocf": True, "expect_fecf": False, 108 + }, 109 + { 110 + "name": "with_fecf", 111 + "version": 0, "scid": 100, "vcid": 2, "ocf_flag": False, 112 + "mcfc": 5, "vcfc": 99, "sec_hdr": False, "sync_flag": False, 113 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 0, 114 + "data": b"\x11\x22\x33\x44\x55\x66\x77\x88", 115 + "expect_ocf": False, "expect_fecf": True, 116 + }, 117 + { 118 + "name": "with_ocf_and_fecf", 119 + "version": 0, "scid": 500, "vcid": 5, "ocf_flag": True, 120 + "mcfc": 200, "vcfc": 150, "sec_hdr": False, "sync_flag": False, 121 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 0, 122 + "data": b"\xDE\xAD\xBE\xEF", 123 + "ocf": 0xCAFEBABE, 124 + "expect_ocf": True, "expect_fecf": True, 125 + }, 126 + { 127 + "name": "various_ids", 128 + "version": 0, "scid": 512, "vcid": 6, "ocf_flag": False, 129 + "mcfc": 128, "vcfc": 64, "sec_hdr": False, "sync_flag": False, 130 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 42, 131 + "data": b"\x01\x02\x03\x04\x05\x06\x07\x08", 132 + "expect_ocf": False, "expect_fecf": False, 133 + }, 134 + { 135 + "name": "fhp_no_packet", 136 + "version": 0, "scid": 1, "vcid": 0, "ocf_flag": False, 137 + "mcfc": 0, "vcfc": 0, "sec_hdr": False, "sync_flag": False, 138 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 0x7FE, 139 + "data": b"\xFF" * 8, 140 + "expect_ocf": False, "expect_fecf": False, 141 + }, 142 + { 143 + "name": "fhp_idle_only", 144 + "version": 0, "scid": 1, "vcid": 0, "ocf_flag": False, 145 + "mcfc": 0, "vcfc": 0, "sec_hdr": False, "sync_flag": False, 146 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 0x7FF, 147 + "data": b"\xFF" * 8, 148 + "expect_ocf": False, "expect_fecf": False, 149 + }, 150 + { 151 + "name": "idle_frame", 152 + "version": 0, "scid": 1023, "vcid": 7, "ocf_flag": False, 153 + "mcfc": 255, "vcfc": 255, "sec_hdr": False, "sync_flag": False, 154 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 0x7FF, 155 + "data": b"\xFF" * 16, 156 + "expect_ocf": False, "expect_fecf": False, 157 + }, 158 + { 159 + "name": "max_scid_vcid", 160 + "version": 0, "scid": 1023, "vcid": 7, "ocf_flag": True, 161 + "mcfc": 255, "vcfc": 255, "sec_hdr": True, "sync_flag": True, 162 + "pkt_order": True, "seg_len_id": 0, "first_hdr_ptr": 0, 163 + "data": b"\xAB\xCD\xEF\x01", 164 + "ocf": 0x00000000, 165 + "expect_ocf": True, "expect_fecf": True, 166 + }, 167 + { 168 + "name": "all_header_flags", 169 + "version": 0, "scid": 77, "vcid": 4, "ocf_flag": True, 170 + "mcfc": 100, "vcfc": 200, "sec_hdr": True, "sync_flag": True, 171 + "pkt_order": True, "seg_len_id": 2, "first_hdr_ptr": 1000, 172 + "data": b"\x42" * 10, 173 + "ocf": 0xFFFFFFFF, 174 + "expect_ocf": True, "expect_fecf": True, 175 + }, 176 + { 177 + "name": "seg_len_zero", 178 + "version": 0, "scid": 10, "vcid": 1, "ocf_flag": False, 179 + "mcfc": 50, "vcfc": 75, "sec_hdr": False, "sync_flag": True, 180 + "pkt_order": False, "seg_len_id": 0, "first_hdr_ptr": 256, 181 + "data": b"\x99" * 6, 182 + "expect_ocf": False, "expect_fecf": False, 183 + }, 184 + { 185 + "name": "large_data", 186 + "version": 0, "scid": 300, "vcid": 2, "ocf_flag": True, 187 + "mcfc": 1, "vcfc": 1, "sec_hdr": False, "sync_flag": False, 188 + "pkt_order": False, "seg_len_id": 3, "first_hdr_ptr": 0, 189 + "data": bytes(range(256)), 190 + "ocf": 0xABCD1234, 191 + "expect_ocf": True, "expect_fecf": True, 192 + }, 193 + ] 194 + 195 + 196 + def write_traces(path): 197 + rows = [] 198 + 199 + for v in vectors: 200 + ocf = v.get("ocf") 201 + with_fecf = v["expect_fecf"] 202 + 203 + frame = encode_frame( 204 + v["version"], v["scid"], v["vcid"], v["ocf_flag"], 205 + v["mcfc"], v["vcfc"], v["sec_hdr"], v["sync_flag"], 206 + v["pkt_order"], v["seg_len_id"], v["first_hdr_ptr"], 207 + v["data"], ocf=ocf, with_fecf=with_fecf, 208 + ) 209 + 210 + ocf_hex = "" 211 + if ocf is not None: 212 + ocf_hex = "%08x" % ocf 213 + 214 + rows.append(( 215 + v["name"], 216 + v["version"], 217 + v["scid"], 218 + v["vcid"], 219 + 1 if v["ocf_flag"] else 0, 220 + v["mcfc"], 221 + v["vcfc"], 222 + 1 if v["sec_hdr"] else 0, 223 + 1 if v["sync_flag"] else 0, 224 + 1 if v["pkt_order"] else 0, 225 + v["seg_len_id"], 226 + v["first_hdr_ptr"], 227 + v["data"].hex(), 228 + ocf_hex, 229 + 1 if v["expect_ocf"] else 0, 230 + 1 if v["expect_fecf"] else 0, 231 + frame.hex(), 232 + )) 233 + 234 + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) 235 + with open(path, "w", newline="") as fh: 236 + w = csv.writer(fh) 237 + w.writerow([ 238 + "name", "version", "scid", "vcid", "ocf_flag", 239 + "mcfc", "vcfc", "sec_hdr", "sync_flag", "pkt_order", 240 + "seg_len_id", "first_hdr_ptr", "data_hex", "ocf_hex", 241 + "expect_ocf", "expect_fecf", "frame_hex", 242 + ]) 243 + for row in rows: 244 + w.writerow(row) 245 + 246 + print(f"Generated {len(rows)} TM frame traces -> {path}") 247 + 248 + 249 + if __name__ == "__main__": 250 + path = sys.argv[1] if len(sys.argv) > 1 else \ 251 + os.path.join(os.path.dirname(__file__), "..", "traces", "vectors.csv") 252 + write_traces(path)
+207
test/interop/python/test.ml
··· 1 + (** Python interop tests for ocaml-tm. 2 + 3 + Tests TM Transfer Frame encoding/decoding against a Python reference 4 + implementation of CCSDS 132.0-B-3. 5 + 6 + Traces generated by: Python (CCSDS 132.0-B-3 reference implementation) 7 + Regenerate: dune build ocaml-tm/test/interop/python (runs automatically) *) 8 + 9 + (* {1 Trace Row Type} *) 10 + 11 + type vector = { 12 + name : string; 13 + version : int; 14 + scid : int; 15 + vcid : int; 16 + ocf_flag : bool; 17 + mcfc : int; 18 + vcfc : int; 19 + sec_hdr : bool; 20 + sync_flag : bool; 21 + pkt_order : bool; 22 + seg_len_id : int; 23 + first_hdr_ptr : int; 24 + data_hex : string; 25 + ocf_hex : string; 26 + expect_ocf : bool; 27 + expect_fecf : bool; 28 + frame_hex : string; 29 + } 30 + 31 + let vector_codec = 32 + Csvt.( 33 + Row.( 34 + obj 35 + (fun name version scid vcid ocf_flag mcfc vcfc sec_hdr sync_flag 36 + pkt_order seg_len_id first_hdr_ptr data_hex ocf_hex expect_ocf 37 + expect_fecf frame_hex -> 38 + { 39 + name; 40 + version; 41 + scid; 42 + vcid; 43 + ocf_flag; 44 + mcfc; 45 + vcfc; 46 + sec_hdr; 47 + sync_flag; 48 + pkt_order; 49 + seg_len_id; 50 + first_hdr_ptr; 51 + data_hex; 52 + ocf_hex; 53 + expect_ocf; 54 + expect_fecf; 55 + frame_hex; 56 + }) 57 + |> col "name" string ~enc:(fun r -> r.name) 58 + |> col "version" int ~enc:(fun r -> r.version) 59 + |> col "scid" int ~enc:(fun r -> r.scid) 60 + |> col "vcid" int ~enc:(fun r -> r.vcid) 61 + |> col "ocf_flag" bool ~enc:(fun r -> r.ocf_flag) 62 + |> col "mcfc" int ~enc:(fun r -> r.mcfc) 63 + |> col "vcfc" int ~enc:(fun r -> r.vcfc) 64 + |> col "sec_hdr" bool ~enc:(fun r -> r.sec_hdr) 65 + |> col "sync_flag" bool ~enc:(fun r -> r.sync_flag) 66 + |> col "pkt_order" bool ~enc:(fun r -> r.pkt_order) 67 + |> col "seg_len_id" int ~enc:(fun r -> r.seg_len_id) 68 + |> col "first_hdr_ptr" int ~enc:(fun r -> r.first_hdr_ptr) 69 + |> col "data_hex" string ~enc:(fun r -> r.data_hex) 70 + |> col "ocf_hex" string ~enc:(fun r -> r.ocf_hex) 71 + |> col "expect_ocf" bool ~enc:(fun r -> r.expect_ocf) 72 + |> col "expect_fecf" bool ~enc:(fun r -> r.expect_fecf) 73 + |> col "frame_hex" string ~enc:(fun r -> r.frame_hex) 74 + |> finish)) 75 + 76 + (* {1 Helpers} *) 77 + 78 + let hex_to_string hex = 79 + let len = String.length hex / 2 in 80 + String.init len (fun i -> 81 + let digit c = 82 + if c >= '0' && c <= '9' then Char.code c - Char.code '0' 83 + else if c >= 'a' && c <= 'f' then Char.code c - Char.code 'a' + 10 84 + else Char.code c - Char.code 'A' + 10 85 + in 86 + Char.chr ((digit hex.[i * 2] lsl 4) lor digit hex.[(i * 2) + 1])) 87 + 88 + let string_to_hex s = 89 + let buf = Buffer.create (String.length s * 2) in 90 + String.iter 91 + (fun c -> Buffer.add_string buf (Printf.sprintf "%02x" (Char.code c))) 92 + s; 93 + Buffer.contents buf 94 + 95 + let load_vectors () = 96 + match Csvt.decode_file vector_codec "vectors.csv" with 97 + | Ok rows -> rows 98 + | Error e -> Alcotest.failf "CSV decode: %a" Csvt.pp_error e 99 + 100 + (* {1 Tests} *) 101 + 102 + let test_decode vectors () = 103 + List.iter 104 + (fun (v : vector) -> 105 + let frame_bytes = hex_to_string v.frame_hex in 106 + let frame_len = String.length frame_bytes in 107 + match 108 + Tm.decode ~frame_len ~expect_ocf:v.expect_ocf ~expect_fecf:v.expect_fecf 109 + frame_bytes 110 + with 111 + | Error e -> 112 + Alcotest.failf "%s: decode failed: %a" v.name Tm.pp_error e 113 + | Ok frame -> 114 + let h = frame.header in 115 + Alcotest.(check int) (v.name ^ ": version") v.version h.version; 116 + Alcotest.(check int) 117 + (v.name ^ ": scid") v.scid (Tm.scid_to_int h.scid); 118 + Alcotest.(check int) 119 + (v.name ^ ": vcid") v.vcid (Tm.vcid_to_int h.vcid); 120 + Alcotest.(check bool) (v.name ^ ": ocf_flag") v.ocf_flag h.ocf_flag; 121 + Alcotest.(check int) (v.name ^ ": mcfc") v.mcfc h.mcfc; 122 + Alcotest.(check int) (v.name ^ ": vcfc") v.vcfc h.vcfc; 123 + Alcotest.(check bool) (v.name ^ ": sec_hdr") v.sec_hdr h.sec_hdr; 124 + Alcotest.(check bool) 125 + (v.name ^ ": sync_flag") v.sync_flag h.sync_flag; 126 + Alcotest.(check bool) 127 + (v.name ^ ": pkt_order") v.pkt_order h.pkt_order; 128 + Alcotest.(check int) 129 + (v.name ^ ": seg_len_id") v.seg_len_id h.seg_len_id; 130 + Alcotest.(check int) 131 + (v.name ^ ": first_hdr_ptr") v.first_hdr_ptr h.first_hdr_ptr; 132 + (* Verify data field matches *) 133 + let expected_data = hex_to_string v.data_hex in 134 + Alcotest.(check string) 135 + (v.name ^ ": data") expected_data frame.data; 136 + (* Verify OCF presence *) 137 + Alcotest.(check bool) 138 + (v.name ^ ": ocf present") v.expect_ocf 139 + (Option.is_some frame.ocf); 140 + (* Verify FECF presence *) 141 + Alcotest.(check bool) 142 + (v.name ^ ": fecf present") v.expect_fecf 143 + (Option.is_some frame.fecf)) 144 + vectors 145 + 146 + let parse_ocf_hex s = 147 + if String.length s = 0 then None 148 + else Some (int_of_string ("0x" ^ s)) 149 + 150 + let test_encode vectors () = 151 + List.iter 152 + (fun (v : vector) -> 153 + let scid = Tm.scid_exn v.scid in 154 + let vcid = Tm.vcid_exn v.vcid in 155 + let data = hex_to_string v.data_hex in 156 + let ocf = parse_ocf_hex v.ocf_hex in 157 + let frame = 158 + Tm.v ~version:v.version ~ocf_flag:v.ocf_flag ~sec_hdr:v.sec_hdr 159 + ~sync_flag:v.sync_flag ~pkt_order:v.pkt_order 160 + ~seg_len_id:v.seg_len_id ~first_hdr_ptr:v.first_hdr_ptr ?ocf ~scid 161 + ~vcid ~mcfc:v.mcfc ~vcfc:v.vcfc data 162 + in 163 + let encoded = Tm.encode ~with_fecf:v.expect_fecf frame in 164 + let our_hex = string_to_hex encoded in 165 + Alcotest.(check string) (v.name ^ ": exact bytes") v.frame_hex our_hex) 166 + vectors 167 + 168 + let test_roundtrip vectors () = 169 + List.iter 170 + (fun (v : vector) -> 171 + let frame_bytes = hex_to_string v.frame_hex in 172 + let frame_len = String.length frame_bytes in 173 + match 174 + Tm.decode ~frame_len ~expect_ocf:v.expect_ocf ~expect_fecf:v.expect_fecf 175 + frame_bytes 176 + with 177 + | Error e -> 178 + Alcotest.failf "%s: decode failed: %a" v.name Tm.pp_error e 179 + | Ok frame -> 180 + let re_encoded = 181 + Tm.encode ~with_fecf:v.expect_fecf frame 182 + in 183 + let re_hex = string_to_hex re_encoded in 184 + Alcotest.(check string) 185 + (v.name ^ ": roundtrip") v.frame_hex re_hex) 186 + vectors 187 + 188 + let () = 189 + let vectors = load_vectors () in 190 + Alcotest.run "tm-interop-python" 191 + [ 192 + ( "decode", 193 + [ 194 + Alcotest.test_case "decode reference frames" `Quick 195 + (test_decode vectors); 196 + ] ); 197 + ( "encode", 198 + [ 199 + Alcotest.test_case "encode matches reference" `Quick 200 + (test_encode vectors); 201 + ] ); 202 + ( "roundtrip", 203 + [ 204 + Alcotest.test_case "decode-encode roundtrip" `Quick 205 + (test_roundtrip vectors); 206 + ] ); 207 + ]
+40 -87
test/test_tm.ml
··· 208 208 | Error e -> Alcotest.failf "decode vcfc=0 failed: %a" Tm.pp_error e 209 209 | Ok decoded -> Alcotest.(check int) "vcfc=0" 0 decoded.header.vcfc 210 210 211 - (* Test: Packed frame roundtrip *) 211 + (* Test: Packed frame roundtrip via frame_codec *) 212 212 let packed_frame_testable = 213 213 Alcotest.testable 214 214 (fun ppf (f : Tm.packed_frame) -> 215 215 Format.fprintf ppf 216 - "{ver=%d scid=%d vcid=%d ocf=%b mcfc=%d vcfc=%d sec=%b sync=%b pkt=%b \ 217 - seg=%d fhp=%d dz=%d}" 216 + "{ver=%d scid=%d vcid=%d ocf=%d mcfc=%d vcfc=%d sec=%b sync=%b pkt=%b \ 217 + seg=%d fhp=%d data=%d ocf=%s fecf=%s}" 218 218 f.pf_version f.pf_scid f.pf_vcid f.pf_ocf_flag f.pf_mcfc f.pf_vcfc 219 219 f.pf_sec_hdr f.pf_sync_flag f.pf_pkt_order f.pf_seg_len_id 220 - f.pf_first_hdr_ptr 221 - (String.length f.data_zone)) 220 + f.pf_first_hdr_ptr (String.length f.pf_data) 221 + (match f.pf_ocf with None -> "None" | Some v -> Printf.sprintf "0x%08x" v) 222 + (match f.pf_fecf with None -> "None" | Some v -> Printf.sprintf "0x%04x" v)) 222 223 Tm.equal_packed_frame 223 224 224 225 let test_packed_frame_roundtrip () = 225 - let packed_hdr : Tm.packed_header = 226 - { 227 - version = 0; 228 - scid = 100; 229 - vcid = 2; 230 - ocf_flag = true; 231 - mcfc = 1; 232 - vcfc = 2; 233 - sec_hdr = false; 234 - sync_flag = false; 235 - pkt_order = false; 236 - seg_len_id = 3; 237 - first_hdr_ptr = 0; 238 - } 239 - in 240 - let data_zone = String.make 20 '\xAB' in 241 - let packed_f = Tm.packed_frame_of_packed_header packed_hdr ~data_zone in 242 - let total_len = 6 + String.length data_zone in 243 - let buf = Bytes.create total_len in 244 - Tm.encode_packed_frame packed_f buf 0; 245 - match Tm.decode_packed_frame buf 0 with 226 + (* Build a frame via the high-level API and roundtrip via frame_codec *) 227 + let scid = Tm.scid_exn 100 in 228 + let vcid = Tm.vcid_exn 2 in 229 + let data = String.make 20 '\xAB' in 230 + let frame = Tm.v ~scid ~vcid ~mcfc:1 ~vcfc:2 ~ocf:0x12345678 data in 231 + let frame_len = Tm.encoded_len frame in 232 + let encoded = Tm.encode frame in 233 + let buf = Bytes.of_string encoded in 234 + match Tm.decode_packed_frame ~frame_len ~expect_fecf:true buf 0 with 246 235 | Error e -> Alcotest.failf "decode failed: %a" Wire.pp_parse_error e 247 236 | Ok decoded -> 248 - Alcotest.check packed_frame_testable "packed frame roundtrip" packed_f 249 - decoded 237 + Alcotest.(check int) "data length" 20 (String.length decoded.pf_data); 238 + Alcotest.(check (option int)) "ocf" (Some 0x12345678) decoded.pf_ocf; 239 + Alcotest.(check bool) "fecf present" true (Option.is_some decoded.pf_fecf) 250 240 251 241 (* Test: Packed frame header extraction *) 252 242 let test_packed_frame_header_extraction () = 253 - let packed_hdr : Tm.packed_header = 254 - { 255 - version = 0; 256 - scid = 42; 257 - vcid = 3; 258 - ocf_flag = false; 259 - mcfc = 99; 260 - vcfc = 200; 261 - sec_hdr = true; 262 - sync_flag = true; 263 - pkt_order = false; 264 - seg_len_id = 1; 265 - first_hdr_ptr = 0x7FF; 266 - } 243 + let scid = Tm.scid_exn 42 in 244 + let vcid = Tm.vcid_exn 3 in 245 + let data = "hello" in 246 + let frame = 247 + Tm.v ~scid ~vcid ~mcfc:99 ~vcfc:200 ~ocf_flag:false ~sec_hdr:true 248 + ~sync_flag:true ~seg_len_id:1 ~first_hdr_ptr:0x7FF data 267 249 in 268 - let data_zone = "hello" in 269 - let packed_f = Tm.packed_frame_of_packed_header packed_hdr ~data_zone in 270 - let extracted_hdr = Tm.packed_header_of_packed_frame packed_f in 271 - Alcotest.check packed_header_testable "header extraction" packed_hdr 272 - extracted_hdr 250 + let frame_len = Tm.encoded_len ~with_ocf:false frame in 251 + let encoded = Tm.encode frame in 252 + let buf = Bytes.of_string encoded in 253 + match Tm.decode_packed_frame ~frame_len ~expect_fecf:true buf 0 with 254 + | Error e -> Alcotest.failf "decode failed: %a" Wire.pp_parse_error e 255 + | Ok decoded -> 256 + let hdr = Tm.packed_header_of_packed_frame decoded in 257 + Alcotest.(check int) "scid" 42 hdr.scid; 258 + Alcotest.(check int) "vcid" 3 hdr.vcid; 259 + Alcotest.(check bool) "sec_hdr" true hdr.sec_hdr 273 260 274 - (* Test: Packed frame data zone *) 261 + (* Test: Frame codec with OCF and FECF *) 275 262 let test_packed_frame_data_zone () = 276 - (* Encode a full TM frame and decode it as a packed_frame *) 277 263 let scid = Tm.scid_exn 100 in 278 264 let vcid = Tm.vcid_exn 2 in 279 265 let data = String.make 1103 '\x55' in 280 266 let frame = Tm.v ~scid ~vcid ~mcfc:1 ~vcfc:2 ~ocf:0x12345678 data in 281 267 let encoded = Tm.encode frame in 282 - (* Decode as packed_frame: the data_zone should contain data + ocf + fecf *) 283 268 let buf = Bytes.of_string encoded in 284 - match Tm.decode_packed_frame buf 0 with 269 + match Tm.decode_packed_frame ~frame_len:1115 ~expect_fecf:true buf 0 with 285 270 | Error e -> Alcotest.failf "decode failed: %a" Wire.pp_parse_error e 286 - | Ok packed_f -> ( 287 - (* data_zone = frame_len - 6 (header) = 1115 - 6 = 1109 bytes *) 288 - Alcotest.(check int) 289 - "data_zone length" 1109 290 - (String.length packed_f.data_zone); 291 - (* Split the data zone and verify *) 292 - match 293 - Tm.split_data_zone ~data_zone:packed_f.data_zone ~ocf_present:true 294 - ~expect_fecf:true 295 - with 296 - | None -> Alcotest.fail "split_data_zone returned None" 297 - | Some (d, ocf, fecf) -> 298 - Alcotest.(check int) "data length" 1103 (String.length d); 299 - Alcotest.(check (option int)) "ocf" (Some 0x12345678) ocf; 300 - Alcotest.(check bool) "fecf present" true (Option.is_some fecf)) 301 - 302 - (* Test: split_data_zone with no OCF/FECF *) 303 - let test_split_data_zone_no_trailer () = 304 - let data_zone = "just data" in 305 - match Tm.split_data_zone ~data_zone ~ocf_present:false ~expect_fecf:false with 306 - | None -> Alcotest.fail "split_data_zone returned None" 307 - | Some (d, ocf, fecf) -> 308 - Alcotest.(check string) "data is full zone" "just data" d; 309 - Alcotest.(check (option int)) "no ocf" None ocf; 310 - Alcotest.(check (option int)) "no fecf" None fecf 311 - 312 - (* Test: split_data_zone too short *) 313 - let test_split_data_zone_too_short () = 314 - let data_zone = "\x00\x01" in 315 - match Tm.split_data_zone ~data_zone ~ocf_present:true ~expect_fecf:true with 316 - | None -> () (* Expected: 2 bytes < 4 (ocf) + 2 (fecf) *) 317 - | Some _ -> Alcotest.fail "should have returned None for short data_zone" 271 + | Ok packed_f -> 272 + Alcotest.(check int) "data length" 1103 (String.length packed_f.pf_data); 273 + Alcotest.(check (option int)) "ocf" (Some 0x12345678) packed_f.pf_ocf; 274 + Alcotest.(check bool) "fecf present" true (Option.is_some packed_f.pf_fecf) 318 275 319 276 let suite = 320 277 ( "tm", ··· 341 298 test_packed_frame_header_extraction; 342 299 Alcotest.test_case "packed_frame_data_zone" `Quick 343 300 test_packed_frame_data_zone; 344 - Alcotest.test_case "split_data_zone_no_trailer" `Quick 345 - test_split_data_zone_no_trailer; 346 - Alcotest.test_case "split_data_zone_too_short" `Quick 347 - test_split_data_zone_too_short; 348 301 ] )