CCSDS 133.0-B Space Packet Protocol for OCaml
0
fork

Configure Feed

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

Initial commit: CCSDS 133.0-B Space Packet Protocol

- Pure OCaml implementation of CCSDS space packets
- Primary header encoding/decoding (6 octets)
- Telemetry and telecommand packet types
- Sequence flags for packet segmentation
- bytesrw streaming I/O support
- Alcotest unit tests
- Crowbar fuzz tests

+867
+17
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Dune package management 7 + dune.lock/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+1
.ocamlformat
··· 1 + version=0.28.1
+15
LICENSE.md
··· 1 + ISC License 2 + 3 + Copyright (c) 2025 Thomas Gazagnaire 4 + 5 + Permission to use, copy, modify, and/or distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 + PERFORMANCE OF THIS SOFTWARE.
+106
README.md
··· 1 + # space-packet 2 + 3 + Pure OCaml implementation of CCSDS 133.0-B-2 Space Packet Protocol. 4 + 5 + ## Overview 6 + 7 + Space packets are the fundamental data unit in CCSDS (Consultative Committee 8 + for Space Data Systems) systems. They carry telemetry, telecommand, and 9 + ancillary data between spacecraft and ground systems. 10 + 11 + This library provides encoding and decoding of space packets with streaming 12 + I/O support via bytesrw. 13 + 14 + ## Features 15 + 16 + - Full CCSDS 133.0-B-2 space packet primary header support 17 + - Telemetry and telecommand packet types 18 + - Sequence flags for packet segmentation 19 + - Application Process Identifier (APID) handling 20 + - Streaming I/O with bytesrw 21 + - Zero-copy decoding where possible 22 + 23 + ## Installation 24 + 25 + ``` 26 + opam install space-packet 27 + ``` 28 + 29 + ## Usage 30 + 31 + ```ocaml 32 + (* Create a telemetry packet *) 33 + let pkt = 34 + Space_packet.make_exn 35 + ~packet_type:Telemetry 36 + ~apid:100 37 + ~sequence_flags:Unsegmented 38 + ~sequence_count:42 39 + "sensor data payload" 40 + 41 + (* Encode to bytes *) 42 + let bytes = Space_packet.encode pkt 43 + 44 + (* Decode from bytes *) 45 + match Space_packet.decode bytes with 46 + | Ok decoded -> Printf.printf "APID: %d\n" (Space_packet.apid decoded) 47 + | Error `Truncated -> Printf.printf "Packet truncated\n" 48 + | Error (`Invalid_version v) -> Printf.printf "Invalid version: %d\n" v 49 + ``` 50 + 51 + ### Streaming I/O 52 + 53 + ```ocaml 54 + (* Write to a bytesrw writer *) 55 + Space_packet.write writer pkt 56 + 57 + (* Read from a bytesrw reader *) 58 + match Space_packet.read reader with 59 + | Ok pkt -> (* process packet *) 60 + | Error _ -> (* handle error *) 61 + ``` 62 + 63 + ## Packet Structure 64 + 65 + ``` 66 + +------------------+------------------+ 67 + | Primary Header | Packet Data | 68 + | (6 octets) | (1-65536 octets) | 69 + +------------------+------------------+ 70 + 71 + Primary Header: 72 + +---------+------+-----+------+---------+---------+-------------+ 73 + | Version | Type | SHF | APID | SeqFlag | SeqCnt | Data Length | 74 + | 3 bits | 1b | 1b | 11b | 2 bits | 14 bits | 16 bits | 75 + +---------+------+-----+------+---------+---------+-------------+ 76 + ``` 77 + 78 + ## API 79 + 80 + - `Space_packet.make` - Create a space packet (returns Result) 81 + - `Space_packet.make_exn` - Create a space packet (raises on error) 82 + - `Space_packet.encode` - Serialize packet to bytes 83 + - `Space_packet.decode` - Parse packet from bytes 84 + - `Space_packet.write` - Write packet to bytesrw writer 85 + - `Space_packet.read` - Read packet from bytesrw reader 86 + - `Space_packet.apid` - Get Application Process ID 87 + - `Space_packet.packet_type` - Get packet type (Telemetry/Telecommand) 88 + - `Space_packet.sequence_count` - Get sequence counter 89 + - `Space_packet.is_idle` - Check if packet is an idle packet (APID 2047) 90 + 91 + ## Related Work 92 + 93 + - [CCSDS 133.0-B-2](https://public.ccsds.org/Pubs/133x0b2e2.pdf) - Space Packet 94 + Protocol specification 95 + - [ocaml-sle](https://github.com/gazagnaire/ocaml-sle) - CCSDS Space Link 96 + Extension protocols (uses space packets) 97 + - [ocaml-tcf](https://github.com/gazagnaire/ocaml-tcf) - CCSDS Time Code 98 + Formats (often used in secondary headers) 99 + - [libcsp](https://github.com/libcsp/libcsp) - Cubesat Space Protocol (C) - 100 + different protocol but similar concepts 101 + - [SatCat5](https://github.com/the-aerospace-corporation/satcat5) - Satellite 102 + communication library (C++/VHDL) 103 + 104 + ## License 105 + 106 + ISC License. See [LICENSE.md](LICENSE.md) for details.
+27
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name space-packet) 4 + 5 + (generate_opam_files true) 6 + 7 + (license ISC) 8 + 9 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 10 + 11 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 12 + 13 + (source 14 + (uri https://tangled.org/gazagnaire.org/ocaml-space-packet)) 15 + 16 + (package 17 + (name space-packet) 18 + (synopsis "CCSDS 133.0-B Space Packet Protocol") 19 + (description 20 + "Pure OCaml implementation of CCSDS 133.0-B-2 space packets. Space packets are the fundamental data unit in CCSDS systems, carrying telemetry, telecommand, and ancillary data between spacecraft and ground systems. Uses bytesrw for streaming I/O.") 21 + (depends 22 + (ocaml (>= 5.1)) 23 + (bytesrw (>= 0.1)) 24 + (fmt (>= 0.9)) 25 + (alcotest :with-test) 26 + (crowbar :with-test) 27 + (odoc :with-doc)))
+15
fuzz/dune
··· 1 + ; Crowbar fuzz testing for space-packet 2 + ; 3 + ; To run: dune exec fuzz/fuzz_space_packet.exe 4 + ; With AFL: afl-fuzz -i fuzz/corpus -o fuzz/findings -- ./_build/default/fuzz/fuzz_space_packet.exe @@ 5 + 6 + (executable 7 + (name fuzz_space_packet) 8 + (modules fuzz_space_packet) 9 + (libraries space-packet crowbar)) 10 + 11 + (rule 12 + (alias fuzz) 13 + (deps fuzz_space_packet.exe) 14 + (action 15 + (run %{exe:fuzz_space_packet.exe})))
+73
fuzz/fuzz_space_packet.ml
··· 1 + (** Fuzz tests for Space_packet module. *) 2 + 3 + open Crowbar 4 + 5 + (** Decode - must not crash on arbitrary input. *) 6 + let test_decode_crash buf = 7 + let _ = Space_packet.decode buf in 8 + () 9 + 10 + (** Roundtrip - valid packets must round-trip. *) 11 + let test_roundtrip buf = 12 + match Space_packet.decode buf with 13 + | Error _ -> () (* Invalid input is fine *) 14 + | Ok pkt -> 15 + let encoded = Space_packet.encode pkt in 16 + (match Space_packet.decode encoded with 17 + | Error _ -> fail "re-decode failed" 18 + | Ok pkt' -> if not (Space_packet.equal pkt pkt') then fail "roundtrip mismatch") 19 + 20 + (** Pretty-print - must not crash. *) 21 + let test_pp buf = 22 + match Space_packet.decode buf with 23 + | Error _ -> () 24 + | Ok pkt -> 25 + let _ = Format.asprintf "%a" Space_packet.pp pkt in 26 + () 27 + 28 + (** Make with constrained values - must not crash. *) 29 + let test_make apid_raw seq_raw data = 30 + let apid = apid_raw mod 2048 in 31 + let seq = seq_raw mod 16384 in 32 + match 33 + Space_packet.make ~packet_type:Telemetry ~apid ~sequence_flags:Unsegmented 34 + ~sequence_count:seq data 35 + with 36 + | Ok pkt -> 37 + let encoded = Space_packet.encode pkt in 38 + (match Space_packet.decode encoded with 39 + | Ok pkt' -> 40 + if not (Space_packet.equal pkt pkt') then fail "make roundtrip mismatch" 41 + | Error _ -> fail "make roundtrip decode failed") 42 + | Error (`Data_too_large _) -> () (* Expected for large data *) 43 + | Error _ -> fail "unexpected make error" 44 + 45 + (** Encode length - encoded packet has correct length. *) 46 + let test_encode_length apid_raw seq_raw data = 47 + let apid = apid_raw mod 2048 in 48 + let seq = seq_raw mod 16384 in 49 + let data = 50 + if String.length data > 65536 then String.sub data 0 65536 else data 51 + in 52 + let data = if String.length data = 0 then "\x00" else data in 53 + match 54 + Space_packet.make ~packet_type:Telemetry ~apid ~sequence_flags:Unsegmented 55 + ~sequence_count:seq data 56 + with 57 + | Ok pkt -> 58 + let encoded = Space_packet.encode pkt in 59 + let expected_len = 6 + String.length data in 60 + if String.length encoded <> expected_len then 61 + fail 62 + (Printf.sprintf "length mismatch: got %d, expected %d" 63 + (String.length encoded) expected_len) 64 + | Error _ -> () 65 + 66 + let () = 67 + add_test ~name:"space-packet: decode crash safety" [ bytes ] test_decode_crash; 68 + add_test ~name:"space-packet: roundtrip" [ bytes ] test_roundtrip; 69 + add_test ~name:"space-packet: pp crash safety" [ bytes ] test_pp; 70 + add_test ~name:"space-packet: make roundtrip" [ range 4096; range 32768; bytes ] 71 + test_make; 72 + add_test ~name:"space-packet: encode length" [ range 4096; range 32768; bytes ] 73 + test_encode_length
+4
lib/dune
··· 1 + (library 2 + (name space_packet) 3 + (public_name space-packet) 4 + (libraries bytesrw fmt))
+297
lib/space_packet.ml
··· 1 + (** CCSDS 133.0-B-2 Space Packet Protocol. *) 2 + 3 + module Slice = Bytesrw.Bytes.Slice 4 + 5 + (* {1 Internal helpers for bytesrw} *) 6 + 7 + let write_bytes w buf = 8 + Bytesrw.Bytes.Writer.write w 9 + (Slice.make (Bytes.unsafe_of_string buf) ~first:0 ~length:(String.length buf)) 10 + 11 + let read_byte r = 12 + match Bytesrw.Bytes.Reader.read r with 13 + | slice when Slice.is_eod slice -> None 14 + | slice -> 15 + let data = Slice.bytes slice in 16 + let first = Slice.first slice in 17 + let length = Slice.length slice in 18 + let byte = Bytes.get_uint8 data first in 19 + if length > 1 then begin 20 + let remaining = 21 + Slice.make data ~first:(first + 1) ~length:(length - 1) 22 + in 23 + Bytesrw.Bytes.Reader.push_back r remaining 24 + end; 25 + Some byte 26 + 27 + let read_bytes r n = 28 + let buf = Bytes.create n in 29 + let rec loop i = 30 + if i >= n then Some buf 31 + else 32 + match read_byte r with 33 + | None -> None 34 + | Some byte -> 35 + Bytes.set_uint8 buf i byte; 36 + loop (i + 1) 37 + in 38 + loop 0 39 + 40 + (* {1 Types} *) 41 + 42 + type packet_type = Telemetry | Telecommand 43 + 44 + type sequence_flags = 45 + | Continuation 46 + | First_segment 47 + | Last_segment 48 + | Unsegmented 49 + 50 + type t = { 51 + version : int; 52 + packet_type : packet_type; 53 + secondary_header : bool; 54 + apid : int; 55 + sequence_flags : sequence_flags; 56 + sequence_count : int; 57 + data : string; 58 + } 59 + 60 + (* {1 Header Access} *) 61 + 62 + let version t = t.version 63 + let packet_type t = t.packet_type 64 + let secondary_header_flag t = t.secondary_header 65 + let apid t = t.apid 66 + let sequence_flags t = t.sequence_flags 67 + let sequence_count t = t.sequence_count 68 + let data_length t = String.length t.data - 1 69 + let data t = t.data 70 + 71 + (* {1 Special APIDs} *) 72 + 73 + let apid_idle = 2047 74 + let apid_max = 2046 75 + let is_idle t = t.apid = apid_idle 76 + 77 + (* {1 Construction} *) 78 + 79 + let make ?(version = 0) ~packet_type ?(secondary_header = false) ~apid 80 + ~sequence_flags ~sequence_count data = 81 + if version < 0 || version > 7 then Error (`Invalid_version version) 82 + else if apid < 0 || apid > 2047 then Error (`Invalid_apid apid) 83 + else if sequence_count < 0 || sequence_count > 16383 then 84 + Error (`Invalid_sequence_count sequence_count) 85 + else if String.length data > 65536 then 86 + Error (`Data_too_large (String.length data)) 87 + else 88 + Ok 89 + { 90 + version; 91 + packet_type; 92 + secondary_header; 93 + apid; 94 + sequence_flags; 95 + sequence_count; 96 + data; 97 + } 98 + 99 + let make_exn ?version ~packet_type ?secondary_header ~apid ~sequence_flags 100 + ~sequence_count data = 101 + match 102 + make ?version ~packet_type ?secondary_header ~apid ~sequence_flags 103 + ~sequence_count data 104 + with 105 + | Ok t -> t 106 + | Error (`Invalid_version v) -> 107 + invalid_arg (Printf.sprintf "Space_packet: invalid version %d" v) 108 + | Error (`Invalid_apid a) -> 109 + invalid_arg (Printf.sprintf "Space_packet: invalid APID %d" a) 110 + | Error (`Invalid_sequence_count s) -> 111 + invalid_arg (Printf.sprintf "Space_packet: invalid sequence count %d" s) 112 + | Error (`Data_too_large len) -> 113 + invalid_arg (Printf.sprintf "Space_packet: data too large (%d bytes)" len) 114 + 115 + (* {1 Encoding/Decoding} 116 + 117 + Primary header layout (6 octets): 118 + Octet 0: version (3b) | type (1b) | shf (1b) | apid high (3b) 119 + Octet 1: apid low (8b) 120 + Octet 2: seq_flags (2b) | seq_count high (6b) 121 + Octet 3: seq_count low (8b) 122 + Octet 4: data_length high (8b) 123 + Octet 5: data_length low (8b) 124 + *) 125 + 126 + let packet_type_to_int = function Telemetry -> 0 | Telecommand -> 1 127 + 128 + let packet_type_of_int = function 129 + | 0 -> Telemetry 130 + | 1 -> Telecommand 131 + | _ -> Telemetry (* Should not happen with 1-bit field *) 132 + 133 + let sequence_flags_to_int = function 134 + | Continuation -> 0 135 + | First_segment -> 1 136 + | Last_segment -> 2 137 + | Unsegmented -> 3 138 + 139 + let sequence_flags_of_int = function 140 + | 0 -> Continuation 141 + | 1 -> First_segment 142 + | 2 -> Last_segment 143 + | 3 -> Unsegmented 144 + | _ -> Unsegmented (* Should not happen with 2-bit field *) 145 + 146 + let encode t = 147 + let header = Bytes.create 6 in 148 + let data_len = String.length t.data - 1 in 149 + (* Octet 0: version (3b) | type (1b) | shf (1b) | apid high (3b) *) 150 + let octet0 = 151 + (t.version lsl 5) 152 + lor (packet_type_to_int t.packet_type lsl 4) 153 + lor ((if t.secondary_header then 1 else 0) lsl 3) 154 + lor (t.apid lsr 8) 155 + in 156 + Bytes.set_uint8 header 0 octet0; 157 + (* Octet 1: apid low (8b) *) 158 + Bytes.set_uint8 header 1 (t.apid land 0xFF); 159 + (* Octet 2: seq_flags (2b) | seq_count high (6b) *) 160 + let octet2 = 161 + (sequence_flags_to_int t.sequence_flags lsl 6) lor (t.sequence_count lsr 8) 162 + in 163 + Bytes.set_uint8 header 2 octet2; 164 + (* Octet 3: seq_count low (8b) *) 165 + Bytes.set_uint8 header 3 (t.sequence_count land 0xFF); 166 + (* Octets 4-5: data_length (16b, big-endian) *) 167 + Bytes.set_uint8 header 4 (data_len lsr 8); 168 + Bytes.set_uint8 header 5 (data_len land 0xFF); 169 + Bytes.unsafe_to_string header ^ t.data 170 + 171 + let decode buf = 172 + if String.length buf < 6 then Error `Truncated 173 + else 174 + let octet0 = Char.code buf.[0] in 175 + let octet1 = Char.code buf.[1] in 176 + let octet2 = Char.code buf.[2] in 177 + let octet3 = Char.code buf.[3] in 178 + let octet4 = Char.code buf.[4] in 179 + let octet5 = Char.code buf.[5] in 180 + let version = octet0 lsr 5 in 181 + if version <> 0 then Error (`Invalid_version version) 182 + else 183 + let packet_type = packet_type_of_int ((octet0 lsr 4) land 1) in 184 + let secondary_header = (octet0 lsr 3) land 1 = 1 in 185 + let apid = ((octet0 land 0x07) lsl 8) lor octet1 in 186 + let sequence_flags = sequence_flags_of_int (octet2 lsr 6) in 187 + let sequence_count = ((octet2 land 0x3F) lsl 8) lor octet3 in 188 + let data_length = (octet4 lsl 8) lor octet5 in 189 + let total_len = 6 + data_length + 1 in 190 + if String.length buf < total_len then Error `Truncated 191 + else 192 + let data = String.sub buf 6 (data_length + 1) in 193 + Ok 194 + { 195 + version; 196 + packet_type; 197 + secondary_header; 198 + apid; 199 + sequence_flags; 200 + sequence_count; 201 + data; 202 + } 203 + 204 + (* {1 Streaming I/O} *) 205 + 206 + let write w t = 207 + let buf = encode t in 208 + write_bytes w buf 209 + 210 + let read r = 211 + match read_bytes r 6 with 212 + | None -> Error `Truncated 213 + | Some header -> 214 + let octet0 = Bytes.get_uint8 header 0 in 215 + let version = octet0 lsr 5 in 216 + if version <> 0 then Error (`Invalid_version version) 217 + else 218 + let octet1 = Bytes.get_uint8 header 1 in 219 + let octet2 = Bytes.get_uint8 header 2 in 220 + let octet3 = Bytes.get_uint8 header 3 in 221 + let octet4 = Bytes.get_uint8 header 4 in 222 + let octet5 = Bytes.get_uint8 header 5 in 223 + let packet_type = packet_type_of_int ((octet0 lsr 4) land 1) in 224 + let secondary_header = (octet0 lsr 3) land 1 = 1 in 225 + let apid = ((octet0 land 0x07) lsl 8) lor octet1 in 226 + let sequence_flags = sequence_flags_of_int (octet2 lsr 6) in 227 + let sequence_count = ((octet2 land 0x3F) lsl 8) lor octet3 in 228 + let data_length = (octet4 lsl 8) lor octet5 in 229 + let data_size = data_length + 1 in 230 + (match read_bytes r data_size with 231 + | None -> Error `Truncated 232 + | Some data_bytes -> 233 + Ok 234 + { 235 + version; 236 + packet_type; 237 + secondary_header; 238 + apid; 239 + sequence_flags; 240 + sequence_count; 241 + data = Bytes.unsafe_to_string data_bytes; 242 + }) 243 + 244 + (* {1 Comparison} *) 245 + 246 + let equal a b = 247 + a.version = b.version 248 + && a.packet_type = b.packet_type 249 + && a.secondary_header = b.secondary_header 250 + && a.apid = b.apid 251 + && a.sequence_flags = b.sequence_flags 252 + && a.sequence_count = b.sequence_count 253 + && String.equal a.data b.data 254 + 255 + let compare a b = 256 + let c = Int.compare a.version b.version in 257 + if c <> 0 then c 258 + else 259 + let c = 260 + Int.compare 261 + (packet_type_to_int a.packet_type) 262 + (packet_type_to_int b.packet_type) 263 + in 264 + if c <> 0 then c 265 + else 266 + let c = Bool.compare a.secondary_header b.secondary_header in 267 + if c <> 0 then c 268 + else 269 + let c = Int.compare a.apid b.apid in 270 + if c <> 0 then c 271 + else 272 + let c = 273 + Int.compare 274 + (sequence_flags_to_int a.sequence_flags) 275 + (sequence_flags_to_int b.sequence_flags) 276 + in 277 + if c <> 0 then c 278 + else 279 + let c = Int.compare a.sequence_count b.sequence_count in 280 + if c <> 0 then c else String.compare a.data b.data 281 + 282 + (* {1 Pretty-printing} *) 283 + 284 + let pp_packet_type ppf = function 285 + | Telemetry -> Fmt.string ppf "TM" 286 + | Telecommand -> Fmt.string ppf "TC" 287 + 288 + let pp_sequence_flags ppf = function 289 + | Continuation -> Fmt.string ppf "cont" 290 + | First_segment -> Fmt.string ppf "first" 291 + | Last_segment -> Fmt.string ppf "last" 292 + | Unsegmented -> Fmt.string ppf "unseg" 293 + 294 + let pp ppf t = 295 + Fmt.pf ppf "Space_packet{v%d %a apid=%d seq=%a:%d shf=%b len=%d}" t.version 296 + pp_packet_type t.packet_type t.apid pp_sequence_flags t.sequence_flags 297 + t.sequence_count t.secondary_header (String.length t.data)
+161
lib/space_packet.mli
··· 1 + (** CCSDS 133.0-B-2 Space Packet Protocol. 2 + 3 + Space packets are the fundamental data unit in CCSDS systems, carrying 4 + telemetry, telecommand, and ancillary data between spacecraft and ground 5 + systems. 6 + 7 + {b Packet Structure} 8 + 9 + A space packet consists of a 6-octet primary header followed by a 10 + variable-length data field: 11 + {v 12 + +------------------+------------------+ 13 + | Primary Header | Packet Data | 14 + | (6 octets) | (0-65536 octets) | 15 + +------------------+------------------+ 16 + v} 17 + 18 + {b Primary Header Format} 19 + 20 + {v 21 + +---------+------+-----+------+---------+---------+-------------+ 22 + | Version | Type | SHF | APID | SeqFlag | SeqCnt | Data Length | 23 + | 3 bits | 1b | 1b | 11b | 2 bits | 14 bits | 16 bits | 24 + +---------+------+-----+------+---------+---------+-------------+ 25 + |<-------- Packet ID -------->|<-- Packet Seq Control ->| | 26 + v} 27 + 28 + - Version: Packet Version Number (0 for CCSDS Version 1) 29 + - Type: 0 = telemetry, 1 = telecommand 30 + - SHF: Secondary Header Flag (1 = secondary header present) 31 + - APID: Application Process Identifier (0-2047) 32 + - SeqFlag: Sequence Flags (segmentation control) 33 + - SeqCnt: Packet Sequence Count (0-16383) 34 + - Data Length: Packet Data Length (octets in data field minus 1) 35 + 36 + {b Secondary Header} 37 + 38 + The secondary header format is mission-specific and not defined by this 39 + module. When present (SHF=1), it appears at the start of the packet data 40 + field. Typical contents include timestamps (often CCSDS CUC or CDS format) 41 + and ancillary data. *) 42 + 43 + (** {1 Types} *) 44 + 45 + type packet_type = 46 + | Telemetry (** Packet type 0: telemetry/science data *) 47 + | Telecommand (** Packet type 1: telecommand *) 48 + 49 + type sequence_flags = 50 + | Continuation (** 00: Continuation segment *) 51 + | First_segment (** 01: First segment of a group *) 52 + | Last_segment (** 10: Last segment of a group *) 53 + | Unsegmented (** 11: Unsegmented (standalone packet) *) 54 + 55 + type t 56 + (** A CCSDS space packet with primary header and data field. *) 57 + 58 + (** {1 Header Access} *) 59 + 60 + val version : t -> int 61 + (** [version t] returns the packet version number (always 0 for CCSDS v1). *) 62 + 63 + val packet_type : t -> packet_type 64 + (** [packet_type t] returns the packet type (telemetry or telecommand). *) 65 + 66 + val secondary_header_flag : t -> bool 67 + (** [secondary_header_flag t] returns true if a secondary header is present. *) 68 + 69 + val apid : t -> int 70 + (** [apid t] returns the Application Process Identifier (0-2047). *) 71 + 72 + val sequence_flags : t -> sequence_flags 73 + (** [sequence_flags t] returns the sequence flags. *) 74 + 75 + val sequence_count : t -> int 76 + (** [sequence_count t] returns the Packet Sequence Count (0-16383). *) 77 + 78 + val data_length : t -> int 79 + (** [data_length t] returns the Packet Data Length field value. This is the 80 + number of octets in the data field minus 1. *) 81 + 82 + val data : t -> string 83 + (** [data t] returns the packet data field. *) 84 + 85 + (** {1 Construction} *) 86 + 87 + val make : 88 + ?version:int -> 89 + packet_type:packet_type -> 90 + ?secondary_header:bool -> 91 + apid:int -> 92 + sequence_flags:sequence_flags -> 93 + sequence_count:int -> 94 + string -> 95 + (t, [ `Invalid_version of int | `Invalid_apid of int | `Invalid_sequence_count of int | `Data_too_large of int ]) result 96 + (** [make ~packet_type ~apid ~sequence_flags ~sequence_count data] creates a 97 + space packet. 98 + 99 + @param version Packet version (default 0, must be 0 for CCSDS v1) 100 + @param packet_type Telemetry or telecommand 101 + @param secondary_header Whether a secondary header is present (default false) 102 + @param apid Application Process ID (0-2047) 103 + @param sequence_flags Segmentation flags 104 + @param sequence_count Sequence counter (0-16383) 105 + @param data Packet data field (max 65536 octets) 106 + 107 + Returns [Error] if parameters are out of range. *) 108 + 109 + val make_exn : 110 + ?version:int -> 111 + packet_type:packet_type -> 112 + ?secondary_header:bool -> 113 + apid:int -> 114 + sequence_flags:sequence_flags -> 115 + sequence_count:int -> 116 + string -> 117 + t 118 + (** Like {!make} but raises [Invalid_argument] on error. *) 119 + 120 + (** {1 Special APIDs} *) 121 + 122 + val apid_idle : int 123 + (** APID 2047 (0x7FF): Idle packets (fill data). *) 124 + 125 + val apid_max : int 126 + (** Maximum valid APID value (2046). APID 2047 is reserved for idle packets. *) 127 + 128 + val is_idle : t -> bool 129 + (** [is_idle t] returns true if this is an idle packet (APID = 2047). *) 130 + 131 + (** {1 Encoding/Decoding} *) 132 + 133 + val encode : t -> string 134 + (** [encode t] serializes the packet to bytes (primary header + data). *) 135 + 136 + val decode : string -> (t, [ `Truncated | `Invalid_version of int ]) result 137 + (** [decode buf] parses a space packet from bytes. Returns [Error `Truncated] 138 + if the buffer is too short, or [Error (`Invalid_version v)] if the version 139 + field is not 0. *) 140 + 141 + (** {1 Streaming I/O} *) 142 + 143 + val write : Bytesrw.Bytes.Writer.t -> t -> unit 144 + (** [write w t] writes the packet to a bytesrw writer. *) 145 + 146 + val read : 147 + Bytesrw.Bytes.Reader.t -> (t, [ `Truncated | `Invalid_version of int ]) result 148 + (** [read r] reads a space packet from a bytesrw reader. *) 149 + 150 + (** {1 Comparison} *) 151 + 152 + val equal : t -> t -> bool 153 + val compare : t -> t -> int 154 + 155 + (** {1 Pretty-printing} *) 156 + 157 + val pp : t Fmt.t 158 + (** Pretty-print a space packet (header fields and data length). *) 159 + 160 + val pp_packet_type : packet_type Fmt.t 161 + val pp_sequence_flags : sequence_flags Fmt.t
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries space-packet alcotest))
+1
test/test.ml
··· 1 + let () = Alcotest.run "space-packet" Test_space_packet.suite
+147
test/test_space_packet.ml
··· 1 + (** Tests for Space_packet module. *) 2 + 3 + let packet_testable = 4 + Alcotest.testable Space_packet.pp Space_packet.equal 5 + 6 + let test_make_valid () = 7 + let result = 8 + Space_packet.make ~packet_type:Telemetry ~apid:100 ~sequence_flags:Unsegmented 9 + ~sequence_count:42 "hello" 10 + in 11 + Alcotest.(check bool) "make succeeds" true (Result.is_ok result) 12 + 13 + let test_make_invalid_apid () = 14 + let result = 15 + Space_packet.make ~packet_type:Telemetry ~apid:3000 ~sequence_flags:Unsegmented 16 + ~sequence_count:0 "" 17 + in 18 + match result with 19 + | Error (`Invalid_apid 3000) -> () 20 + | _ -> Alcotest.fail "expected Invalid_apid 3000" 21 + 22 + let test_make_invalid_sequence_count () = 23 + let result = 24 + Space_packet.make ~packet_type:Telemetry ~apid:0 ~sequence_flags:Unsegmented 25 + ~sequence_count:20000 "" 26 + in 27 + match result with 28 + | Error (`Invalid_sequence_count 20000) -> () 29 + | _ -> Alcotest.fail "expected Invalid_sequence_count 20000" 30 + 31 + let test_encode_decode_roundtrip () = 32 + let pkt = 33 + Space_packet.make_exn ~packet_type:Telemetry ~apid:123 ~sequence_flags:Unsegmented 34 + ~sequence_count:456 "test data" 35 + in 36 + let encoded = Space_packet.encode pkt in 37 + match Space_packet.decode encoded with 38 + | Ok decoded -> Alcotest.(check packet_testable) "roundtrip" pkt decoded 39 + | Error `Truncated -> Alcotest.fail "unexpected truncated" 40 + | Error (`Invalid_version v) -> 41 + Alcotest.fail (Printf.sprintf "unexpected invalid version %d" v) 42 + 43 + let test_encode_decode_telecommand () = 44 + let pkt = 45 + Space_packet.make_exn ~packet_type:Telecommand ~apid:2046 ~sequence_flags:First_segment 46 + ~sequence_count:16383 ~secondary_header:true "cmd" 47 + in 48 + let encoded = Space_packet.encode pkt in 49 + match Space_packet.decode encoded with 50 + | Ok decoded -> 51 + Alcotest.(check packet_testable) "telecommand roundtrip" pkt decoded; 52 + Alcotest.(check bool) "is telecommand" true 53 + (Space_packet.packet_type decoded = Telecommand); 54 + Alcotest.(check bool) "has secondary header" true 55 + (Space_packet.secondary_header_flag decoded) 56 + | Error _ -> Alcotest.fail "decode failed" 57 + 58 + let test_encode_decode_empty_data () = 59 + let pkt = 60 + Space_packet.make_exn ~packet_type:Telemetry ~apid:0 ~sequence_flags:Continuation 61 + ~sequence_count:0 "\x00" 62 + in 63 + let encoded = Space_packet.encode pkt in 64 + Alcotest.(check int) "header + 1 byte" 7 (String.length encoded); 65 + match Space_packet.decode encoded with 66 + | Ok decoded -> Alcotest.(check packet_testable) "empty data roundtrip" pkt decoded 67 + | Error _ -> Alcotest.fail "decode failed" 68 + 69 + let test_decode_truncated_header () = 70 + match Space_packet.decode "short" with 71 + | Error `Truncated -> () 72 + | _ -> Alcotest.fail "expected Truncated" 73 + 74 + let test_decode_truncated_data () = 75 + (* Create a valid 6-byte header claiming 100 bytes of data, but provide less *) 76 + let header = "\x00\x00\x00\x00\x00\x63" in (* data_length = 99, so expects 100 bytes *) 77 + match Space_packet.decode (header ^ "short") with 78 + | Error `Truncated -> () 79 + | _ -> Alcotest.fail "expected Truncated" 80 + 81 + let test_idle_packet () = 82 + let pkt = 83 + Space_packet.make_exn ~packet_type:Telemetry ~apid:Space_packet.apid_idle 84 + ~sequence_flags:Unsegmented ~sequence_count:0 "\x00" 85 + in 86 + Alcotest.(check bool) "is idle" true (Space_packet.is_idle pkt); 87 + Alcotest.(check int) "apid is 2047" 2047 (Space_packet.apid pkt) 88 + 89 + let test_sequence_flags () = 90 + let test_flag flag expected_str = 91 + let pkt = 92 + Space_packet.make_exn ~packet_type:Telemetry ~apid:1 ~sequence_flags:flag 93 + ~sequence_count:0 "\x00" 94 + in 95 + let encoded = Space_packet.encode pkt in 96 + match Space_packet.decode encoded with 97 + | Ok decoded -> 98 + let actual = Space_packet.sequence_flags decoded in 99 + Alcotest.(check bool) expected_str true (actual = flag) 100 + | Error _ -> Alcotest.fail "decode failed" 101 + in 102 + test_flag Continuation "continuation"; 103 + test_flag First_segment "first_segment"; 104 + test_flag Last_segment "last_segment"; 105 + test_flag Unsegmented "unsegmented" 106 + 107 + let test_header_fields () = 108 + let pkt = 109 + Space_packet.make_exn ~packet_type:Telecommand ~apid:1234 110 + ~sequence_flags:Last_segment ~sequence_count:5678 ~secondary_header:true 111 + "payload" 112 + in 113 + Alcotest.(check int) "version" 0 (Space_packet.version pkt); 114 + Alcotest.(check bool) "packet_type" true 115 + (Space_packet.packet_type pkt = Telecommand); 116 + Alcotest.(check bool) "secondary_header" true (Space_packet.secondary_header_flag pkt); 117 + Alcotest.(check int) "apid" 1234 (Space_packet.apid pkt); 118 + Alcotest.(check bool) "sequence_flags" true 119 + (Space_packet.sequence_flags pkt = Last_segment); 120 + Alcotest.(check int) "sequence_count" 5678 (Space_packet.sequence_count pkt); 121 + Alcotest.(check int) "data_length" 6 (Space_packet.data_length pkt); 122 + Alcotest.(check string) "data" "payload" (Space_packet.data pkt) 123 + 124 + let suite = 125 + [ 126 + ( "make", 127 + [ 128 + Alcotest.test_case "valid" `Quick test_make_valid; 129 + Alcotest.test_case "invalid_apid" `Quick test_make_invalid_apid; 130 + Alcotest.test_case "invalid_sequence_count" `Quick 131 + test_make_invalid_sequence_count; 132 + ] ); 133 + ( "encode_decode", 134 + [ 135 + Alcotest.test_case "roundtrip" `Quick test_encode_decode_roundtrip; 136 + Alcotest.test_case "telecommand" `Quick test_encode_decode_telecommand; 137 + Alcotest.test_case "empty_data" `Quick test_encode_decode_empty_data; 138 + Alcotest.test_case "truncated_header" `Quick test_decode_truncated_header; 139 + Alcotest.test_case "truncated_data" `Quick test_decode_truncated_data; 140 + ] ); 141 + ( "special", 142 + [ 143 + Alcotest.test_case "idle_packet" `Quick test_idle_packet; 144 + Alcotest.test_case "sequence_flags" `Quick test_sequence_flags; 145 + Alcotest.test_case "header_fields" `Quick test_header_fields; 146 + ] ); 147 + ]