···11+MIT License
22+33+Copyright (c) 2025 Thomas Gazagnaire
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+93
README.md
···11+# tm
22+33+CCSDS TM (Telemetry) Transfer Frames for OCaml.
44+55+## Overview
66+77+This library implements parsing and encoding of CCSDS TM Transfer Frames as
88+specified in CCSDS 132.0-B-3. TM frames are the fundamental data unit for
99+transporting telemetry from spacecraft to ground stations.
1010+1111+## Features
1212+1313+- TM frame primary header (6 bytes) encoding/decoding
1414+- Operational Control Field (OCF) with CLCW support
1515+- Frame Error Control Field (FECF) using CRC-16-CCITT
1616+- Command Link Control Word (CLCW) for COP-1 protocol
1717+- Typed spacecraft ID (SCID) and virtual channel ID (VCID)
1818+- Security-hardened parsing with bounds checking
1919+2020+## Installation
2121+2222+```
2323+opam install tm
2424+```
2525+2626+## Usage
2727+2828+```ocaml
2929+(* Create a TM frame *)
3030+let scid = Tm.scid_exn 100 in
3131+let vcid = Tm.vcid_exn 2 in
3232+let data = String.make 1103 '\x00' in
3333+let frame = Tm.make ~scid ~vcid ~mcfc:1 ~vcfc:2 data in
3434+3535+(* Encode to bytes *)
3636+let bytes = Tm.encode frame in
3737+3838+(* Decode from bytes *)
3939+match Tm.decode bytes with
4040+| Ok frame -> Printf.printf "SCID: %d\n" (Tm.scid_to_int frame.header.scid)
4141+| Error e -> Format.printf "Error: %a\n" Tm.pp_error e
4242+```
4343+4444+### Working with CLCW
4545+4646+```ocaml
4747+(* Extract CLCW from frame OCF *)
4848+match Tm.get_clcw frame with
4949+| Ok clcw ->
5050+ Printf.printf "Report value (N(R)): %d\n" clcw.report_value;
5151+ if clcw.flags.lockout then print_endline "FARM-1 in lockout!"
5252+| Error `No_ocf -> print_endline "No OCF present"
5353+| Error (`Invalid_vcid _) -> print_endline "Invalid VCID in CLCW"
5454+5555+(* Create a CLCW *)
5656+let clcw = Tm.make_clcw ~vcid ~report_value:42 ~lockout:false () in
5757+let ocf_word = Tm.encode_clcw clcw in
5858+```
5959+6060+## Frame Structure
6161+6262+```
6363++----------------+-------------+------+------+
6464+| Primary Header | Data Field | OCF | FECF |
6565+| (6 bytes) | (variable) | (4B) | (2B) |
6666++----------------+-------------+------+------+
6767+```
6868+6969+Standard CCSDS frame length is 1115 bytes (configurable).
7070+7171+## API
7272+7373+### Types
7474+- `Tm.scid` - Spacecraft ID (10 bits, 0-1023)
7575+- `Tm.vcid` - Virtual Channel ID (3 bits, 0-7)
7676+- `Tm.header` - TM frame primary header
7777+- `Tm.clcw` - Command Link Control Word
7878+- `Tm.t` - Complete TM frame
7979+8080+### Functions
8181+- `Tm.decode` - Parse TM frame from bytes
8282+- `Tm.encode` - Serialize TM frame to bytes
8383+- `Tm.compute_fecf` - Calculate CRC-16-CCITT
8484+- `Tm.get_clcw` - Extract CLCW from frame OCF
8585+8686+## References
8787+8888+- [CCSDS 132.0-B-3](https://public.ccsds.org/Pubs/132x0b3.pdf) - TM Space Data Link Protocol specification
8989+- [CCSDS 232.1-B-2](https://public.ccsds.org/Pubs/232x1b2.pdf) - Communications Operation Procedure-1 (COP-1)
9090+9191+## License
9292+9393+MIT License. See [LICENSE.md](LICENSE.md) for details.
+24
dune-project
···11+(lang dune 3.0)
22+33+(name tm)
44+55+(generate_opam_files true)
66+77+(license MIT)
88+(authors "Thomas Gazagnaire <thomas@gazagnaire.org>")
99+(maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>")
1010+1111+(source
1212+ (uri https://tangled.org/gazagnaire.org/ocaml-tm))
1313+1414+(package
1515+ (name tm)
1616+ (synopsis "CCSDS TM Transfer Frames (CCSDS 132.0-B)")
1717+ (description
1818+ "Parser and encoder for CCSDS Telemetry Transfer Frames. Supports TM frame \
1919+ primary headers, Operational Control Field (OCF), Frame Error Control \
2020+ Field (FECF/CRC-16), and CLCW (Command Link Control Word) for COP-1.")
2121+ (depends
2222+ (ocaml (>= 4.14))
2323+ (alcotest :with-test)
2424+ (crowbar :with-test)))
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Fuzz tests for TM frames.
77+88+ Key properties tested: 1. No crashes on malformed input 2. Parser handles
99+ truncated data gracefully 3. Encode/decode roundtrip preserves data 4. CRC
1010+ validation catches corruption
1111+1212+ Security context:
1313+ - TM frames received from spacecraft may be corrupted or malicious
1414+ - Parser must handle any input without crashing
1515+ - Memory safety is critical for ground station software *)
1616+1717+open Crowbar
1818+1919+(* Truncate input to reasonable size to avoid memory issues *)
2020+let truncate ?(max_len = 2048) s =
2121+ if String.length s > max_len then String.sub s 0 max_len else s
2222+2323+(* Property 1: No crashes on arbitrary input *)
2424+let test_decode_no_crash input =
2525+ let input = truncate input in
2626+ let _ = Tm.decode input in
2727+ ()
2828+2929+(* Property 2: No crashes on header parsing *)
3030+let test_header_no_crash input =
3131+ let input = truncate ~max_len:100 input in
3232+ let _ = Tm.decode_header input in
3333+ ()
3434+3535+(* Property 3: Header roundtrip *)
3636+let test_header_roundtrip scid_val vcid_val mcfc vcfc fhp =
3737+ let scid_val = scid_val mod 1024 in
3838+ let vcid_val = vcid_val mod 8 in
3939+ let mcfc = mcfc mod 256 in
4040+ let vcfc = vcfc mod 256 in
4141+ let fhp = fhp mod 2048 in
4242+ match (Tm.scid scid_val, Tm.vcid vcid_val) with
4343+ | Some scid, Some vcid -> (
4444+ let hdr = Tm.make_header ~scid ~vcid ~mcfc ~vcfc ~first_hdr_ptr:fhp () in
4545+ let encoded = Tm.encode_header hdr in
4646+ match Tm.decode_header encoded with
4747+ | Ok decoded ->
4848+ if Tm.scid_to_int decoded.scid <> scid_val then
4949+ failf "scid mismatch: %d vs %d"
5050+ (Tm.scid_to_int decoded.scid)
5151+ scid_val;
5252+ if Tm.vcid_to_int decoded.vcid <> vcid_val then
5353+ failf "vcid mismatch: %d vs %d"
5454+ (Tm.vcid_to_int decoded.vcid)
5555+ vcid_val;
5656+ if decoded.mcfc <> mcfc then
5757+ failf "mcfc mismatch: %d vs %d" decoded.mcfc mcfc;
5858+ if decoded.vcfc <> vcfc then
5959+ failf "vcfc mismatch: %d vs %d" decoded.vcfc vcfc
6060+ | Error _ -> failf "decode failed for valid header")
6161+ | _ -> ()
6262+6363+(* Property 4: CLCW roundtrip *)
6464+let test_clcw_roundtrip vcid_val report farm_b =
6565+ let vcid_val = vcid_val mod 8 in
6666+ let report = report mod 256 in
6767+ let farm_b = farm_b mod 4 in
6868+ match Tm.vcid vcid_val with
6969+ | Some vcid -> (
7070+ let clcw =
7171+ Tm.make_clcw ~vcid ~report_value:report ~farm_b_counter:farm_b ()
7272+ in
7373+ let encoded = Tm.encode_clcw clcw in
7474+ match Tm.decode_clcw encoded with
7575+ | Ok decoded ->
7676+ if decoded.report_value <> report then
7777+ failf "report mismatch: %d vs %d" decoded.report_value report
7878+ | Error _ -> failf "decode failed for valid clcw")
7979+ | None -> ()
8080+8181+(* Property 5: Full frame roundtrip with FECF *)
8282+let test_frame_roundtrip data =
8383+ let data = truncate ~max_len:1103 data in
8484+ if String.length data = 0 then ()
8585+ else
8686+ match (Tm.scid 100, Tm.vcid 2) with
8787+ | Some scid, Some vcid -> (
8888+ let frame = Tm.make ~scid ~vcid ~mcfc:0 ~vcfc:0 data in
8989+ let encoded = Tm.encode frame in
9090+ match Tm.decode ~frame_len:(String.length encoded) encoded with
9191+ | Ok decoded ->
9292+ if decoded.data <> data then fail "data mismatch in roundtrip"
9393+ | Error _ -> fail "decode failed for valid frame")
9494+ | _ -> ()
9595+9696+(* Property 6: FECF detects single-bit corruption *)
9797+let test_fecf_corruption data bit_pos =
9898+ let data = truncate ~max_len:1103 data in
9999+ if String.length data < 10 then ()
100100+ else
101101+ match (Tm.scid 0, Tm.vcid 0) with
102102+ | Some scid, Some vcid -> (
103103+ let frame = Tm.make ~scid ~vcid ~mcfc:0 ~vcfc:0 data in
104104+ let encoded = Tm.encode frame in
105105+ let len = String.length encoded in
106106+ if len < 3 then ()
107107+ else
108108+ (* Flip a bit in the frame (not in FECF itself) *)
109109+ let byte_pos = bit_pos mod (len - 2) in
110110+ let bit = bit_pos / (len - 2) mod 8 in
111111+ let bytes = Bytes.of_string encoded in
112112+ let old_byte = Bytes.get bytes byte_pos in
113113+ let new_byte = Char.chr (Char.code old_byte lxor (1 lsl bit)) in
114114+ Bytes.set bytes byte_pos new_byte;
115115+ let corrupted = Bytes.to_string bytes in
116116+ match Tm.decode ~frame_len:len corrupted with
117117+ | Error (Tm.Fecf_mismatch _) -> () (* Expected *)
118118+ | Error _ -> () (* Other errors are acceptable *)
119119+ | Ok _ ->
120120+ (* CRC collision - very rare but possible *)
121121+ ())
122122+ | _ -> ()
123123+124124+let () =
125125+ add_test ~name:"tm: decode no crash" [ bytes ] test_decode_no_crash;
126126+ add_test ~name:"tm: header no crash" [ bytes ] test_header_no_crash;
127127+ add_test ~name:"tm: header roundtrip"
128128+ [ range 2000; range 20; range 300; range 300; range 3000 ]
129129+ test_header_roundtrip;
130130+ add_test ~name:"tm: clcw roundtrip"
131131+ [ range 20; range 300; range 10 ]
132132+ test_clcw_roundtrip;
133133+ add_test ~name:"tm: frame roundtrip" [ bytes ] test_frame_roundtrip;
134134+ add_test ~name:"tm: fecf corruption"
135135+ [ bytes; range 20000 ]
136136+ test_fecf_corruption