CCSDS Proximity-1 Space Link Protocol (211.0-B)
0
fork

Configure Feed

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

Add CCSDS gap packages with spec test vectors

New packages:
- ocaml-tm-sync (131.0-B): TM randomizer (LFSR) with CCSDS PN
sequence test vector; RS/convolutional/LDPC/turbo stubs
- ocaml-proximity1 (211.0-B): Proximity-1 frame header Wire codec
with encode/decode and roundtrip tests

Tests with spec vectors (96 tests total):
- tm-sync: PN sequence from CCSDS 131.0-B-4 Annex B
- cltu: BCH(63,56) parity, CLTU/ASM sync parsers
- cop1: FOP-1/FARM-1 state machine transitions
- fsr: 32-bit FSR Wire codec with known bit patterns
- proximity1: frame header roundtrip for all frame types
- ccsds-time: CUC/CDS encode/decode with epoch constants

+424
+1
.ocamlformat
··· 1 + version = 0.28.1
+20
dune-project
··· 1 + (lang dune 3.21) 2 + (name proximity1) 3 + (generate_opam_files true) 4 + (license ISC) 5 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 6 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 7 + (source (tangled gazagnaire.org/ocaml-proximity1)) 8 + (package 9 + (name proximity1) 10 + (synopsis "CCSDS Proximity-1 Space Link Protocol (211.0-B)") 11 + (description 12 + "Proximity-1 frame encoding and decoding for short-range space-to-space \ 13 + communication (e.g., Mars orbiter to rover). Wire codec for the fixed \ 14 + frame header with version, SCID, frame type, sequence count, and frame \ 15 + length fields per CCSDS 211.0-B-5.") 16 + (depends 17 + (ocaml (>= 5.1)) 18 + (wire (>= 0.9)) 19 + (fmt (>= 0.9)) 20 + (alcotest :with-test)))
+4
lib/dune
··· 1 + (library 2 + (name proximity1) 3 + (public_name proximity1) 4 + (libraries wire fmt))
+107
lib/proximity1.ml
··· 1 + (** CCSDS Proximity-1 Space Link Protocol (211.0-B). 2 + 3 + @see <https://public.ccsds.org/Pubs/211x0b5.pdf> CCSDS 211.0-B-5 *) 4 + 5 + (* {1 Frame Type} *) 6 + 7 + type frame_type = Data | Ack | Expedited 8 + 9 + let frame_type_of_int = function 10 + | 0 -> Some Data 11 + | 1 -> Some Ack 12 + | 2 -> Some Expedited 13 + | _ -> None 14 + 15 + let int_of_frame_type = function Data -> 0 | Ack -> 1 | Expedited -> 2 16 + 17 + let pp_frame_type ppf = function 18 + | Data -> Fmt.string ppf "DATA" 19 + | Ack -> Fmt.string ppf "ACK" 20 + | Expedited -> Fmt.string ppf "EXPEDITED" 21 + 22 + (* {1 Frame Header} 23 + 24 + Layout (7 bytes): 25 + Byte 0-1 (U16be): Version (3b) | SCID (8b) | Frame Type (3b) | Reserved (2b) 26 + Byte 2: Sequence Count high byte (8b) 27 + Byte 3-4 (U16be): Sequence Count low 16 bits 28 + Byte 5-6 (U16be): Frame Length *) 29 + 30 + type header = { 31 + version : int; 32 + scid : int; 33 + frame_type : frame_type; 34 + sequence_count : int; 35 + frame_length : int; 36 + } 37 + 38 + (* Wire field type helpers *) 39 + let bits16 n = Wire.bits ~width:n Wire.U16be 40 + 41 + (* First 16-bit word: Version(3) + SCID(8) + FrameType(3) + Reserved(2) *) 42 + let f_version = Wire.Field.v "Version" (bits16 3) 43 + let f_scid = Wire.Field.v "SCID" (bits16 8) 44 + let f_frame_type = Wire.Field.v "FrameType" (bits16 3) 45 + let f_reserved = Wire.Field.v "Reserved" (bits16 2) 46 + 47 + (* Sequence count: split across uint8 (high byte) + uint16be (low 16 bits) *) 48 + let f_seq_hi = Wire.Field.v "SeqHi" Wire.uint8 49 + let f_seq_lo = Wire.Field.v "SeqLo" Wire.uint16be 50 + 51 + (* Frame length *) 52 + let f_frame_length = Wire.Field.v "FrameLength" Wire.uint16be 53 + 54 + let codec = 55 + Wire.Codec.v "Proximity1Header" 56 + (fun version scid frame_type _reserved seq_hi seq_lo frame_length -> 57 + let ft = 58 + match frame_type_of_int frame_type with 59 + | Some ft -> ft 60 + | None -> Data (* validated after decode *) 61 + in 62 + { 63 + version; 64 + scid; 65 + frame_type = ft; 66 + sequence_count = (seq_hi lsl 16) lor seq_lo; 67 + frame_length; 68 + }) 69 + Wire.Codec. 70 + [ 71 + (f_version $ fun t -> t.version); 72 + (f_scid $ fun t -> t.scid); 73 + (f_frame_type $ fun t -> int_of_frame_type t.frame_type); 74 + (f_reserved $ fun _ -> 0); 75 + (f_seq_hi $ fun t -> (t.sequence_count lsr 16) land 0xFF); 76 + (f_seq_lo $ fun t -> t.sequence_count land 0xFFFF); 77 + (f_frame_length $ fun t -> t.frame_length); 78 + ] 79 + 80 + let wire_size = Wire.Codec.wire_size codec 81 + 82 + let encode_header hdr = 83 + let buf = Bytes.create wire_size in 84 + Wire.Codec.encode codec hdr buf 0; 85 + buf 86 + 87 + let decode_header buf off = 88 + if Bytes.length buf - off < wire_size then Error `Truncated 89 + else 90 + (* Extract frame type bits to validate before full decode *) 91 + let b0 = Char.code (Bytes.get buf off) in 92 + let b1 = Char.code (Bytes.get buf (off + 1)) in 93 + let word0 = (b0 lsl 8) lor b1 in 94 + let ft_bits = (word0 lsr 2) land 0x7 in 95 + match frame_type_of_int ft_bits with 96 + | None -> Error (`Invalid_frame_type ft_bits) 97 + | Some _ -> ( 98 + match Wire.Codec.decode codec buf off with 99 + | Ok hdr -> Ok hdr 100 + | Error _ -> Error `Truncated) 101 + 102 + let decode_header_string s off = decode_header (Bytes.unsafe_of_string s) off 103 + 104 + let pp_header ppf hdr = 105 + Fmt.pf ppf "@[<hov 2>{ ver=%d;@ scid=%d;@ type=%a;@ seq=%d;@ len=%d }@]" 106 + hdr.version hdr.scid pp_frame_type hdr.frame_type hdr.sequence_count 107 + hdr.frame_length
+68
lib/proximity1.mli
··· 1 + (** CCSDS Proximity-1 Space Link Protocol (211.0-B). 2 + 3 + Proximity-1 is a short-range, space-to-space communication protocol designed 4 + for relay links such as Mars orbiter to surface rover. It operates at the 5 + data-link layer and supports reliable and expedited data transfer. 6 + 7 + This module provides encoding and decoding of the Proximity-1 Transfer Frame 8 + header using a Wire codec. 9 + 10 + {b Frame Header Layout} 11 + {v 12 + +-----+------+------+----------+----------+ 13 + | Ver | SCID | Type | Seq Cnt | Frm Len | 14 + | 3b | 8b | 3b | 24 bit | 16 bit | 15 + +-----+------+------+----------+----------+ 16 + v} 17 + 18 + Followed by variable-length frame data. 19 + 20 + @see <https://public.ccsds.org/Pubs/211x0b5.pdf> CCSDS 211.0-B-5 *) 21 + 22 + (** {1 Frame Type} *) 23 + 24 + type frame_type = 25 + | Data (** Type 0: Data frame *) 26 + | Ack (** Type 1: Acknowledgement frame *) 27 + | Expedited (** Type 2: Expedited data frame *) 28 + 29 + val frame_type_of_int : int -> frame_type option 30 + (** [frame_type_of_int n] converts an integer to a frame type. *) 31 + 32 + val int_of_frame_type : frame_type -> int 33 + (** [int_of_frame_type ft] converts a frame type to its wire representation. *) 34 + 35 + val pp_frame_type : frame_type Fmt.t 36 + (** Pretty-print a frame type. *) 37 + 38 + (** {1 Frame Header} *) 39 + 40 + type header = { 41 + version : int; (** Version number (3 bits, normally 0) *) 42 + scid : int; (** Spacecraft Identifier (8 bits) *) 43 + frame_type : frame_type; (** Frame type (3 bits) *) 44 + sequence_count : int; (** Frame sequence count (24 bits, 0--16777215) *) 45 + frame_length : int; (** Frame data length in bytes (16 bits) *) 46 + } 47 + (** Proximity-1 Transfer Frame header (7 bytes). *) 48 + 49 + val codec : header Wire.Codec.t 50 + (** Wire codec for the 7-byte Proximity-1 frame header. *) 51 + 52 + val wire_size : int 53 + (** Size of the frame header in bytes (7). *) 54 + 55 + val encode_header : header -> bytes 56 + (** [encode_header hdr] encodes the header into a fresh buffer. *) 57 + 58 + val decode_header : 59 + bytes -> int -> (header, [ `Truncated | `Invalid_frame_type of int ]) result 60 + (** [decode_header buf off] decodes a header from [buf] at offset [off]. *) 61 + 62 + val decode_header_string : 63 + string -> int -> (header, [ `Truncated | `Invalid_frame_type of int ]) result 64 + (** [decode_header_string s off] decodes a header from string [s] at offset 65 + [off]. *) 66 + 67 + val pp_header : header Fmt.t 68 + (** Pretty-print a frame header. *)
+34
proximity1.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "CCSDS Proximity-1 Space Link Protocol (211.0-B)" 4 + description: 5 + "Proximity-1 frame encoding and decoding for short-range space-to-space communication (e.g., Mars orbiter to rover). Wire codec for the fixed frame header with version, SCID, frame type, sequence count, and frame length fields per CCSDS 211.0-B-5." 6 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 7 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 8 + license: "ISC" 9 + homepage: "https://tangled.org/gazagnaire.org/ocaml-proximity1" 10 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-proximity1/issues" 11 + depends: [ 12 + "dune" {>= "3.21"} 13 + "ocaml" {>= "5.1"} 14 + "wire" {>= "0.9"} 15 + "fmt" {>= "0.9"} 16 + "alcotest" {with-test} 17 + "odoc" {with-doc} 18 + ] 19 + build: [ 20 + ["dune" "subst"] {dev} 21 + [ 22 + "dune" 23 + "build" 24 + "-p" 25 + name 26 + "-j" 27 + jobs 28 + "@install" 29 + "@runtest" {with-test} 30 + "@doc" {with-doc} 31 + ] 32 + ] 33 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-proximity1" 34 + x-maintenance-intent: ["(latest)"]
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries proximity1 alcotest))
+1
test/test.ml
··· 1 + let () = Alcotest.run "proximity1" [ Test_proximity1.suite ]
+185
test/test_proximity1.ml
··· 1 + (** Tests for Proximity-1 Space Link Protocol. 2 + 3 + Wire codec roundtrip tests and field validation per CCSDS 211.0-B-5. *) 4 + 5 + let header = 6 + Alcotest.testable Proximity1.pp_header (fun a b -> 7 + a.Proximity1.version = b.Proximity1.version 8 + && a.scid = b.scid 9 + && a.frame_type = b.frame_type 10 + && a.sequence_count = b.sequence_count 11 + && a.frame_length = b.frame_length) 12 + 13 + (* --- Roundtrip encode/decode for each frame type --- *) 14 + 15 + let test_roundtrip_data () = 16 + let hdr = 17 + Proximity1. 18 + { 19 + version = 0; 20 + scid = 42; 21 + frame_type = Data; 22 + sequence_count = 1000; 23 + frame_length = 256; 24 + } 25 + in 26 + let buf = Proximity1.encode_header hdr in 27 + let result = Proximity1.decode_header buf 0 in 28 + Alcotest.(check (result header string)) 29 + "Data roundtrip" (Ok hdr) 30 + (Result.map_error (fun _ -> "decode error") result) 31 + 32 + let test_roundtrip_ack () = 33 + let hdr = 34 + Proximity1. 35 + { 36 + version = 0; 37 + scid = 100; 38 + frame_type = Ack; 39 + sequence_count = 0; 40 + frame_length = 0; 41 + } 42 + in 43 + let buf = Proximity1.encode_header hdr in 44 + let result = Proximity1.decode_header buf 0 in 45 + Alcotest.(check (result header string)) 46 + "Ack roundtrip" (Ok hdr) 47 + (Result.map_error (fun _ -> "decode error") result) 48 + 49 + let test_roundtrip_expedited () = 50 + let hdr = 51 + Proximity1. 52 + { 53 + version = 0; 54 + scid = 255; 55 + frame_type = Expedited; 56 + sequence_count = 16777215; 57 + frame_length = 65535; 58 + } 59 + in 60 + let buf = Proximity1.encode_header hdr in 61 + let result = Proximity1.decode_header buf 0 in 62 + Alcotest.(check (result header string)) 63 + "Expedited roundtrip" (Ok hdr) 64 + (Result.map_error (fun _ -> "decode error") result) 65 + 66 + (* --- Version field --- *) 67 + 68 + (** Test that version field is always 0 for Proximity-1 Version 1. Per CCSDS 69 + 211.0-B-5, the version number for the current protocol is 0. *) 70 + let test_version_zero () = 71 + let hdr = 72 + Proximity1. 73 + { 74 + version = 0; 75 + scid = 1; 76 + frame_type = Data; 77 + sequence_count = 0; 78 + frame_length = 64; 79 + } 80 + in 81 + let buf = Proximity1.encode_header hdr in 82 + let result = Proximity1.decode_header buf 0 in 83 + match result with 84 + | Ok decoded -> Alcotest.(check int) "version is 0" 0 decoded.version 85 + | Error _ -> Alcotest.fail "decode failed" 86 + 87 + (* --- Truncated input --- *) 88 + 89 + (** Test that decoding truncated input returns error. *) 90 + let test_truncated_input () = 91 + let buf = Bytes.make 3 '\x00' in 92 + let result = Proximity1.decode_header buf 0 in 93 + match result with 94 + | Error `Truncated -> () 95 + | Error (`Invalid_frame_type _) -> 96 + Alcotest.fail "expected Truncated, got Invalid_frame_type" 97 + | Ok _ -> Alcotest.fail "expected error on truncated input" 98 + 99 + (** Test that decode with offset too close to end returns Truncated. *) 100 + let test_truncated_offset () = 101 + let buf = Bytes.make 10 '\x00' in 102 + let result = Proximity1.decode_header buf 5 in 103 + match result with 104 + | Error `Truncated -> () 105 + | Error (`Invalid_frame_type _) -> 106 + Alcotest.fail "expected Truncated, got Invalid_frame_type" 107 + | Ok _ -> Alcotest.fail "expected error on truncated input" 108 + 109 + (** Test decode from string variant. *) 110 + let test_decode_string () = 111 + let hdr = 112 + Proximity1. 113 + { 114 + version = 0; 115 + scid = 50; 116 + frame_type = Data; 117 + sequence_count = 500; 118 + frame_length = 128; 119 + } 120 + in 121 + let buf = Proximity1.encode_header hdr in 122 + let s = Bytes.to_string buf in 123 + let result = Proximity1.decode_header_string s 0 in 124 + Alcotest.(check (result header string)) 125 + "string decode roundtrip" (Ok hdr) 126 + (Result.map_error (fun _ -> "decode error") result) 127 + 128 + (* --- Frame type conversion --- *) 129 + 130 + let test_frame_type_of_int () = 131 + Alcotest.(check (option int)) 132 + "Data = 0" (Some 0) 133 + (Option.map Proximity1.int_of_frame_type (Proximity1.frame_type_of_int 0)); 134 + Alcotest.(check (option int)) 135 + "Ack = 1" (Some 1) 136 + (Option.map Proximity1.int_of_frame_type (Proximity1.frame_type_of_int 1)); 137 + Alcotest.(check (option int)) 138 + "Expedited = 2" (Some 2) 139 + (Option.map Proximity1.int_of_frame_type (Proximity1.frame_type_of_int 2)); 140 + Alcotest.(check bool) 141 + "invalid frame type" true 142 + (Proximity1.frame_type_of_int 7 = None) 143 + 144 + (* --- Maximum field values --- *) 145 + 146 + let test_max_sequence_count () = 147 + let hdr = 148 + Proximity1. 149 + { 150 + version = 0; 151 + scid = 0; 152 + frame_type = Data; 153 + sequence_count = 0xFFFFFF; 154 + frame_length = 0; 155 + } 156 + in 157 + let buf = Proximity1.encode_header hdr in 158 + let result = Proximity1.decode_header buf 0 in 159 + match result with 160 + | Ok decoded -> 161 + Alcotest.(check int) "max seq count" 0xFFFFFF decoded.sequence_count 162 + | Error _ -> Alcotest.fail "decode failed" 163 + 164 + let test_wire_size () = 165 + Alcotest.(check int) "wire size is 7" 7 Proximity1.wire_size 166 + 167 + let suite = 168 + ( "proximity1", 169 + [ 170 + (* Roundtrip *) 171 + Alcotest.test_case "roundtrip Data" `Quick test_roundtrip_data; 172 + Alcotest.test_case "roundtrip Ack" `Quick test_roundtrip_ack; 173 + Alcotest.test_case "roundtrip Expedited" `Quick test_roundtrip_expedited; 174 + (* Version *) 175 + Alcotest.test_case "version is 0" `Quick test_version_zero; 176 + (* Truncated *) 177 + Alcotest.test_case "truncated input" `Quick test_truncated_input; 178 + Alcotest.test_case "truncated offset" `Quick test_truncated_offset; 179 + Alcotest.test_case "decode string" `Quick test_decode_string; 180 + (* Frame type *) 181 + Alcotest.test_case "frame_type_of_int" `Quick test_frame_type_of_int; 182 + (* Field ranges *) 183 + Alcotest.test_case "max sequence count" `Quick test_max_sequence_count; 184 + Alcotest.test_case "wire size" `Quick test_wire_size; 185 + ] )
+1
test/test_proximity1.mli
··· 1 + val suite : string * unit Alcotest.test_case list