CCSDS Synchronization and Channel Coding (131.0-B, 231.0-B)
0
fork

Configure Feed

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

Merge ocaml-cltu + ocaml-ccsds-coding into ocaml-scc

Create a single package matching the CCSDS standard name
"Synchronization and Channel Coding" (131.0-B, 231.0-B):

Scc.Sync — CLTU/BCH, ASM markers, stream sync parsers
Scc.Coding — randomizer, RS interleaving, FEC presets

API: Scc.Sync.Cltu.encode, Scc.Coding.Randomizer.create, etc.

Remove ocaml-cltu, ocaml-ccsds-coding, ocaml-transport (superseded).

+2582
+83
README.md
··· 1 + # ocaml-scc — CCSDS Synchronization and Channel Coding 2 + 3 + Pure OCaml implementation of the CCSDS Synchronization and Channel Coding 4 + sublayer per [CCSDS 131.0-B](https://public.ccsds.org/Pubs/131x0b4.pdf) (TM) 5 + and [CCSDS 231.0-B](https://public.ccsds.org/Pubs/231x0b4e1.pdf) (TC). 6 + 7 + ## Overview 8 + 9 + The SCC sublayer sits between the Physical Layer and the Data Link Protocol 10 + sublayer. It handles frame synchronization and forward error correction: 11 + 12 + ``` 13 + Space Packet / TC Frame / TM Frame 14 + | 15 + +----------v-----------+ 16 + | Data Link Protocol | ocaml-tc, ocaml-tm, ocaml-aos, ... 17 + +-----------+-----------+ 18 + | 19 + +----------v-----------+ 20 + | Synchronization & | ** this package ** 21 + | Channel Coding (SCC) | 22 + +-----------+-----------+ 23 + | 24 + Physical Layer (RF) 25 + ``` 26 + 27 + ## Modules 28 + 29 + ### Synchronization (`Scc.Sync`) 30 + 31 + | Module | Standard | Description | 32 + |--------|----------|-------------| 33 + | `Scc.Sync.Cltu` | 231.0-B | CLTU encode/decode with BCH(63,56) | 34 + | `Scc.Sync.Asm` | 131.0-B | ASM marker (0x1ACFFC1D) insert/detect | 35 + | `Scc.Sync.Cltu_sync` | 231.0-B | Stream parser for CLTU extraction | 36 + | `Scc.Sync.Asm_sync` | 131.0-B | Stream parser for ASM-framed data | 37 + 38 + ### Channel Coding (`Scc.Coding`) 39 + 40 + | Module | Standard | Description | 41 + |--------|----------|-------------| 42 + | `Scc.Coding.Randomizer` | 131.0-B §9 | LFSR pseudo-random sequence | 43 + | `Scc.Coding.Reed_solomon` | 131.0-B §4 | RS(255,223) with interleaving I=1..8 | 44 + | `Scc.Coding.Convolutional` | 131.0-B §3 | Rate 1/2, K=7 (G1=0x79, G2=0x5B) | 45 + | `Scc.Coding.Turbo` | 131.0-B §6 | Parallel concatenated + BCJR/MAP | 46 + | `Scc.Coding.Ldpc` | 131.0-B §7 | Belief propagation decoder | 47 + 48 + The generic algorithms live in standalone packages (`ocaml-reed-solomon`, 49 + `ocaml-viterbi`, `ocaml-turbo`, `ocaml-ldpc`). This package provides the 50 + CCSDS-specific parameterizations. 51 + 52 + ## Usage 53 + 54 + ```ocaml 55 + (* TC uplink: frame -> CLTU *) 56 + let cltu = Scc.Sync.Cltu.encode tc_frame 57 + 58 + (* TM downlink: randomize + RS encode + convolutional encode *) 59 + let r = Scc.Coding.Randomizer.create () in 60 + Scc.Coding.Randomizer.apply r frame 0 (Bytes.length frame); 61 + let codeword = Scc.Coding.Reed_solomon.encode ~interleave:I5 frame in 62 + let coded = Scc.Coding.Convolutional.encode codeword 63 + 64 + (* Stream parsing: extract CLTUs from raw bytes *) 65 + let state = ref (Scc.Sync.Cltu_sync.init ()) in 66 + let new_state, result = Scc.Sync.Cltu_sync.feed !state raw_bytes in 67 + state := new_state 68 + ``` 69 + 70 + ## Eio Transport (`scc-eio`) 71 + 72 + The `scc-eio` package provides Eio flow-based send/recv: 73 + 74 + ```ocaml 75 + (* Receive CLTU-framed TC from a raw byte stream *) 76 + match Scc_eio.Cltu.recv flow with 77 + | Ok frame -> process_tc frame 78 + | Error `Closed -> () 79 + | Error (`Transport e) -> log_error e 80 + 81 + (* Send ASM-framed TM *) 82 + Scc_eio.Asm.send flow tm_frame 83 + ```
+37
dune-project
··· 1 + (lang dune 3.21) 2 + (name scc) 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-scc)) 8 + (package 9 + (name scc) 10 + (synopsis "CCSDS Synchronization and Channel Coding (131.0-B, 231.0-B)") 11 + (description 12 + "Synchronization and Channel Coding sublayer per CCSDS 131.0-B \ 13 + and 231.0-B. Sync: ASM markers, CLTU with BCH(63,56). Coding: \ 14 + pseudo-random randomizer (LFSR), RS(255,223) with interleaving, \ 15 + and CCSDS presets for convolutional, turbo, and LDPC codes.") 16 + (depends 17 + (ocaml (>= 5.1)) 18 + (reed-solomon (>= 0.1)) 19 + (viterbi (>= 0.1)) 20 + (turbo (>= 0.1)) 21 + (ldpc (>= 0.1)) 22 + (fmt (>= 0.9)) 23 + (logs (>= 0.7)) 24 + (alcotest :with-test) 25 + (alcobar :with-test))) 26 + 27 + (package 28 + (name scc-eio) 29 + (synopsis "Eio-based CCSDS SCC transport (CLTU, ASM)") 30 + (depends 31 + (ocaml (>= 5.1)) 32 + (scc (= :version)) 33 + (eio (>= 1.0)) 34 + (bytesrw-eio (>= 0.1)) 35 + (cstruct (>= 6.0)) 36 + (fmt (>= 0.9)) 37 + (logs (>= 0.7))))
+4
eio/dune
··· 1 + (library 2 + (name scc_eio) 3 + (public_name scc-eio) 4 + (libraries scc eio bytesrw-eio cstruct fmt logs))
+97
eio/scc_eio.ml
··· 1 + (** Eio-based CLTU/ASM transport. 2 + 3 + This module provides Eio flow-based send/receive functions that use the pure 4 + encoding/decoding from {!Cltu}. 5 + 6 + Uses CCSDS-standard framing with sync pattern detection: 7 + - CLTU: Start sequence (0xEB90) + BCH codeblocks + tail sequence 8 + - ASM: Sync marker (0x1ACFFC1D) + frame 9 + 10 + Frame boundaries are detected from sync patterns only - no length prefixes. 11 + This matches realistic space link behavior. *) 12 + 13 + module S = Scc.Sync 14 + 15 + type error = S.error 16 + 17 + (** {1 CLTU transport} 18 + 19 + CLTU framing using the pure sync parser from {!Cltu.Cltu_sync}. Reads raw 20 + bytes from the stream and feeds them to the parser. *) 21 + module Cltu = struct 22 + let recv (flow : _ Eio.Flow.source) = 23 + try 24 + let reader = Eio.Buf_read.of_flow ~max_size:S.Cltu_sync.max_cltu flow in 25 + let state = ref (S.Cltu_sync.init ()) in 26 + let rec read_until_frame () = 27 + let byte = Eio.Buf_read.any_char reader in 28 + let new_state, result = S.Cltu_sync.feed !state (Bytes.make 1 byte) in 29 + state := new_state; 30 + match result with 31 + | None -> read_until_frame () 32 + | Some (Ok frame) -> Ok frame 33 + | Some (Error e) -> Error (`Transport e) 34 + in 35 + read_until_frame () 36 + with 37 + | End_of_file -> Error `Closed 38 + | exn -> Error (`Error (Printexc.to_string exn)) 39 + 40 + let send (flow : _ Eio.Flow.sink) frame = 41 + try 42 + let cltu = S.Cltu.encode frame in 43 + Eio.Flow.copy_string (Bytes.to_string cltu) flow; 44 + Ok () 45 + with exn -> Error (`Error (Printexc.to_string exn)) 46 + end 47 + 48 + (** {1 ASM transport} 49 + 50 + ASM framing using the pure sync parser from {!Cltu.Asm_sync}. Reads raw 51 + bytes from the stream and feeds them to the parser. *) 52 + module Asm = struct 53 + let recv_fixed ~frame_len (flow : _ Eio.Flow.source) = 54 + try 55 + let reader = Eio.Buf_read.of_flow ~max_size:(4 + frame_len) flow in 56 + let state = ref (S.Asm_sync.init ~frame_len) in 57 + let rec read_until_frame () = 58 + let byte = Eio.Buf_read.any_char reader in 59 + let new_state, result = S.Asm_sync.feed !state (Bytes.make 1 byte) in 60 + state := new_state; 61 + match result with 62 + | None -> read_until_frame () 63 + | Some (Ok frame) -> Ok frame 64 + | Some (Error e) -> Error (`Transport e) 65 + in 66 + read_until_frame () 67 + with 68 + | End_of_file -> Error `Closed 69 + | exn -> Error (`Error (Printexc.to_string exn)) 70 + 71 + let recv_tc (flow : _ Eio.Flow.source) = 72 + try 73 + let reader = Eio.Buf_read.of_flow ~max_size:65536 flow in 74 + let state = ref (S.Asm_sync.init_tc ()) in 75 + let rec read_until_frame () = 76 + let byte = Eio.Buf_read.any_char reader in 77 + let new_state, result = S.Asm_sync.feed !state (Bytes.make 1 byte) in 78 + state := new_state; 79 + match result with 80 + | None -> read_until_frame () 81 + | Some (Ok frame) -> Ok frame 82 + | Some (Error e) -> Error (`Transport e) 83 + in 84 + read_until_frame () 85 + with 86 + | End_of_file -> Error `Closed 87 + | exn -> Error (`Error (Printexc.to_string exn)) 88 + 89 + let recv (flow : _ Eio.Flow.source) = recv_tc flow 90 + 91 + let send (flow : _ Eio.Flow.sink) frame = 92 + try 93 + let data = S.Asm.encode frame in 94 + Eio.Flow.copy_string (Bytes.to_string data) flow; 95 + Ok () 96 + with exn -> Error (`Error (Printexc.to_string exn)) 97 + end
+71
eio/scc_eio.mli
··· 1 + (** Eio-based transport layer. 2 + 3 + This module provides Eio flow-based send/receive functions that use the pure 4 + encoding/decoding from {!Transport}. 5 + 6 + Uses CCSDS-standard framing with sync pattern detection: 7 + - CLTU: Start sequence (0xEB90) + BCH codeblocks + tail sequence 8 + - ASM: Sync marker (0x1ACFFC1D) + frame 9 + 10 + Frame boundaries are detected from sync patterns only - no length prefixes. 11 + This matches realistic space link behavior. *) 12 + 13 + type error = Scc.Sync.error 14 + 15 + (** {1 CLTU transport} 16 + 17 + CLTU (Command Link Transfer Unit) framing per CCSDS 231.0-B-4. 18 + 19 + Searches for start sequence (0xEB90) in the raw byte stream, reads 20 + codeblocks until tail sequence (0xC5C5C5C5C5C5C579), and verifies BCH(63,56) 21 + parity. *) 22 + module Cltu : sig 23 + val recv : 24 + _ Eio.Flow.source -> 25 + (bytes, [> `Closed | `Transport of error | `Error of string ]) result 26 + (** Receive a CLTU-encoded frame from a raw byte stream. 27 + 28 + Returns: 29 + - [Ok frame] on successful decode 30 + - [Error `Closed] on end of stream 31 + - [Error (`Transport e)] on BCH or format error 32 + - [Error (`Error msg)] on I/O error *) 33 + 34 + val send : _ Eio.Flow.sink -> bytes -> (unit, [> `Error of string ]) result 35 + (** Send a CLTU-encoded frame to a raw byte stream. *) 36 + end 37 + 38 + (** {1 ASM transport} 39 + 40 + ASM (Attached Sync Marker) framing for TM/AOS downlink per CCSDS 131.0-B-4. 41 + 42 + Searches for ASM marker (0x1ACFFC1D) in the raw byte stream and reads the 43 + following frame. *) 44 + module Asm : sig 45 + val recv_fixed : 46 + frame_len:int -> 47 + _ Eio.Flow.source -> 48 + (bytes, [> `Closed | `Transport of error | `Error of string ]) result 49 + (** Receive a fixed-length ASM-prefixed frame (for TM/AOS). 50 + 51 + Returns: 52 + - [Ok frame] on success 53 + - [Error `Closed] on end of stream 54 + - [Error (`Transport e)] on format error 55 + - [Error (`Error msg)] on I/O error *) 56 + 57 + val recv_tc : 58 + _ Eio.Flow.source -> 59 + (bytes, [> `Closed | `Transport of error | `Error of string ]) result 60 + (** Receive a variable-length ASM-prefixed TC frame. 61 + 62 + Parses TC header to determine frame length. *) 63 + 64 + val recv : 65 + _ Eio.Flow.source -> 66 + (bytes, [> `Closed | `Transport of error | `Error of string ]) result 67 + (** Receive an ASM-prefixed frame (default: variable-length TC). *) 68 + 69 + val send : _ Eio.Flow.sink -> bytes -> (unit, [> `Error of string ]) result 70 + (** Send an ASM-prefixed frame to a raw byte stream. *) 71 + end
+22
fuzz/dune
··· 1 + (executable 2 + (name fuzz) 3 + (modules fuzz fuzz_sync fuzz_coding) 4 + (libraries scc alcobar)) 5 + 6 + (rule 7 + (alias runtest) 8 + (enabled_if 9 + (<> %{profile} afl)) 10 + (deps fuzz.exe) 11 + (action 12 + (run %{exe:fuzz.exe}))) 13 + 14 + (rule 15 + (alias fuzz) 16 + (enabled_if 17 + (= %{profile} afl)) 18 + (deps fuzz.exe) 19 + (action 20 + (progn 21 + (run %{exe:fuzz.exe} --gen-corpus corpus) 22 + (run afl-fuzz -V 60 -i corpus -o _fuzz -- %{exe:fuzz.exe} @@))))
+1
fuzz/fuzz.ml
··· 1 + let () = Alcobar.run "scc" [ Fuzz_sync.suite; Fuzz_coding.suite ]
+78
fuzz/fuzz_coding.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + open Alcobar 7 + 8 + (** Roundtrip: random bytes -> randomize -> derandomize -> verify equal. 9 + 10 + The randomizer XORs with a deterministic LFSR sequence. Applying it twice 11 + with a fresh (reset) randomizer each time yields the original data. *) 12 + let test_roundtrip buf = 13 + let r = Scc.Coding.Randomizer.create () in 14 + let randomized = Scc.Coding.Randomizer.apply_string r buf in 15 + Scc.Coding.Randomizer.reset r; 16 + let derandomized = Scc.Coding.Randomizer.apply_string r randomized in 17 + check_eq ~pp:pp_string buf derandomized 18 + 19 + (** Self-inverse property: randomize applied twice is identity. 20 + 21 + XOR is its own inverse, so apply(apply(data)) = data when the randomizer is 22 + reset between applications. *) 23 + let test_self_inverse buf = 24 + let r1 = Scc.Coding.Randomizer.create () in 25 + let r2 = Scc.Coding.Randomizer.create () in 26 + let once = Scc.Coding.Randomizer.apply_string r1 buf in 27 + let twice = Scc.Coding.Randomizer.apply_string r2 once in 28 + check_eq ~pp:pp_string buf twice 29 + 30 + (** In-place apply matches apply_string. *) 31 + let test_apply_consistency buf = 32 + let r1 = Scc.Coding.Randomizer.create () in 33 + let result_string = Scc.Coding.Randomizer.apply_string r1 buf in 34 + let r2 = Scc.Coding.Randomizer.create () in 35 + let buf_copy = Bytes.of_string buf in 36 + Scc.Coding.Randomizer.apply r2 buf_copy 0 (Bytes.length buf_copy); 37 + let result_inplace = Bytes.to_string buf_copy in 38 + check_eq ~pp:pp_string result_string result_inplace 39 + 40 + (** Sequence determinism: sequence(n) matches byte-by-byte next_byte. *) 41 + let test_sequence_determinism len_raw = 42 + let len = len_raw mod 512 in 43 + let seq = Scc.Coding.Randomizer.sequence len in 44 + let r = Scc.Coding.Randomizer.create () in 45 + for i = 0 to len - 1 do 46 + let expected = Char.code (Bytes.get seq i) in 47 + let got = Scc.Coding.Randomizer.next_byte r in 48 + if expected <> got then 49 + fail 50 + (Fmt.str "sequence mismatch at byte %d: expected 0x%02X, got 0x%02X" i 51 + expected got) 52 + done 53 + 54 + (** Randomized data differs from input (for non-empty, non-zero input). 55 + 56 + This is a weak property check: for any non-trivial input, the randomized 57 + output should differ (the LFSR sequence is non-zero for the first ~255 bytes 58 + at least). *) 59 + let test_randomized_differs buf = 60 + if String.length buf > 0 then begin 61 + let r = Scc.Coding.Randomizer.create () in 62 + let randomized = Scc.Coding.Randomizer.apply_string r buf in 63 + (* The zero string would XOR to just the LFSR sequence, which is non-zero. 64 + Any non-zero input XORed with a non-zero sequence will almost certainly 65 + differ from the input. We just check it's not identical. *) 66 + if buf = randomized && String.length buf > 0 then 67 + fail "randomized output is identical to input" 68 + end 69 + 70 + let suite = 71 + ( "tm_sync", 72 + [ 73 + test_case "roundtrip" [ bytes ] test_roundtrip; 74 + test_case "self-inverse" [ bytes ] test_self_inverse; 75 + test_case "apply consistency" [ bytes ] test_apply_consistency; 76 + test_case "sequence determinism" [ range 512 ] test_sequence_determinism; 77 + test_case "randomized differs" [ bytes ] test_randomized_differs; 78 + ] )
+4
fuzz/fuzz_coding.mli
··· 1 + (** Fuzz tests for {!Ccsds_coding}. *) 2 + 3 + val suite : string * Alcobar.test_case list 4 + (** Test suite. *)
+71
fuzz/fuzz_sync.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + open Alcobar 7 + 8 + (** Roundtrip: random bytes -> Scc.Sync.encode -> Scc.Sync.decode -> verify 9 + equal. *) 10 + let test_roundtrip buf = 11 + guard (String.length buf > 0); 12 + let frame = Bytes.of_string buf in 13 + let cltu = Scc.Sync.Cltu.encode frame in 14 + match Scc.Sync.Cltu.decode cltu with 15 + | Error e -> fail (Fmt.str "decode failed: %a" Scc.Sync.pp_error e) 16 + | Ok (data, _remaining) -> 17 + (* Decoded data includes fill padding, so check prefix matches *) 18 + let frame_len = Bytes.length frame in 19 + if Bytes.length data < frame_len then 20 + fail 21 + (Fmt.str "decoded data too short: got %d, expected >= %d" 22 + (Bytes.length data) frame_len) 23 + else 24 + let prefix = Bytes.sub data 0 frame_len in 25 + if prefix <> frame then fail "roundtrip mismatch: prefix differs" 26 + 27 + (** Crash safety: random bytes -> Scc.Sync.decode -> should not crash. *) 28 + let test_decode_crash buf = 29 + let _ = Scc.Sync.Cltu.decode (Bytes.of_string buf) in 30 + () 31 + 32 + (** BCH parity: random 7-byte block -> compute parity -> verify syndrome. *) 33 + let test_bch_parity buf = 34 + (* Pad or truncate to exactly 8 bytes (7 data + 1 parity) *) 35 + let block = Bytes.make 8 '\000' in 36 + let len = min 7 (String.length buf) in 37 + Bytes.blit_string buf 0 block 0 len; 38 + let parity = Scc.Sync.Cltu.bch_parity block 0 in 39 + Bytes.set block 7 parity; 40 + if not (Scc.Sync.Cltu.bch_verify block 0 parity) then 41 + fail "BCH verify failed on freshly computed parity" 42 + 43 + (** ASM roundtrip: random bytes -> Asm.encode -> Asm.decode -> verify equal. *) 44 + let test_asm_roundtrip buf = 45 + let frame = Bytes.of_string buf in 46 + let encoded = Scc.Sync.Asm.encode frame in 47 + match Scc.Sync.Asm.decode encoded with 48 + | Error e -> fail (Fmt.str "ASM decode failed: %a" Scc.Sync.pp_error e) 49 + | Ok (decoded, _) -> if decoded <> frame then fail "ASM roundtrip mismatch" 50 + 51 + (** ASM decode crash safety. *) 52 + let test_asm_decode_crash buf = 53 + let _ = Scc.Sync.Asm.decode (Bytes.of_string buf) in 54 + () 55 + 56 + (** Cltu_sync crash safety: feed random bytes to sync parser. *) 57 + let test_cltu_sync_crash buf = 58 + let state = Scc.Sync.Cltu_sync.init () in 59 + let _ = Scc.Sync.Cltu_sync.feed state (Bytes.of_string buf) in 60 + () 61 + 62 + let suite = 63 + ( "cltu", 64 + [ 65 + test_case "CLTU roundtrip" [ bytes ] test_roundtrip; 66 + test_case "CLTU decode crash safety" [ bytes ] test_decode_crash; 67 + test_case "BCH parity" [ bytes ] test_bch_parity; 68 + test_case "ASM roundtrip" [ bytes ] test_asm_roundtrip; 69 + test_case "ASM decode crash safety" [ bytes ] test_asm_decode_crash; 70 + test_case "Cltu_sync crash safety" [ bytes ] test_cltu_sync_crash; 71 + ] )
+4
fuzz/fuzz_sync.mli
··· 1 + (** Fuzz tests for {!Cltu}. *) 2 + 3 + val suite : string * Alcobar.test_case list 4 + (** Test suite. *)
+199
lib/coding.ml
··· 1 + (** CCSDS TM Synchronization and Channel Coding (131.0-B). 2 + 3 + @see <https://public.ccsds.org/Pubs/131x0b4.pdf> CCSDS 131.0-B-4 *) 4 + 5 + (* {1 Randomizer} 6 + 7 + CCSDS pseudo-random sequence for TM frame randomization. 8 + 9 + LFSR polynomial: h(x) = x^8 + x^7 + x^5 + x^3 + 1 10 + Initial state: 0xFF (all ones) 11 + 12 + The LFSR shifts right by one position each clock. The output bit comes 13 + from bit 0 (the rightmost stage). The feedback bit, entering at bit 7 14 + (the leftmost stage), is the XOR of bits at positions 0, 3, 5, 7. 15 + 16 + Output bits are packed MSB-first into bytes: the first output bit goes to 17 + bit 7 of the first byte. 18 + 19 + Test vector (first 20 bytes): 20 + FF 48 0E C0 9A 0D 70 BC 8E 2C 93 AD A7 B7 46 CE 5A 97 7D CC 21 + 22 + Per CCSDS 131.0-B-4 Section 9, the sequence is applied to all bits of 23 + the Transfer Frame (excluding the ASM) starting from the first bit after 24 + the ASM. The LFSR is reset to all-ones at the start of each frame. *) 25 + 26 + module Randomizer = struct 27 + type t = { mutable state : int } 28 + 29 + let create () = { state = 0xFF } 30 + let reset t = t.state <- 0xFF 31 + 32 + (* Advance the LFSR by one bit. 33 + 34 + CCSDS 131.0-B-4 Section 9: the LFSR shifts right. The output bit comes 35 + from bit 0 (the rightmost stage). The feedback bit, computed from the 36 + polynomial h(x) = x^8 + x^7 + x^5 + x^3 + 1, enters at bit 7 (the 37 + leftmost stage). 38 + 39 + Feedback taps (0-indexed from rightmost): bits 0, 2, 4, 6. 40 + These correspond to polynomial terms: 1, x^3, x^5, x^7 when mapped 41 + through the shift-right register convention. 42 + 43 + Output bits are packed MSB-first into bytes: the first output bit goes 44 + to bit 7 of the first byte. *) 45 + let step t = 46 + let s = t.state in 47 + let out = s land 1 in 48 + (* Feedback: XOR of bits at positions 0, 3, 5, 7 49 + corresponding to polynomial terms 1, x^3, x^5, x^7 *) 50 + let xor_all = s lxor (s lsr 3) lxor (s lsr 5) lxor (s lsr 7) in 51 + let fb = xor_all land 1 in 52 + t.state <- (s lsr 1) lor (fb lsl 7); 53 + out 54 + 55 + let next_byte t = 56 + let b = ref 0 in 57 + for _ = 0 to 7 do 58 + b := (!b lsl 1) lor step t 59 + done; 60 + !b 61 + 62 + let apply t buf off len = 63 + for i = 0 to len - 1 do 64 + let r = next_byte t in 65 + let orig = Char.code (Bytes.get buf (off + i)) in 66 + Bytes.set buf (off + i) (Char.chr (orig lxor r)) 67 + done 68 + 69 + let apply_string t s = 70 + let buf = Bytes.of_string s in 71 + apply t buf 0 (Bytes.length buf); 72 + Bytes.unsafe_to_string buf 73 + 74 + let sequence n = 75 + let t = create () in 76 + let buf = Bytes.create n in 77 + for i = 0 to n - 1 do 78 + Bytes.set buf i (Char.chr (next_byte t)) 79 + done; 80 + buf 81 + end 82 + 83 + (* {1 Reed-Solomon} 84 + 85 + CCSDS RS(255,223) with interleaving. 86 + 87 + For interleave depth I: 88 + - Encode: split the input (223*I bytes) into I sub-streams via round-robin 89 + (byte j goes to sub-stream j mod I), encode each sub-stream independently 90 + with RS(255,223), then interleave the I codewords back into a single 91 + output of 255*I bytes. 92 + - Decode: de-interleave the received 255*I bytes into I sub-streams, 93 + decode each independently, then re-interleave the I data blocks. *) 94 + 95 + module Reed_solomon = struct 96 + type interleave = I1 | I2 | I3 | I4 | I5 | I8 97 + 98 + let interleave_depth = function 99 + | I1 -> 1 100 + | I2 -> 2 101 + | I3 -> 3 102 + | I4 -> 4 103 + | I5 -> 5 104 + | I8 -> 8 105 + 106 + let rs = Reed_solomon.ccsds 107 + 108 + let encode ?(interleave = I1) data = 109 + let depth = interleave_depth interleave in 110 + let data_len = rs.k * depth in 111 + if Bytes.length data <> data_len then 112 + invalid_arg 113 + (Fmt.str "Reed_solomon.encode: expected %d bytes (223*%d), got %d" 114 + data_len depth (Bytes.length data)); 115 + if depth = 1 then Reed_solomon.encode_systematic rs data 116 + else begin 117 + (* De-interleave input into [depth] sub-streams of 223 bytes each *) 118 + let subs = Array.init depth (fun _ -> Bytes.make rs.k '\x00') in 119 + for i = 0 to Bytes.length data - 1 do 120 + let sub_idx = i mod depth in 121 + let byte_idx = i / depth in 122 + Bytes.set subs.(sub_idx) byte_idx (Bytes.get data i) 123 + done; 124 + (* Encode each sub-stream *) 125 + let coded = Array.map (Reed_solomon.encode_systematic rs) subs in 126 + (* Interleave the [depth] codewords of 255 bytes each *) 127 + let out_len = rs.n * depth in 128 + let out = Bytes.make out_len '\x00' in 129 + for sub = 0 to depth - 1 do 130 + for j = 0 to rs.n - 1 do 131 + Bytes.set out ((j * depth) + sub) (Bytes.get coded.(sub) j) 132 + done 133 + done; 134 + out 135 + end 136 + 137 + let decode ?(interleave = I1) codeword = 138 + let depth = interleave_depth interleave in 139 + let code_len = rs.n * depth in 140 + if Bytes.length codeword <> code_len then 141 + Error 142 + (Fmt.str "Reed_solomon.decode: expected %d bytes (255*%d), got %d" 143 + code_len depth (Bytes.length codeword)) 144 + else if depth = 1 then 145 + match Reed_solomon.decode rs codeword with 146 + | Ok data -> Ok data 147 + | Error e -> Error (Fmt.str "%a" Reed_solomon.pp_error e) 148 + else begin 149 + (* De-interleave into [depth] sub-streams of 255 bytes each *) 150 + let subs = Array.init depth (fun _ -> Bytes.make rs.n '\x00') in 151 + for sub = 0 to depth - 1 do 152 + for j = 0 to rs.n - 1 do 153 + Bytes.set subs.(sub) j (Bytes.get codeword ((j * depth) + sub)) 154 + done 155 + done; 156 + (* Decode each sub-stream *) 157 + let results = Array.map (Reed_solomon.decode rs) subs in 158 + (* Check for errors *) 159 + let err = 160 + Array.fold_left 161 + (fun acc r -> 162 + match acc with 163 + | Some _ -> acc 164 + | None -> ( 165 + match r with 166 + | Ok _ -> None 167 + | Error e -> Some (Fmt.str "%a" Reed_solomon.pp_error e))) 168 + None results 169 + in 170 + match err with 171 + | Some e -> Error e 172 + | None -> 173 + (* Re-interleave the decoded data *) 174 + let data_len = rs.k * depth in 175 + let data = Bytes.make data_len '\x00' in 176 + for sub = 0 to depth - 1 do 177 + let decoded = 178 + match results.(sub) with Ok d -> d | Error _ -> assert false 179 + in 180 + for j = 0 to rs.k - 1 do 181 + Bytes.set data ((j * depth) + sub) (Bytes.get decoded j) 182 + done 183 + done; 184 + Ok data 185 + end 186 + end 187 + 188 + (* {1 Re-exported coding modules} 189 + 190 + These modules are re-exported from their standalone packages for backward 191 + compatibility. New code should depend on the standalone packages directly. *) 192 + 193 + module Convolutional = struct 194 + let encode data = Viterbi.encode Viterbi.ccsds data 195 + let decode symbols = Viterbi.decode Viterbi.ccsds symbols 196 + end 197 + 198 + module Turbo = Turbo 199 + module Ldpc = Ldpc
+133
lib/coding.mli
··· 1 + (** CCSDS TM Synchronization and Channel Coding (131.0-B). 2 + 3 + Channel coding schemes for TM (telemetry) downlinks as specified in CCSDS 4 + 131.0-B-4. The Attached Sync Marker (ASM) is handled by the {!Cltu.Asm} 5 + module in [ocaml-cltu]; this package covers the coding layers that sit 6 + between ASM framing and the TM Transfer Frame. 7 + 8 + @see <https://public.ccsds.org/Pubs/131x0b4.pdf> CCSDS 131.0-B-4 *) 9 + 10 + (** {1 Randomizer} 11 + 12 + CCSDS pseudo-random sequence for TM frame randomization. 13 + 14 + The randomizer XORs each byte of the Transfer Frame (excluding the ASM) with 15 + a deterministic pseudo-random sequence generated by an 8-bit LFSR. 16 + 17 + - Polynomial: {m h(x) = x^8 + x^7 + x^5 + x^3 + 1} 18 + - Initial state: all ones (0xFF) 19 + - The sequence restarts at the beginning of each Transfer Frame 20 + 21 + @see <https://public.ccsds.org/Pubs/131x0b4.pdf> Section 9 *) 22 + module Randomizer : sig 23 + type t 24 + (** Randomizer state. Mutable -- each call to {!next_byte} or {!apply} 25 + advances the internal LFSR. *) 26 + 27 + val create : unit -> t 28 + (** [create ()] returns a new randomizer initialized to the start-of-frame 29 + state (LFSR = 0xFF). *) 30 + 31 + val reset : t -> unit 32 + (** [reset t] resets the LFSR to 0xFF for the next frame. *) 33 + 34 + val next_byte : t -> int 35 + (** [next_byte t] advances the LFSR by 8 steps and returns the next 36 + pseudo-random byte (0--255). *) 37 + 38 + val apply : t -> bytes -> int -> int -> unit 39 + (** [apply t buf off len] XORs [buf.[off] .. buf.[off+len-1]] with the 40 + pseudo-random sequence, advancing the LFSR state. Call {!reset} before 41 + each new frame. 42 + 43 + This is an in-place operation: the buffer is modified. To derandomize a 44 + received frame, call [apply] again with a freshly reset randomizer -- XOR 45 + is its own inverse. *) 46 + 47 + val apply_string : t -> string -> string 48 + (** [apply_string t s] returns a new string with each byte XORed against the 49 + pseudo-random sequence. The randomizer state advances by [String.length s] 50 + bytes. *) 51 + 52 + val sequence : int -> bytes 53 + (** [sequence n] returns the first [n] bytes of the CCSDS pseudo-random 54 + sequence (starting from the initial state 0xFF). Useful for test vectors 55 + and offline analysis. *) 56 + end 57 + 58 + (** {1 Reed-Solomon} 59 + 60 + Reed-Solomon RS(255,223) codec per CCSDS 131.0-B-4 Section 4. 61 + 62 + The standard TM code is RS(255,223) over GF(2^8) with symbol size 8, capable 63 + of correcting up to 16 symbol errors per codeword. Interleaving depths of 1, 64 + 2, 3, 4, 5, and 8 are defined. 65 + 66 + For interleave depth I, the encoder takes [223*I] data bytes, splits them 67 + round-robin into I sub-streams, encodes each with RS(255,223), and 68 + interleaves the I codewords into [255*I] output bytes. 69 + 70 + @see <https://public.ccsds.org/Pubs/131x0b4.pdf> Section 4 *) 71 + module Reed_solomon : sig 72 + type interleave = 73 + | I1 74 + | I2 75 + | I3 76 + | I4 77 + | I5 78 + | I8 (** Interleaving depth per CCSDS 131.0-B-4 Table 4-1. *) 79 + 80 + val encode : ?interleave:interleave -> bytes -> bytes 81 + (** [encode ?interleave data] appends RS parity symbols to the data. 82 + 83 + Input must be exactly [223 * I] bytes where [I] is the interleaving depth 84 + (default 1). Output is [255 * I] bytes. 85 + 86 + @raise Invalid_argument if the input length is wrong. *) 87 + 88 + val decode : ?interleave:interleave -> bytes -> (bytes, string) result 89 + (** [decode ?interleave codeword] corrects errors and strips parity. 90 + 91 + Input must be exactly [255 * I] bytes. Returns [Ok data] with the 92 + [223 * I] data bytes on success, or [Error msg] if decoding fails. *) 93 + end 94 + 95 + (** {1 Convolutional Coding} 96 + 97 + Rate 1/2, constraint length K=7 convolutional code with Viterbi decoding, 98 + per CCSDS 131.0-B-4 Section 3. 99 + 100 + This module re-exports the CCSDS preset from the standalone {!Viterbi} 101 + package. 102 + 103 + @see <https://public.ccsds.org/Pubs/131x0b4.pdf> Section 3 *) 104 + module Convolutional : sig 105 + val encode : bytes -> bytes 106 + (** [encode data] applies rate 1/2 K=7 convolutional encoding. 107 + 108 + The output is a 2-byte big-endian length header (the original data length 109 + in bytes) followed by [ceil((n*8 + 6) * 2 / 8)] bytes of coded symbols 110 + (two coded bits per input bit, plus 6 tail bits to flush the encoder). *) 111 + 112 + val decode : bytes -> (bytes, string) result 113 + (** [decode symbols] performs hard-decision Viterbi decoding. 114 + 115 + The input is the output of {!encode}: a 2-byte length header followed by 116 + the byte-packed coded symbol stream. Returns [Ok data] with the original 117 + data bytes, or [Error msg] if decoding fails. *) 118 + end 119 + 120 + module Turbo = Turbo 121 + (** {1 Turbo Codes} 122 + 123 + Re-exported from the standalone {!Turbo} package. 124 + 125 + @see <https://public.ccsds.org/Pubs/131x0b4.pdf> Section 6 *) 126 + 127 + module Ldpc = Ldpc 128 + (** {1 LDPC Codes} 129 + 130 + Re-exported from the standalone {!Ldpc} package. 131 + 132 + @see <https://public.ccsds.org/Pubs/131x0b4.pdf> Section 7 133 + @see <https://public.ccsds.org/Pubs/231x0b4.pdf> CCSDS 231.0-B Section 4 *)
+4
lib/dune
··· 1 + (library 2 + (name scc) 3 + (public_name scc) 4 + (libraries reed-solomon viterbi turbo ldpc fmt logs))
+2
lib/scc.ml
··· 1 + module Sync = Sync 2 + module Coding = Coding
+61
lib/scc.mli
··· 1 + (** CCSDS Synchronization and Channel Coding (131.0-B, 231.0-B). 2 + 3 + The SCC sublayer sits between the Physical Layer and the Data Link Protocol 4 + sublayer. It provides: 5 + 6 + - {b Synchronization} ({!Sync}): Frame delimiting via ASM (Attached Sync 7 + Marker) for TM downlink and CLTU (Command Link Transmission Unit) with 8 + BCH(63,56) for TC uplink. Stream parsers extract frames from continuous 9 + byte streams. 10 + 11 + - {b Channel Coding} ({!Coding}): Error-control coding including the CCSDS 12 + pseudo-random randomizer (LFSR), Reed-Solomon RS(255,223) with 13 + interleaving depths I=1..8, and CCSDS presets for convolutional (rate 1/2 14 + K=7), turbo, and LDPC codes. 15 + 16 + The generic coding algorithms live in separate packages ({!Reed_solomon}, 17 + {!Viterbi}, {!Turbo}, {!Ldpc}). This module provides the CCSDS-specific 18 + parameterizations and composition. 19 + 20 + {b Usage} 21 + 22 + {[ 23 + (* Synchronization: wrap a TC frame in a CLTU *) 24 + let cltu = Scc.Sync.Cltu.encode tc_frame 25 + 26 + (* Channel coding: randomize + RS encode *) 27 + let r = Scc.Coding.Randomizer.create () in 28 + Scc.Coding.Randomizer.apply r frame 0 (Bytes.length frame); 29 + let codeword = Scc.Coding.Reed_solomon.encode ~interleave:I5 frame 30 + 31 + (* Stream parsing: extract CLTUs from raw bytes *) 32 + let state = Scc.Sync.Cltu_sync.init () in 33 + let state, result = Scc.Sync.Cltu_sync.feed state raw_bytes 34 + ]} 35 + 36 + @see <https://public.ccsds.org/Pubs/131x0b4.pdf> CCSDS 131.0-B-4 37 + @see <https://public.ccsds.org/Pubs/231x0b4e1.pdf> CCSDS 231.0-B-4 *) 38 + 39 + (** {1 Synchronization} 40 + 41 + Frame synchronization per CCSDS 131.0-B (TM: ASM) and 231.0-B (TC: CLTU). 42 + 43 + - {!Sync.Cltu}: CLTU encoding/decoding with BCH(63,56) codeblocks 44 + - {!Sync.Asm}: ASM marker insertion/detection (0x1ACFFC1D) 45 + - {!Sync.Cltu_sync}: Pure state machine for CLTU extraction from byte 46 + streams 47 + - {!Sync.Asm_sync}: Pure state machine for ASM-framed data extraction *) 48 + 49 + module Sync = Sync 50 + 51 + (** {1 Channel Coding} 52 + 53 + Error-control coding per CCSDS 131.0-B and 231.0-B. 54 + 55 + - {!Coding.Randomizer}: CCSDS LFSR pseudo-random sequence (Section 9) 56 + - {!Coding.Reed_solomon}: RS(255,223) with interleaving I=1..8 (Section 4) 57 + - {!Coding.Convolutional}: Rate 1/2, K=7 with Viterbi decoding (Section 3) 58 + - {!Coding.Turbo}: Turbo codes with BCJR/MAP decoding (Section 6) 59 + - {!Coding.Ldpc}: LDPC codes with belief propagation decoding (Section 7) *) 60 + 61 + module Coding = Coding
+441
lib/sync.ml
··· 1 + (** Transport layer encoding/decoding - I/O agnostic. 2 + 3 + CCSDS defines standard framing for space links: 4 + - TC uplink: CLTU (Command Link Transmission Unit) with BCH/LDPC coding 5 + - TM downlink: ASM (Attached Sync Marker) + optional Reed-Solomon 6 + 7 + This module provides pure encode/decode functions. The actual I/O is handled 8 + by the shell layer (e.g., borealis.eio). *) 9 + 10 + (** {1 Errors} *) 11 + 12 + type error = 13 + | Too_short 14 + | Invalid_length of int 15 + | Invalid_start_sequence 16 + | Invalid_tail_sequence 17 + | Asm_not_found 18 + | Frame_too_large of int 19 + | Bch_error of int (** BCH parity error at codeblock index *) 20 + 21 + let pp_error ppf = function 22 + | Too_short -> Format.fprintf ppf "data too short" 23 + | Invalid_length n -> Format.fprintf ppf "invalid frame length: %d" n 24 + | Invalid_start_sequence -> Format.fprintf ppf "invalid CLTU start sequence" 25 + | Invalid_tail_sequence -> Format.fprintf ppf "invalid CLTU tail sequence" 26 + | Asm_not_found -> Format.fprintf ppf "ASM marker not found" 27 + | Frame_too_large n -> Format.fprintf ppf "frame too large: %d bytes" n 28 + | Bch_error n -> Format.fprintf ppf "BCH parity error at codeblock %d" n 29 + 30 + (** {1 CLTU - Command Link Transmission Unit} 31 + 32 + CCSDS 231.0-B-4: TC Synchronization and Channel Coding 33 + 34 + CLTUs provide reliable uplink of TC frames with: 35 + - Start sequence for acquisition (0xEB90) 36 + - BCH(63,56) codeblocks: 7 data bytes + 1 parity byte 37 + - Tail sequence for completion detection 38 + 39 + Structure: 40 + {v 41 + +------------+------------------+------------------+------------+ 42 + | Start Seq | Codeblock 1 | Codeblock N | Tail Seq | 43 + | 0xEB90 | 7 data + 1 parity| 7 data + 1 parity| 8 bytes | 44 + +------------+------------------+------------------+------------+ 45 + v} 46 + 47 + Note: Current implementation is simplified - BCH parity is set to 0x00. Full 48 + BCH(63,56) encoding/decoding is TODO. *) 49 + module Cltu = struct 50 + (** Start sequence: 0xEB90 (acquisition pattern) *) 51 + let start_seq = Bytes.of_string "\xEB\x90" 52 + 53 + (** Tail sequence: 0xC5C5C5C5C5C5C579 (idle pattern + terminator) *) 54 + let tail_seq = Bytes.of_string "\xC5\xC5\xC5\xC5\xC5\xC5\xC5\x79" 55 + 56 + (** Codeblock size: 7 data bytes + 1 parity byte *) 57 + let codeblock_data = 7 58 + 59 + let codeblock_size = 8 60 + 61 + (** Fill byte used for padding incomplete final codeblock. 62 + 63 + Per CCSDS 231.0-B-4, fill consists of alternating ones/zeros starting with 64 + zero: 01010101 = 0x55. Some implementations (e.g., NASA CryptoLib) use 65 + 0xC5 instead, but 0x55 is the standard. *) 66 + let fill_byte = '\x55' 67 + 68 + (** BCH(63,56) generator polynomial: x^7 + x^6 + x^2 + 1 = 0x45 69 + 70 + Per CCSDS 231.0-B-4, codeword is: 56 info bits + 7 parity bits + 1 filler. 71 + 72 + Polynomial representation: bit 6 is x^6 coeff, bit 0 is x^0 coeff. g(x) = 73 + x^7 + x^6 + x^2 + 1 -> feedback taps at positions 6, 2, 0 = 0x45 *) 74 + let bch_poly = 0x45 75 + 76 + (** Compute BCH(63,56) parity for 7 data bytes starting at offset. 77 + 78 + Uses LFSR feedback: processes 56 bits MSB first, outputs 7-bit remainder. 79 + Per CCSDS 231.0-B-4, the parity bits P0-P6 must be complemented before 80 + being placed in the codeword. Parity is placed in MSB 7 bits with filler 81 + bit (0) in LSB. Zero allocation in hot path. *) 82 + let bch_parity data offset = 83 + let sr = ref 0 in 84 + for i = 0 to 6 do 85 + let byte = Char.code (Bytes.get data (offset + i)) in 86 + for j = 7 downto 0 do 87 + let din = (byte lsr j) land 1 in 88 + let fb = (!sr lsr 6) lxor din land 1 in 89 + sr := (!sr lsl 1) lxor (if fb = 1 then bch_poly else 0) land 0x7F 90 + done 91 + done; 92 + (* Complement parity bits, shift to MSB 7 bits, filler bit (0) in LSB *) 93 + Char.chr ((lnot !sr land 0x7F) lsl 1) 94 + 95 + (** Compute BCH syndrome for 8-byte codeblock (7 data + 1 parity) at offset. 96 + 97 + The parity is stored complemented per CCSDS 231.0-B-4, so we un-complement 98 + before processing. Returns 0 if valid. Zero allocation in hot path. *) 99 + let bch_syndrome data offset = 100 + let sr = ref 0 in 101 + for i = 0 to 6 do 102 + let byte = Char.code (Bytes.get data (offset + i)) in 103 + for j = 7 downto 0 do 104 + let din = (byte lsr j) land 1 in 105 + let fb = (!sr lsr 6) lxor din land 1 in 106 + sr := (!sr lsl 1) lxor (if fb = 1 then bch_poly else 0) land 0x7F 107 + done 108 + done; 109 + (* Un-complement and process 7 parity bits (MSB 7 bits of byte 7) *) 110 + let parity_byte = lnot (Char.code (Bytes.get data (offset + 7))) in 111 + for j = 7 downto 1 do 112 + let din = (parity_byte lsr j) land 1 in 113 + let fb = (!sr lsr 6) lxor din land 1 in 114 + sr := (!sr lsl 1) lxor (if fb = 1 then bch_poly else 0) land 0x7F 115 + done; 116 + !sr 117 + 118 + (** Verify BCH(63,56) codeblock. Returns true if valid (syndrome = 0). *) 119 + let bch_verify data offset _parity = bch_syndrome data offset = 0 120 + 121 + (** Encode TC frame as CLTU with BCH codeblocks. 122 + 123 + The frame is split into 7-byte chunks, each becoming a codeblock with BCH 124 + parity. The final chunk is padded with fill bytes (0xC5) if needed. *) 125 + let encode frame = 126 + let frame_len = Bytes.length frame in 127 + let num_codeblocks = (frame_len + codeblock_data - 1) / codeblock_data in 128 + let cltu_len = 129 + Bytes.length start_seq 130 + + (num_codeblocks * codeblock_size) 131 + + Bytes.length tail_seq 132 + in 133 + let cltu = Bytes.create cltu_len in 134 + (* Start sequence *) 135 + Bytes.blit start_seq 0 cltu 0 (Bytes.length start_seq); 136 + let pos = ref (Bytes.length start_seq) in 137 + (* Codeblocks *) 138 + for i = 0 to num_codeblocks - 1 do 139 + let src_offset = i * codeblock_data in 140 + let remaining = frame_len - src_offset in 141 + let data_len = min codeblock_data remaining in 142 + (* Copy data bytes *) 143 + Bytes.blit frame src_offset cltu !pos data_len; 144 + (* Pad with fill bytes if needed *) 145 + for j = data_len to codeblock_data - 1 do 146 + Bytes.set cltu (!pos + j) fill_byte 147 + done; 148 + (* Add BCH parity *) 149 + let parity = bch_parity cltu !pos in 150 + Bytes.set cltu (!pos + codeblock_data) parity; 151 + pos := !pos + codeblock_size 152 + done; 153 + (* Tail sequence *) 154 + Bytes.blit tail_seq 0 cltu !pos (Bytes.length tail_seq); 155 + cltu 156 + 157 + (** Decode CLTU to TC frame. 158 + 159 + Verifies start/tail sequences, BCH parity, and extracts data from 160 + codeblocks. 161 + 162 + Returns the frame data without fill bytes. Note: The decoder cannot 163 + distinguish padding from actual 0xC5 bytes in the payload. In practice, 164 + the frame length is known from the TC frame header. *) 165 + let decode cltu = 166 + let cltu_len = Bytes.length cltu in 167 + let start_len = Bytes.length start_seq in 168 + let tail_len = Bytes.length tail_seq in 169 + let min_len = start_len + codeblock_size + tail_len in 170 + if cltu_len < min_len then Error Too_short 171 + else if Bytes.sub cltu 0 start_len <> start_seq then 172 + Error Invalid_start_sequence 173 + else if Bytes.sub cltu (cltu_len - tail_len) tail_len <> tail_seq then 174 + Error Invalid_tail_sequence 175 + else 176 + let codeblocks_len = cltu_len - start_len - tail_len in 177 + if codeblocks_len mod codeblock_size <> 0 then 178 + Error (Invalid_length codeblocks_len) 179 + else 180 + let num_codeblocks = codeblocks_len / codeblock_size in 181 + let max_data_len = num_codeblocks * codeblock_data in 182 + let data = Bytes.create max_data_len in 183 + (* Verify BCH parity for all codeblocks first *) 184 + let rec verify_codeblocks i pos = 185 + if i >= num_codeblocks then Ok () 186 + else 187 + let parity = Bytes.get cltu (pos + codeblock_data) in 188 + if not (bch_verify cltu pos parity) then Error (Bch_error i) 189 + else verify_codeblocks (i + 1) (pos + codeblock_size) 190 + in 191 + match verify_codeblocks 0 start_len with 192 + | Error _ as e -> e 193 + | Ok () -> 194 + (* Extract data from codeblocks *) 195 + let pos = ref start_len in 196 + for i = 0 to num_codeblocks - 1 do 197 + Bytes.blit cltu !pos data (i * codeblock_data) codeblock_data; 198 + pos := !pos + codeblock_size 199 + done; 200 + (* Per CCSDS 231.0-B-4 Section 3.4.2: fill bytes are NOT removed 201 + by the decoding process. Removal is the responsibility of the 202 + sublayer above (TC Transfer Frame) which uses the frame length 203 + field to delimit the end of the Transfer Frame. *) 204 + Ok (data, Bytes.empty) 205 + end 206 + 207 + (** {1 ASM - Attached Sync Marker} 208 + 209 + CCSDS 131.0-B-4: TM Synchronization and Channel Coding 210 + 211 + ASMs provide frame synchronization for TM downlink: 212 + - 32-bit sync marker (0x1ACFFC1D) with good autocorrelation 213 + - Followed by TM Transfer Frame 214 + - Optional Reed-Solomon or convolutional coding 215 + 216 + Structure: 217 + {v 218 + +------------+------------------+------------------+ 219 + | ASM | TM Frame | Optional FEC | 220 + | 0x1ACFFC1D | (variable len) | (Reed-Solomon) | 221 + +------------+------------------+------------------+ 222 + v} 223 + 224 + The ASM pattern 0x1ACFFC1D was chosen for: 225 + - Low autocorrelation sidelobes 226 + - Unlikely to appear in random data 227 + - Works with various channel coding schemes *) 228 + module Asm = struct 229 + (** ASM pattern: 0x1ACFFC1D *) 230 + let marker = Bytes.of_string "\x1A\xCF\xFC\x1D" 231 + 232 + let marker_len = 4 233 + 234 + (** Encode frame with ASM prefix. *) 235 + let encode frame = 236 + let len = Bytes.length frame in 237 + let buf = Bytes.create (marker_len + len) in 238 + Bytes.blit marker 0 buf 0 marker_len; 239 + Bytes.blit frame 0 buf marker_len len; 240 + buf 241 + 242 + (** Decode ASM-prefixed data, returning frame. 243 + 244 + Returns [Ok (frame, remaining)] where remaining is empty (ASM doesn't 245 + encode length, so we consume all input). *) 246 + let decode data = 247 + let len = Bytes.length data in 248 + if len < marker_len then Error Too_short 249 + else if Bytes.sub data 0 marker_len <> marker then Error Asm_not_found 250 + else 251 + let frame = Bytes.sub data marker_len (len - marker_len) in 252 + Ok (frame, Bytes.empty) 253 + 254 + (** Search for ASM in data stream. 255 + 256 + Returns [Some offset] of first ASM marker found, or [None]. Useful for 257 + acquiring sync in continuous TM stream. *) 258 + let find_sync data = 259 + let len = Bytes.length data in 260 + let rec search i = 261 + if i > len - marker_len then None 262 + else if Bytes.sub data i marker_len = marker then Some i 263 + else search (i + 1) 264 + in 265 + search 0 266 + end 267 + 268 + (** {1 CLTU Sync Parser} 269 + 270 + Pure state machine for extracting CLTUs from a continuous byte stream. 271 + 272 + Usage: Initialize with [init], then repeatedly call [feed] with incoming 273 + bytes. When a complete CLTU is detected and decoded, [feed] returns 274 + [Some (Ok frame)]. On BCH error, returns [Some (Error e)]. 275 + 276 + {b State machine}: 277 + - Searching: Looking for start sequence (0xEB90) 278 + - Reading: Accumulating bytes until tail sequence detected 279 + - Complete: Full CLTU received, ready to decode *) 280 + module Cltu_sync = struct 281 + let max_cltu = 2048 282 + 283 + type state = 284 + | Searching of { buf : Buffer.t } (** Looking for start sequence *) 285 + | Reading of { buf : Buffer.t } (** Accumulating CLTU bytes *) 286 + 287 + let init () = Searching { buf = Buffer.create 2 } 288 + 289 + (** Feed bytes to the sync parser. 290 + 291 + Returns [(new_state, result)] where [result] is: 292 + - [None] if more bytes needed 293 + - [Some (Ok frame)] on successful CLTU decode 294 + - [Some (Error e)] on BCH or format error *) 295 + let feed state data = 296 + let len = Bytes.length data in 297 + let rec process state i = 298 + if i >= len then (state, None) 299 + else 300 + let byte = Bytes.get data i in 301 + match state with 302 + | Searching { buf } -> 303 + let buf_len = Buffer.length buf in 304 + if buf_len = 0 && byte = Bytes.get Cltu.start_seq 0 then begin 305 + Buffer.add_char buf byte; 306 + process (Searching { buf }) (i + 1) 307 + end 308 + else if buf_len = 1 && byte = Bytes.get Cltu.start_seq 1 then begin 309 + Buffer.add_char buf byte; 310 + process (Reading { buf }) (i + 1) 311 + end 312 + else begin 313 + Buffer.clear buf; 314 + process (Searching { buf }) (i + 1) 315 + end 316 + | Reading { buf } -> 317 + Buffer.add_char buf byte; 318 + let buf_len = Buffer.length buf in 319 + if buf_len > max_cltu then begin 320 + (* Too large - reset and search again *) 321 + Buffer.clear buf; 322 + process (Searching { buf }) (i + 1) 323 + end 324 + else if buf_len >= 18 then begin 325 + (* Minimum CLTU: start(2) + codeblock(8) + tail(8) *) 326 + let contents = Buffer.contents buf in 327 + let tail_start = buf_len - 8 in 328 + if 329 + String.sub contents tail_start 8 = Bytes.to_string Cltu.tail_seq 330 + then begin 331 + (* Complete CLTU - decode it *) 332 + let cltu = Bytes.of_string contents in 333 + Buffer.clear buf; 334 + match Cltu.decode cltu with 335 + | Error e -> (Searching { buf }, Some (Error e)) 336 + | Ok (frame, _) -> (Searching { buf }, Some (Ok frame)) 337 + end 338 + else process (Reading { buf }) (i + 1) 339 + end 340 + else process (Reading { buf }) (i + 1) 341 + in 342 + process state 0 343 + end 344 + 345 + (** {1 ASM Sync Parser} 346 + 347 + Pure state machine for extracting ASM-framed data from a continuous byte 348 + stream. 349 + 350 + Two modes: 351 + - Fixed-length: For TM/AOS frames with known size 352 + - Variable-length TC: Parses TC header to determine frame size 353 + 354 + Usage: Initialize with [init ~frame_len] or [init_tc], then call [feed]. *) 355 + module Asm_sync = struct 356 + type mode = 357 + | Fixed of int (** Fixed frame length *) 358 + | Tc_variable (** Parse TC header for length *) 359 + 360 + type state = 361 + | Searching of { buf : Buffer.t; mode : mode } 362 + | Reading of { frame : bytes; pos : int; mode : mode } 363 + 364 + let init ~frame_len = 365 + Searching { buf = Buffer.create 4; mode = Fixed frame_len } 366 + 367 + let init_tc () = Searching { buf = Buffer.create 4; mode = Tc_variable } 368 + 369 + (** Feed bytes to the sync parser. *) 370 + let feed state data = 371 + let len = Bytes.length data in 372 + let rec process state i = 373 + if i >= len then (state, None) 374 + else 375 + let byte = Bytes.get data i in 376 + match state with 377 + | Searching { buf; mode } -> 378 + Buffer.add_char buf byte; 379 + if Buffer.length buf > 4 then begin 380 + (* Shift buffer - keep last 4 bytes *) 381 + let contents = Buffer.contents buf in 382 + Buffer.clear buf; 383 + Buffer.add_substring buf contents 1 4 384 + end; 385 + if 386 + Buffer.length buf = 4 387 + && Buffer.contents buf = Bytes.to_string Asm.marker 388 + then begin 389 + (* Found marker *) 390 + Buffer.clear buf; 391 + match mode with 392 + | Fixed frame_len -> 393 + let frame = Bytes.create frame_len in 394 + process (Reading { frame; pos = 0; mode }) (i + 1) 395 + | Tc_variable -> 396 + (* Need to read 5-byte TC header first *) 397 + let frame = Bytes.create 5 in 398 + process (Reading { frame; pos = 0; mode }) (i + 1) 399 + end 400 + else process (Searching { buf; mode }) (i + 1) 401 + | Reading { frame; pos; mode } -> 402 + Bytes.set frame pos byte; 403 + let new_pos = pos + 1 in 404 + let frame_len = Bytes.length frame in 405 + if new_pos >= frame_len then begin 406 + match mode with 407 + | Fixed _ -> 408 + (* Complete fixed-length frame *) 409 + (Searching { buf = Buffer.create 4; mode }, Some (Ok frame)) 410 + | Tc_variable -> 411 + if frame_len = 5 then begin 412 + (* Just read TC header - parse length and allocate full frame *) 413 + let len_field = 414 + ((Char.code (Bytes.get frame 2) land 0x03) lsl 8) 415 + lor Char.code (Bytes.get frame 3) 416 + in 417 + let total_len = len_field + 1 in 418 + if total_len < 5 then 419 + ( Searching { buf = Buffer.create 4; mode }, 420 + Some (Error (Invalid_length total_len)) ) 421 + else if total_len = 5 then 422 + (* Frame is just the header *) 423 + ( Searching { buf = Buffer.create 4; mode }, 424 + Some (Ok frame) ) 425 + else begin 426 + (* Need more bytes - create full frame and copy header *) 427 + let full_frame = Bytes.create total_len in 428 + Bytes.blit frame 0 full_frame 0 5; 429 + process 430 + (Reading { frame = full_frame; pos = 5; mode }) 431 + (i + 1) 432 + end 433 + end 434 + else 435 + (* Complete variable-length frame *) 436 + (Searching { buf = Buffer.create 4; mode }, Some (Ok frame)) 437 + end 438 + else process (Reading { frame; pos = new_pos; mode }) (i + 1) 439 + in 440 + process state 0 441 + end
+151
lib/sync.mli
··· 1 + (** Transport layer encoding/decoding - I/O agnostic. 2 + 3 + CCSDS defines standard framing for space links: 4 + - TC uplink: CLTU (Command Link Transmission Unit) with BCH/LDPC coding 5 + - TM downlink: ASM (Attached Sync Marker) + optional Reed-Solomon 6 + 7 + This module provides pure encode/decode functions. The actual I/O is handled 8 + by the shell layer (e.g., borealis.eio). *) 9 + 10 + (** {1 Errors} *) 11 + 12 + type error = 13 + | Too_short 14 + | Invalid_length of int 15 + | Invalid_start_sequence 16 + | Invalid_tail_sequence 17 + | Asm_not_found 18 + | Frame_too_large of int 19 + | Bch_error of int (** BCH parity error at codeblock index *) 20 + 21 + val pp_error : error Fmt.t 22 + 23 + (** {1 CLTU - Command Link Transmission Unit} 24 + 25 + CCSDS 231.0-B-4: TC Synchronization and Channel Coding 26 + 27 + CLTUs provide reliable uplink of TC frames with: 28 + - Start sequence (0xEB90) for acquisition 29 + - BCH(63,56) codeblocks: 7 data + 1 parity byte 30 + - Tail sequence for completion detection *) 31 + module Cltu : sig 32 + val start_seq : bytes 33 + (** Start sequence: 0xEB90 *) 34 + 35 + val tail_seq : bytes 36 + (** Tail sequence: 0xC5C5C5C5C5C5C579 *) 37 + 38 + val codeblock_data : int 39 + (** Data bytes per codeblock (7). *) 40 + 41 + val codeblock_size : int 42 + (** Total bytes per codeblock including parity (8). *) 43 + 44 + val fill_byte : char 45 + (** Fill byte for padding (0x55). *) 46 + 47 + val encode : bytes -> bytes 48 + (** Encode TC frame as CLTU with BCH codeblocks. 49 + 50 + The frame is split into 7-byte chunks. Each chunk becomes a codeblock with 51 + BCH parity. Final chunk is padded with fill bytes. *) 52 + 53 + val bch_parity : bytes -> int -> char 54 + (** [bch_parity data offset] computes BCH(63,56) parity for 7 data bytes 55 + starting at [offset]. Returns the 7-bit parity byte. *) 56 + 57 + val bch_verify : bytes -> int -> char -> bool 58 + (** [bch_verify data offset parity] verifies BCH(63,56) codeblock at [offset]. 59 + Returns [true] if syndrome is zero (valid or no error). *) 60 + 61 + val decode : bytes -> (bytes * bytes, error) result 62 + (** Decode CLTU to TC frame data. 63 + 64 + Verifies start/tail sequences, BCH parity, and extracts data from 65 + codeblocks. Returns [(data, remaining)] where remaining is always empty. 66 + Returns [Error (Bch_error n)] if codeblock [n] has invalid parity. 67 + 68 + Per CCSDS 231.0-B-4 Section 3.4.2, fill bytes (0x55) are NOT removed by 69 + the decoding process. Removal is the responsibility of the sublayer above, 70 + which uses the TC frame length field to delimit the end of the Transfer 71 + Frame. *) 72 + end 73 + 74 + (** {1 ASM - Attached Sync Marker} 75 + 76 + CCSDS 131.0-B-4: TM Synchronization and Channel Coding 77 + 78 + ASMs provide frame synchronization for TM downlink: 79 + - 32-bit sync marker (0x1ACFFC1D) 80 + - Followed by TM Transfer Frame *) 81 + module Asm : sig 82 + val marker : bytes 83 + (** ASM pattern: 0x1ACFFC1D *) 84 + 85 + val marker_len : int 86 + (** ASM marker length (4 bytes). *) 87 + 88 + val encode : bytes -> bytes 89 + (** Add ASM prefix to frame. *) 90 + 91 + val decode : bytes -> (bytes * bytes, error) result 92 + (** Strip ASM prefix from data. Returns [(frame, remaining)] where remaining 93 + is always empty. *) 94 + 95 + val find_sync : bytes -> int option 96 + (** Search for ASM marker in data stream. Returns byte offset of first marker 97 + found, or [None]. *) 98 + end 99 + 100 + (** {1 CLTU Sync Parser} 101 + 102 + Pure state machine for extracting CLTUs from a continuous byte stream. 103 + Searches for start sequence (0xEB90), accumulates codeblocks until tail 104 + sequence, then decodes with BCH verification. 105 + 106 + This is the recommended way to receive CLTUs from a raw byte stream. *) 107 + module Cltu_sync : sig 108 + val max_cltu : int 109 + (** Maximum CLTU size (2048 bytes). *) 110 + 111 + type state 112 + (** Opaque parser state. *) 113 + 114 + val init : unit -> state 115 + (** Initialize sync parser in searching mode. *) 116 + 117 + val feed : state -> bytes -> state * (bytes, error) result option 118 + (** Feed bytes to the parser. 119 + 120 + Returns [(new_state, result)] where [result] is: 121 + - [None]: More bytes needed 122 + - [Some (Ok frame)]: Complete CLTU decoded successfully 123 + - [Some (Error e)]: BCH or format error *) 124 + end 125 + 126 + (** {1 ASM Sync Parser} 127 + 128 + Pure state machine for extracting ASM-framed data from a continuous byte 129 + stream. Searches for ASM marker (0x1ACFFC1D), then reads frame data. 130 + 131 + Supports two modes: 132 + - Fixed-length: For TM/AOS frames with known size 133 + - Variable-length TC: Parses TC header to determine frame size *) 134 + module Asm_sync : sig 135 + type state 136 + (** Opaque parser state. *) 137 + 138 + val init : frame_len:int -> state 139 + (** Initialize for fixed-length frames (TM/AOS). *) 140 + 141 + val init_tc : unit -> state 142 + (** Initialize for variable-length TC frames. *) 143 + 144 + val feed : state -> bytes -> state * (bytes, error) result option 145 + (** Feed bytes to the parser. 146 + 147 + Returns [(new_state, result)] where [result] is: 148 + - [None]: More bytes needed 149 + - [Some (Ok frame)]: Complete frame received 150 + - [Some (Error e)]: Format error (e.g., invalid TC length) *) 151 + end
+35
scc-eio.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Eio-based CCSDS SCC transport (CLTU, ASM)" 4 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 5 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 6 + license: "ISC" 7 + homepage: "https://tangled.org/gazagnaire.org/ocaml-scc" 8 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-scc/issues" 9 + depends: [ 10 + "dune" {>= "3.21"} 11 + "ocaml" {>= "5.1"} 12 + "scc" {= version} 13 + "eio" {>= "1.0"} 14 + "bytesrw-eio" {>= "0.1"} 15 + "cstruct" {>= "6.0"} 16 + "fmt" {>= "0.9"} 17 + "logs" {>= "0.7"} 18 + "odoc" {with-doc} 19 + ] 20 + build: [ 21 + ["dune" "subst"] {dev} 22 + [ 23 + "dune" 24 + "build" 25 + "-p" 26 + name 27 + "-j" 28 + jobs 29 + "@install" 30 + "@runtest" {with-test} 31 + "@doc" {with-doc} 32 + ] 33 + ] 34 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-scc" 35 + x-maintenance-intent: ["(latest)"]
+39
scc.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "CCSDS Synchronization and Channel Coding (131.0-B, 231.0-B)" 4 + description: 5 + "Synchronization and Channel Coding sublayer per CCSDS 131.0-B and 231.0-B. Sync: ASM markers, CLTU with BCH(63,56). Coding: pseudo-random randomizer (LFSR), RS(255,223) with interleaving, and CCSDS presets for convolutional, turbo, and LDPC codes." 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-scc" 10 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-scc/issues" 11 + depends: [ 12 + "dune" {>= "3.21"} 13 + "ocaml" {>= "5.1"} 14 + "reed-solomon" {>= "0.1"} 15 + "viterbi" {>= "0.1"} 16 + "turbo" {>= "0.1"} 17 + "ldpc" {>= "0.1"} 18 + "fmt" {>= "0.9"} 19 + "logs" {>= "0.7"} 20 + "alcotest" {with-test} 21 + "alcobar" {with-test} 22 + "odoc" {with-doc} 23 + ] 24 + build: [ 25 + ["dune" "subst"] {dev} 26 + [ 27 + "dune" 28 + "build" 29 + "-p" 30 + name 31 + "-j" 32 + jobs 33 + "@install" 34 + "@runtest" {with-test} 35 + "@doc" {with-doc} 36 + ] 37 + ] 38 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-scc" 39 + x-maintenance-intent: ["(latest)"]
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries scc alcotest))
+1
test/test.ml
··· 1 + let () = Alcotest.run "scc" [ Test_sync.suite; Test_coding.suite ]
+556
test/test_coding.ml
··· 1 + (** CCSDS-specific tests for TM Synchronization and Channel Coding. 2 + 3 + Generic error correction tests live in the standalone viterbi, turbo, and 4 + ldpc packages. This file tests the CCSDS randomizer, RS interleaving, and 5 + the concatenated coding pipeline. *) 6 + 7 + (** CCSDS pseudo-random sequence: first 40 bytes from CCSDS 131.0-B-4. *) 8 + let ccsds_pn_first_40 = 9 + [| 10 + 0xFF; 11 + 0x48; 12 + 0x0E; 13 + 0xC0; 14 + 0x9A; 15 + 0x0D; 16 + 0x70; 17 + 0xBC; 18 + 0x8E; 19 + 0x2C; 20 + 0x93; 21 + 0xAD; 22 + 0xA7; 23 + 0xB7; 24 + 0x46; 25 + 0xCE; 26 + 0x5A; 27 + 0x97; 28 + 0x7D; 29 + 0xCC; 30 + 0x32; 31 + 0xA2; 32 + 0xBF; 33 + 0x3E; 34 + 0x0A; 35 + 0x10; 36 + 0xF1; 37 + 0x88; 38 + 0x94; 39 + 0xCD; 40 + 0xEA; 41 + 0xB1; 42 + 0xFE; 43 + 0x90; 44 + 0x1D; 45 + 0x81; 46 + 0x34; 47 + 0x1A; 48 + 0xE1; 49 + 0x79; 50 + |] 51 + 52 + (* --- PN sequence correctness --- *) 53 + 54 + let test_pn_sequence_first_40 () = 55 + let seq = Scc.Coding.Randomizer.sequence 40 in 56 + for i = 0 to 39 do 57 + Alcotest.(check int) 58 + (Printf.sprintf "PN byte %d" i) 59 + ccsds_pn_first_40.(i) 60 + (Char.code (Bytes.get seq i)) 61 + done 62 + 63 + let test_pn_sequence_255_consistency () = 64 + let seq = Scc.Coding.Randomizer.sequence 255 in 65 + let r = Scc.Coding.Randomizer.create () in 66 + for i = 0 to 254 do 67 + let expected = Scc.Coding.Randomizer.next_byte r in 68 + Alcotest.(check int) 69 + (Printf.sprintf "PN byte %d" i) 70 + expected 71 + (Char.code (Bytes.get seq i)) 72 + done 73 + 74 + let test_randomize_all_zeros () = 75 + let zeros = Bytes.make 255 '\x00' in 76 + let r = Scc.Coding.Randomizer.create () in 77 + Scc.Coding.Randomizer.apply r zeros 0 255; 78 + let expected = Scc.Coding.Randomizer.sequence 255 in 79 + Alcotest.(check string) 80 + "all-zeros -> PN sequence" (Bytes.to_string expected) 81 + (Bytes.to_string zeros) 82 + 83 + (* --- Roundtrip --- *) 84 + 85 + let test_roundtrip () = 86 + let data = Bytes.init 128 (fun i -> Char.chr (i land 0xFF)) in 87 + let original = Bytes.copy data in 88 + let r = Scc.Coding.Randomizer.create () in 89 + Scc.Coding.Randomizer.apply r data 0 128; 90 + Alcotest.(check bool) 91 + "randomized differs from original" true (data <> original); 92 + Scc.Coding.Randomizer.reset r; 93 + Scc.Coding.Randomizer.apply r data 0 128; 94 + Alcotest.(check string) 95 + "roundtrip recovers original" (Bytes.to_string original) 96 + (Bytes.to_string data) 97 + 98 + let test_roundtrip_string () = 99 + let input = String.init 64 (fun i -> Char.chr (((i * 7) + 13) land 0xFF)) in 100 + let r = Scc.Coding.Randomizer.create () in 101 + let randomized = Scc.Coding.Randomizer.apply_string r input in 102 + Scc.Coding.Randomizer.reset r; 103 + let recovered = Scc.Coding.Randomizer.apply_string r randomized in 104 + Alcotest.(check string) "string roundtrip" input recovered 105 + 106 + (* --- Reset behavior --- *) 107 + 108 + let test_reset () = 109 + let r = Scc.Coding.Randomizer.create () in 110 + let _ = Scc.Coding.Randomizer.next_byte r in 111 + let _ = Scc.Coding.Randomizer.next_byte r in 112 + let _ = Scc.Coding.Randomizer.next_byte r in 113 + Scc.Coding.Randomizer.reset r; 114 + let first = Scc.Coding.Randomizer.next_byte r in 115 + Alcotest.(check int) "first byte after reset" 0xFF first 116 + 117 + let test_period () = 118 + let r = Scc.Coding.Randomizer.create () in 119 + let first_255 = Array.init 255 (fun _ -> Scc.Coding.Randomizer.next_byte r) in 120 + let next_255 = Array.init 255 (fun _ -> Scc.Coding.Randomizer.next_byte r) in 121 + for i = 0 to 254 do 122 + Alcotest.(check int) 123 + (Printf.sprintf "period byte %d" i) 124 + first_255.(i) next_255.(i) 125 + done 126 + 127 + let test_apply_with_offset () = 128 + let buf = Bytes.make 16 '\x00' in 129 + let r = Scc.Coding.Randomizer.create () in 130 + Scc.Coding.Randomizer.apply r buf 4 8; 131 + for i = 0 to 3 do 132 + Alcotest.(check int) 133 + (Printf.sprintf "untouched byte %d" i) 134 + 0 135 + (Char.code (Bytes.get buf i)) 136 + done; 137 + let pn = Scc.Coding.Randomizer.sequence 8 in 138 + for i = 0 to 7 do 139 + Alcotest.(check int) 140 + (Printf.sprintf "applied byte %d" (i + 4)) 141 + (Char.code (Bytes.get pn i)) 142 + (Char.code (Bytes.get buf (i + 4))) 143 + done 144 + 145 + (* --- Reed-Solomon tests --- *) 146 + 147 + let test_rs_roundtrip_i1 () = 148 + let data = Bytes.init 223 (fun i -> Char.chr (i land 0xFF)) in 149 + let coded = Scc.Coding.Reed_solomon.encode data in 150 + Alcotest.(check int) "coded length" 255 (Bytes.length coded); 151 + Alcotest.(check string) 152 + "data preserved in codeword" (Bytes.to_string data) 153 + (Bytes.sub_string coded 0 223); 154 + match Scc.Coding.Reed_solomon.decode coded with 155 + | Ok recovered -> 156 + Alcotest.(check string) 157 + "RS roundtrip I=1" (Bytes.to_string data) 158 + (Bytes.to_string recovered) 159 + | Error e -> Alcotest.fail (Printf.sprintf "RS decode failed: %s" e) 160 + 161 + let test_rs_roundtrip_i5 () = 162 + let data = Bytes.init 1115 (fun i -> Char.chr (((i * 7) + 3) land 0xFF)) in 163 + let coded = Scc.Coding.Reed_solomon.encode ~interleave:I5 data in 164 + Alcotest.(check int) "coded length I=5" 1275 (Bytes.length coded); 165 + match Scc.Coding.Reed_solomon.decode ~interleave:I5 coded with 166 + | Ok recovered -> 167 + Alcotest.(check string) 168 + "RS roundtrip I=5" (Bytes.to_string data) 169 + (Bytes.to_string recovered) 170 + | Error e -> Alcotest.fail (Printf.sprintf "RS decode I=5 failed: %s" e) 171 + 172 + let test_rs_error_correction () = 173 + let data = Bytes.init 223 (fun i -> Char.chr (((i * 13) + 42) land 0xFF)) in 174 + let coded = Scc.Coding.Reed_solomon.encode data in 175 + let corrupted = Bytes.copy coded in 176 + for i = 0 to 15 do 177 + let pos = i * 15 in 178 + let old_val = Char.code (Bytes.get corrupted pos) in 179 + Bytes.set corrupted pos (Char.chr (old_val lxor 0xFF)) 180 + done; 181 + match Scc.Coding.Reed_solomon.decode corrupted with 182 + | Ok recovered -> 183 + Alcotest.(check string) 184 + "RS corrects 16 errors" (Bytes.to_string data) 185 + (Bytes.to_string recovered) 186 + | Error e -> 187 + Alcotest.fail (Printf.sprintf "RS failed to correct 16 errors: %s" e) 188 + 189 + let test_rs_error_correction_interleaved () = 190 + let data = Bytes.init 1115 (fun i -> Char.chr (((i * 11) + 7) land 0xFF)) in 191 + let coded = Scc.Coding.Reed_solomon.encode ~interleave:I5 data in 192 + let corrupted = Bytes.copy coded in 193 + let depth = 5 in 194 + for sub = 0 to depth - 1 do 195 + for err = 0 to 2 do 196 + let j = (err * 80) + 10 in 197 + let pos = (j * depth) + sub in 198 + let old_val = Char.code (Bytes.get corrupted pos) in 199 + Bytes.set corrupted pos (Char.chr (old_val lxor 0xAA)) 200 + done 201 + done; 202 + match Scc.Coding.Reed_solomon.decode ~interleave:I5 corrupted with 203 + | Ok recovered -> 204 + Alcotest.(check string) 205 + "RS corrects interleaved errors" (Bytes.to_string data) 206 + (Bytes.to_string recovered) 207 + | Error e -> 208 + Alcotest.fail (Printf.sprintf "RS interleaved decode failed: %s" e) 209 + 210 + let test_rs_uncorrectable_17_errors () = 211 + let data = Bytes.init 223 (fun i -> Char.chr (((i * 13) + 42) land 0xFF)) in 212 + let coded = Scc.Coding.Reed_solomon.encode data in 213 + let corrupted = Bytes.copy coded in 214 + for i = 0 to 16 do 215 + let pos = i * 14 in 216 + let old_val = Char.code (Bytes.get corrupted pos) in 217 + Bytes.set corrupted pos (Char.chr (old_val lxor 0xFF)) 218 + done; 219 + match Scc.Coding.Reed_solomon.decode corrupted with 220 + | Ok _ -> 221 + Alcotest.fail 222 + "RS decode should fail with 17 errors (exceeds t=16 capacity)" 223 + | Error _ -> () 224 + 225 + (* --- Interop test vectors --- *) 226 + 227 + let ccsds_pn_full_255 = 228 + [| 229 + 0xFF; 230 + 0x48; 231 + 0x0E; 232 + 0xC0; 233 + 0x9A; 234 + 0x0D; 235 + 0x70; 236 + 0xBC; 237 + 0x8E; 238 + 0x2C; 239 + 0x93; 240 + 0xAD; 241 + 0xA7; 242 + 0xB7; 243 + 0x46; 244 + 0xCE; 245 + 0x5A; 246 + 0x97; 247 + 0x7D; 248 + 0xCC; 249 + 0x32; 250 + 0xA2; 251 + 0xBF; 252 + 0x3E; 253 + 0x0A; 254 + 0x10; 255 + 0xF1; 256 + 0x88; 257 + 0x94; 258 + 0xCD; 259 + 0xEA; 260 + 0xB1; 261 + 0xFE; 262 + 0x90; 263 + 0x1D; 264 + 0x81; 265 + 0x34; 266 + 0x1A; 267 + 0xE1; 268 + 0x79; 269 + 0x1C; 270 + 0x59; 271 + 0x27; 272 + 0x5B; 273 + 0x4F; 274 + 0x6E; 275 + 0x8D; 276 + 0x9C; 277 + 0xB5; 278 + 0x2E; 279 + 0xFB; 280 + 0x98; 281 + 0x65; 282 + 0x45; 283 + 0x7E; 284 + 0x7C; 285 + 0x14; 286 + 0x21; 287 + 0xE3; 288 + 0x11; 289 + 0x29; 290 + 0x9B; 291 + 0xD5; 292 + 0x63; 293 + 0xFD; 294 + 0x20; 295 + 0x3B; 296 + 0x02; 297 + 0x68; 298 + 0x35; 299 + 0xC2; 300 + 0xF2; 301 + 0x38; 302 + 0xB2; 303 + 0x4E; 304 + 0xB6; 305 + 0x9E; 306 + 0xDD; 307 + 0x1B; 308 + 0x39; 309 + 0x6A; 310 + 0x5D; 311 + 0xF7; 312 + 0x30; 313 + 0xCA; 314 + 0x8A; 315 + 0xFC; 316 + 0xF8; 317 + 0x28; 318 + 0x43; 319 + 0xC6; 320 + 0x22; 321 + 0x53; 322 + 0x37; 323 + 0xAA; 324 + 0xC7; 325 + 0xFA; 326 + 0x40; 327 + 0x76; 328 + 0x04; 329 + 0xD0; 330 + 0x6B; 331 + 0x85; 332 + 0xE4; 333 + 0x71; 334 + 0x64; 335 + 0x9D; 336 + 0x6D; 337 + 0x3D; 338 + 0xBA; 339 + 0x36; 340 + 0x72; 341 + 0xD4; 342 + 0xBB; 343 + 0xEE; 344 + 0x61; 345 + 0x95; 346 + 0x15; 347 + 0xF9; 348 + 0xF0; 349 + 0x50; 350 + 0x87; 351 + 0x8C; 352 + 0x44; 353 + 0xA6; 354 + 0x6F; 355 + 0x55; 356 + 0x8F; 357 + 0xF4; 358 + 0x80; 359 + 0xEC; 360 + 0x09; 361 + 0xA0; 362 + 0xD7; 363 + 0x0B; 364 + 0xC8; 365 + 0xE2; 366 + 0xC9; 367 + 0x3A; 368 + 0xDA; 369 + 0x7B; 370 + 0x74; 371 + 0x6C; 372 + 0xE5; 373 + 0xA9; 374 + 0x77; 375 + 0xDC; 376 + 0xC3; 377 + 0x2A; 378 + 0x2B; 379 + 0xF3; 380 + 0xE0; 381 + 0xA1; 382 + 0x0F; 383 + 0x18; 384 + 0x89; 385 + 0x4C; 386 + 0xDE; 387 + 0xAB; 388 + 0x1F; 389 + 0xE9; 390 + 0x01; 391 + 0xD8; 392 + 0x13; 393 + 0x41; 394 + 0xAE; 395 + 0x17; 396 + 0x91; 397 + 0xC5; 398 + 0x92; 399 + 0x75; 400 + 0xB4; 401 + 0xF6; 402 + 0xE8; 403 + 0xD9; 404 + 0xCB; 405 + 0x52; 406 + 0xEF; 407 + 0xB9; 408 + 0x86; 409 + 0x54; 410 + 0x57; 411 + 0xE7; 412 + 0xC1; 413 + 0x42; 414 + 0x1E; 415 + 0x31; 416 + 0x12; 417 + 0x99; 418 + 0xBD; 419 + 0x56; 420 + 0x3F; 421 + 0xD2; 422 + 0x03; 423 + 0xB0; 424 + 0x26; 425 + 0x83; 426 + 0x5C; 427 + 0x2F; 428 + 0x23; 429 + 0x8B; 430 + 0x24; 431 + 0xEB; 432 + 0x69; 433 + 0xED; 434 + 0xD1; 435 + 0xB3; 436 + 0x96; 437 + 0xA5; 438 + 0xDF; 439 + 0x73; 440 + 0x0C; 441 + 0xA8; 442 + 0xAF; 443 + 0xCF; 444 + 0x82; 445 + 0x84; 446 + 0x3C; 447 + 0x62; 448 + 0x25; 449 + 0x33; 450 + 0x7A; 451 + 0xAC; 452 + 0x7F; 453 + 0xA4; 454 + 0x07; 455 + 0x60; 456 + 0x4D; 457 + 0x06; 458 + 0xB8; 459 + 0x5E; 460 + 0x47; 461 + 0x16; 462 + 0x49; 463 + 0xD6; 464 + 0xD3; 465 + 0xDB; 466 + 0xA3; 467 + 0x67; 468 + 0x2D; 469 + 0x4B; 470 + 0xBE; 471 + 0xE6; 472 + 0x19; 473 + 0x51; 474 + 0x5F; 475 + 0x9F; 476 + 0x05; 477 + 0x08; 478 + 0x78; 479 + 0xC4; 480 + 0x4A; 481 + 0x66; 482 + 0xF5; 483 + 0x58; 484 + |] 485 + 486 + let test_interop_pn_full_255 () = 487 + let seq = Scc.Coding.Randomizer.sequence 255 in 488 + for i = 0 to 254 do 489 + Alcotest.(check int) 490 + (Printf.sprintf "PN byte %d" i) 491 + ccsds_pn_full_255.(i) 492 + (Char.code (Bytes.get seq i)) 493 + done 494 + 495 + let test_interop_pn_period_exact () = 496 + let r = Scc.Coding.Randomizer.create () in 497 + for _ = 0 to 254 do 498 + ignore (Scc.Coding.Randomizer.next_byte r) 499 + done; 500 + let byte_256 = Scc.Coding.Randomizer.next_byte r in 501 + Alcotest.(check int) "byte 256 = byte 1 (period 255)" 0xFF byte_256 502 + 503 + (* --- Pipeline tests --- *) 504 + 505 + let test_pipeline_rs_conv_roundtrip () = 506 + let data = Bytes.init 223 (fun i -> Char.chr (((i * 17) + 5) land 0xFF)) in 507 + let rs_coded = Scc.Coding.Reed_solomon.encode data in 508 + let conv_coded = Scc.Coding.Convolutional.encode rs_coded in 509 + match Scc.Coding.Convolutional.decode conv_coded with 510 + | Error e -> 511 + Alcotest.fail 512 + (Printf.sprintf "Convolutional decode failed in pipeline: %s" e) 513 + | Ok rs_decoded_cw -> ( 514 + match Scc.Coding.Reed_solomon.decode rs_decoded_cw with 515 + | Error e -> 516 + Alcotest.fail (Printf.sprintf "RS decode failed in pipeline: %s" e) 517 + | Ok recovered -> 518 + Alcotest.(check string) 519 + "pipeline RS+Conv roundtrip" (Bytes.to_string data) 520 + (Bytes.to_string recovered)) 521 + 522 + let suite = 523 + ( "ccsds-coding", 524 + [ 525 + (* PN sequence correctness *) 526 + Alcotest.test_case "PN sequence first 40 bytes" `Quick 527 + test_pn_sequence_first_40; 528 + Alcotest.test_case "PN sequence 255-byte consistency" `Quick 529 + test_pn_sequence_255_consistency; 530 + Alcotest.test_case "all-zeros produces PN sequence" `Quick 531 + test_randomize_all_zeros; 532 + (* Roundtrip *) 533 + Alcotest.test_case "randomize/derandomize roundtrip" `Quick test_roundtrip; 534 + Alcotest.test_case "string roundtrip" `Quick test_roundtrip_string; 535 + (* Reset and period *) 536 + Alcotest.test_case "reset restores initial state" `Quick test_reset; 537 + Alcotest.test_case "255-byte period" `Quick test_period; 538 + Alcotest.test_case "apply with offset" `Quick test_apply_with_offset; 539 + (* Reed-Solomon *) 540 + Alcotest.test_case "RS roundtrip I=1" `Quick test_rs_roundtrip_i1; 541 + Alcotest.test_case "RS roundtrip I=5" `Quick test_rs_roundtrip_i5; 542 + Alcotest.test_case "RS error correction (16 errors)" `Quick 543 + test_rs_error_correction; 544 + Alcotest.test_case "RS error correction interleaved" `Quick 545 + test_rs_error_correction_interleaved; 546 + Alcotest.test_case "RS uncorrectable (17 errors)" `Quick 547 + test_rs_uncorrectable_17_errors; 548 + (* Interop test vectors *) 549 + Alcotest.test_case "interop: PN sequence full 255 bytes" `Quick 550 + test_interop_pn_full_255; 551 + Alcotest.test_case "interop: PN period exactly 255" `Quick 552 + test_interop_pn_period_exact; 553 + (* Pipeline tests *) 554 + Alcotest.test_case "pipeline: RS + convolutional roundtrip" `Quick 555 + test_pipeline_rs_conv_roundtrip; 556 + ] )
+8
test/test_coding.mli
··· 1 + (** CCSDS-specific tests for TM Synchronization and Channel Coding. 2 + 3 + Tests for the CCSDS randomizer, Reed-Solomon with interleaving, and the 4 + concatenated coding pipeline. Generic error correction tests live in the 5 + standalone viterbi, turbo, and ldpc packages. *) 6 + 7 + val suite : string * unit Alcotest.test_case list 8 + (** Test suite. *)
+476
test/test_sync.ml
··· 1 + (** Tests for CLTU encoding/decoding and BCH parity. 2 + 3 + Tests BCH(63,56) parity with known expected values, single-bit error 4 + detection, exact wire format, decode error paths, and stream synchronization 5 + state machines. *) 6 + 7 + (* --- BCH parity computation --- *) 8 + 9 + (** Test BCH parity for all-zeros data. Per CCSDS 231.0-B-4, the BCH(63,56) 10 + parity for 7 zero bytes is computed by the generator polynomial x^7 + x^6 + 11 + x^2 + 1. For all-zero input, the LFSR stays at zero, so the complemented 12 + parity is 0xFE (0x7F << 1). *) 13 + let test_bch_parity_zeros () = 14 + let data = Bytes.make 7 '\x00' in 15 + let parity = Scc.Sync.Cltu.bch_parity data 0 in 16 + (* Complement of 0x00 (7 bits) = 0x7F, shifted left by 1 = 0xFE *) 17 + Alcotest.(check int) "BCH parity of zeros" 0xFE (Char.code parity) 18 + 19 + (** Test BCH parity for all-0xFF data. bch_verify expects an 8-byte codeblock (7 20 + data + 1 parity). *) 21 + let test_bch_parity_all_ff () = 22 + let data = Bytes.make 7 '\xFF' in 23 + let parity = Scc.Sync.Cltu.bch_parity data 0 in 24 + (* Build a proper 8-byte codeblock for verification *) 25 + let codeblock = Bytes.create 8 in 26 + Bytes.blit data 0 codeblock 0 7; 27 + Bytes.set codeblock 7 parity; 28 + let verify = Scc.Sync.Cltu.bch_verify codeblock 0 parity in 29 + Alcotest.(check bool) "BCH verify for 0xFF data" true verify 30 + 31 + (** Test BCH verify: correct parity passes, wrong parity fails. *) 32 + let test_bch_verify_correct () = 33 + let data = Bytes.of_string "\x40\x00\x00\x00\x00\x00\x00" in 34 + let parity = Scc.Sync.Cltu.bch_parity data 0 in 35 + (* Create a codeblock with parity appended *) 36 + let codeblock = Bytes.create 8 in 37 + Bytes.blit data 0 codeblock 0 7; 38 + Bytes.set codeblock 7 parity; 39 + Alcotest.(check bool) 40 + "correct parity verifies" true 41 + (Scc.Sync.Cltu.bch_verify codeblock 0 parity) 42 + 43 + let test_bch_verify_wrong () = 44 + let data = Bytes.of_string "\x40\x00\x00\x00\x00\x00\x00" in 45 + let parity = Scc.Sync.Cltu.bch_parity data 0 in 46 + let wrong_parity = Char.chr (Char.code parity lxor 0x02) in 47 + let codeblock = Bytes.create 8 in 48 + Bytes.blit data 0 codeblock 0 7; 49 + Bytes.set codeblock 7 wrong_parity; 50 + Alcotest.(check bool) 51 + "wrong parity fails" false 52 + (Scc.Sync.Cltu.bch_verify codeblock 0 wrong_parity) 53 + 54 + (* --- CLTU encode/decode roundtrip --- *) 55 + 56 + (** Test CLTU encode then decode roundtrip for a 7-byte frame (one codeblock). 57 + *) 58 + let test_cltu_roundtrip_one_block () = 59 + let frame = Bytes.of_string "\x01\x02\x03\x04\x05\x06\x07" in 60 + let encoded = Scc.Sync.Cltu.encode frame in 61 + let result = Scc.Sync.Cltu.decode encoded in 62 + match result with 63 + | Ok (decoded, _remaining) -> 64 + (* The decoded data includes the original 7 bytes (one codeblock) *) 65 + Alcotest.(check int) "decoded length" 7 (Bytes.length decoded); 66 + Alcotest.(check string) 67 + "decoded matches original" (Bytes.to_string frame) 68 + (Bytes.sub_string decoded 0 7) 69 + | Error e -> Alcotest.failf "decode failed: %a" Scc.Sync.pp_error e 70 + 71 + (** Test CLTU roundtrip for a multi-block frame. *) 72 + let test_cltu_roundtrip_multi_block () = 73 + let frame = Bytes.init 20 (fun i -> Char.chr (i land 0xFF)) in 74 + let encoded = Scc.Sync.Cltu.encode frame in 75 + let result = Scc.Sync.Cltu.decode encoded in 76 + match result with 77 + | Ok (decoded, _remaining) -> 78 + (* 20 bytes requires 3 codeblocks (7+7+6), decoded has 3*7=21 bytes 79 + (last block padded with fill byte) *) 80 + Alcotest.(check int) "decoded length" 21 (Bytes.length decoded); 81 + (* First 20 bytes match original frame *) 82 + Alcotest.(check string) 83 + "first 20 bytes match" (Bytes.to_string frame) 84 + (Bytes.sub_string decoded 0 20) 85 + | Error e -> Alcotest.failf "decode failed: %a" Scc.Sync.pp_error e 86 + 87 + (* --- ASM marker --- *) 88 + 89 + (** Test ASM marker detection. *) 90 + let test_asm_marker () = 91 + Alcotest.(check int) "ASM marker length" 4 Scc.Sync.Asm.marker_len; 92 + Alcotest.(check string) 93 + "ASM marker value" "\x1A\xCF\xFC\x1D" 94 + (Bytes.to_string Scc.Sync.Asm.marker) 95 + 96 + (** Test ASM encode/decode roundtrip. *) 97 + let test_asm_roundtrip () = 98 + let frame = Bytes.of_string "test frame data" in 99 + let encoded = Scc.Sync.Asm.encode frame in 100 + let result = Scc.Sync.Asm.decode encoded in 101 + match result with 102 + | Ok (decoded, _) -> 103 + Alcotest.(check string) 104 + "ASM roundtrip" (Bytes.to_string frame) (Bytes.to_string decoded) 105 + | Error e -> Alcotest.failf "ASM decode failed: %a" Scc.Sync.pp_error e 106 + 107 + (** Test ASM find_sync. *) 108 + let test_asm_find_sync () = 109 + (* Prepend some garbage before the ASM *) 110 + let garbage = Bytes.of_string "\x00\x00\x00\x00\x00" in 111 + let buf = Bytes.create (5 + 4 + 10) in 112 + Bytes.blit garbage 0 buf 0 5; 113 + Bytes.blit Scc.Sync.Asm.marker 0 buf 5 4; 114 + Bytes.fill buf 9 10 '\x00'; 115 + let result = Scc.Sync.Asm.find_sync buf in 116 + Alcotest.(check (option int)) "find ASM at offset 5" (Some 5) result 117 + 118 + (** Test ASM find_sync with no marker. *) 119 + let test_asm_find_sync_missing () = 120 + let buf = Bytes.make 20 '\x00' in 121 + let result = Scc.Sync.Asm.find_sync buf in 122 + Alcotest.(check (option int)) "no ASM found" None result 123 + 124 + (* --- CLTU start and tail sequences --- *) 125 + 126 + (** Test CLTU start sequence is 0xEB90. *) 127 + let test_cltu_start_sequence () = 128 + Alcotest.(check int) 129 + "start seq length" 2 130 + (Bytes.length Scc.Sync.Cltu.start_seq); 131 + Alcotest.(check int) 132 + "start seq byte 0" 0xEB 133 + (Char.code (Bytes.get Scc.Sync.Cltu.start_seq 0)); 134 + Alcotest.(check int) 135 + "start seq byte 1" 0x90 136 + (Char.code (Bytes.get Scc.Sync.Cltu.start_seq 1)) 137 + 138 + (** Test CLTU tail sequence is present and correct. *) 139 + let test_cltu_tail_sequence () = 140 + Alcotest.(check int) "tail seq length" 8 (Bytes.length Scc.Sync.Cltu.tail_seq); 141 + (* Tail sequence: 0xC5C5C5C5C5C5C579 *) 142 + let expected = "\xC5\xC5\xC5\xC5\xC5\xC5\xC5\x79" in 143 + Alcotest.(check string) 144 + "tail seq value" expected 145 + (Bytes.to_string Scc.Sync.Cltu.tail_seq) 146 + 147 + (** Test that encoded CLTU starts with start_seq and ends with tail_seq. *) 148 + let test_cltu_structure () = 149 + let frame = Bytes.make 7 '\xAA' in 150 + let encoded = Scc.Sync.Cltu.encode frame in 151 + let len = Bytes.length encoded in 152 + (* Starts with 0xEB90 *) 153 + Alcotest.(check int) "starts with EB" 0xEB (Char.code (Bytes.get encoded 0)); 154 + Alcotest.(check int) "starts with 90" 0x90 (Char.code (Bytes.get encoded 1)); 155 + (* Ends with tail sequence *) 156 + let tail_start = len - 8 in 157 + Alcotest.(check string) 158 + "ends with tail" 159 + (Bytes.to_string Scc.Sync.Cltu.tail_seq) 160 + (Bytes.sub_string encoded tail_start 8) 161 + 162 + (* --- Cltu_sync state machine --- *) 163 + 164 + (** Test Cltu_sync: feed a complete CLTU as a single chunk. *) 165 + let test_cltu_sync_complete () = 166 + let frame = Bytes.of_string "\x01\x02\x03\x04\x05\x06\x07" in 167 + let cltu = Scc.Sync.Cltu.encode frame in 168 + let state = Scc.Sync.Cltu_sync.init () in 169 + let _state, result = Scc.Sync.Cltu_sync.feed state cltu in 170 + match result with 171 + | Some (Ok decoded) -> 172 + Alcotest.(check string) 173 + "sync decoded frame" (Bytes.to_string frame) 174 + (Bytes.sub_string decoded 0 7) 175 + | Some (Error e) -> Alcotest.failf "sync decode error: %a" Scc.Sync.pp_error e 176 + | None -> Alcotest.fail "expected complete CLTU" 177 + 178 + (** Test Cltu_sync: feed CLTU byte by byte. *) 179 + let test_cltu_sync_byte_by_byte () = 180 + let frame = Bytes.of_string "\xAA\xBB\xCC\xDD\xEE\xFF\x11" in 181 + let cltu = Scc.Sync.Cltu.encode frame in 182 + let state = ref (Scc.Sync.Cltu_sync.init ()) in 183 + let result = ref None in 184 + for i = 0 to Bytes.length cltu - 1 do 185 + let byte = Bytes.make 1 (Bytes.get cltu i) in 186 + let new_state, r = Scc.Sync.Cltu_sync.feed !state byte in 187 + state := new_state; 188 + if r <> None then result := r 189 + done; 190 + match !result with 191 + | Some (Ok decoded) -> 192 + Alcotest.(check string) 193 + "byte-by-byte sync decoded" (Bytes.to_string frame) 194 + (Bytes.sub_string decoded 0 7) 195 + | Some (Error e) -> Alcotest.failf "sync decode error: %a" Scc.Sync.pp_error e 196 + | None -> Alcotest.fail "expected complete CLTU from byte-by-byte feed" 197 + 198 + (* --- Asm_sync state machine --- *) 199 + 200 + (** Test Asm_sync with fixed-length frames. *) 201 + let test_asm_sync_fixed () = 202 + let frame = Bytes.init 16 (fun i -> Char.chr (i land 0xFF)) in 203 + let encoded = Scc.Sync.Asm.encode frame in 204 + let state = Scc.Sync.Asm_sync.init ~frame_len:16 in 205 + let _state, result = Scc.Sync.Asm_sync.feed state encoded in 206 + match result with 207 + | Some (Ok decoded) -> 208 + Alcotest.(check string) 209 + "ASM sync fixed" (Bytes.to_string frame) (Bytes.to_string decoded) 210 + | Some (Error e) -> Alcotest.failf "ASM sync error: %a" Scc.Sync.pp_error e 211 + | None -> Alcotest.fail "expected complete frame from ASM sync" 212 + 213 + (* --- Error cases --- *) 214 + 215 + (** Test decode of too-short data. *) 216 + let test_decode_too_short () = 217 + let buf = Bytes.make 5 '\x00' in 218 + match Scc.Sync.Cltu.decode buf with 219 + | Error Scc.Sync.Too_short -> () 220 + | Error e -> Alcotest.failf "expected Too_short, got: %a" Scc.Sync.pp_error e 221 + | Ok _ -> Alcotest.fail "expected error on short input" 222 + 223 + (** Test decode with invalid start sequence. *) 224 + let test_decode_bad_start () = 225 + let frame = Bytes.make 7 '\x00' in 226 + let encoded = Scc.Sync.Cltu.encode frame in 227 + (* Corrupt start sequence *) 228 + Bytes.set encoded 0 '\x00'; 229 + match Scc.Sync.Cltu.decode encoded with 230 + | Error Scc.Sync.Invalid_start_sequence -> () 231 + | Error e -> 232 + Alcotest.failf "expected Invalid_start_sequence, got: %a" 233 + Scc.Sync.pp_error e 234 + | Ok _ -> Alcotest.fail "expected error on bad start sequence" 235 + 236 + (* --- Fill byte value --- *) 237 + 238 + (** Test fill byte is 0x55 per CCSDS 231.0-B-4 (alternating 01010101). *) 239 + let test_fill_byte_value () = 240 + Alcotest.(check int) 241 + "fill byte is 0x55" 0x55 242 + (Char.code Scc.Sync.Cltu.fill_byte) 243 + 244 + (* --- Known BCH parity values --- *) 245 + 246 + (** Test BCH parity against exact known values for non-trivial inputs. 247 + 248 + These are independently computed expected parities, NOT encode-then-verify 249 + tautologies. The BCH(63,56) generator polynomial is x^7+x^6+x^2+1 with 250 + complemented parity per CCSDS 231.0-B-4. *) 251 + let test_bch_known_parities () = 252 + let cases = 253 + [ 254 + ("\x40\x00\x00\x00\x00\x00\x00", 0x9C, "0x40 + zeros"); 255 + ("\xFF\xFF\xFF\xFF\xFF\xFF\xFF", 0x86, "all 0xFF"); 256 + ("\x01\x00\x00\x00\x00\x00\x00", 0x24, "0x01 + zeros"); 257 + ("ABCDEFG", 0xB8, "ASCII ABCDEFG"); 258 + ("\x01\x02\x03\x04\x05\x06\x07", 0x70, "01..07"); 259 + ] 260 + in 261 + List.iter 262 + (fun (input, expected, label) -> 263 + let data = Bytes.of_string input in 264 + let parity = Char.code (Scc.Sync.Cltu.bch_parity data 0) in 265 + Alcotest.(check int) (Printf.sprintf "parity %s" label) expected parity) 266 + cases 267 + 268 + (* --- BCH single-bit error detection --- *) 269 + 270 + (** Test that flipping any single bit in the 56 data bits is detected by 271 + bch_verify. BCH(63,56) guarantees single-error detection; this tests all 56 272 + data-bit positions. *) 273 + let test_bch_single_bit_error_data () = 274 + let data = Bytes.of_string "ABCDEFG" in 275 + let parity = Scc.Sync.Cltu.bch_parity data 0 in 276 + let codeblock = Bytes.create 8 in 277 + Bytes.blit data 0 codeblock 0 7; 278 + Bytes.set codeblock 7 parity; 279 + (* Verify the uncorrupted codeblock is valid *) 280 + Alcotest.(check bool) 281 + "uncorrupted valid" true 282 + (Scc.Sync.Cltu.bch_verify codeblock 0 parity); 283 + (* Flip each data bit (bytes 0-6, bits 0-7) and verify detection *) 284 + for byte_pos = 0 to 6 do 285 + for bit_pos = 0 to 7 do 286 + let corrupted = Bytes.copy codeblock in 287 + let orig = Char.code (Bytes.get corrupted byte_pos) in 288 + Bytes.set corrupted byte_pos (Char.chr (orig lxor (1 lsl bit_pos))); 289 + let result = Scc.Sync.Cltu.bch_verify corrupted 0 parity in 290 + Alcotest.(check bool) 291 + (Printf.sprintf "data bit %d.%d detected" byte_pos bit_pos) 292 + false result 293 + done 294 + done 295 + 296 + (** Test that flipping any single bit in the 7 parity bits is detected by 297 + bch_verify. The parity byte has 7 significant bits (bit 0 is filler). *) 298 + let test_bch_single_bit_error_parity () = 299 + let data = Bytes.of_string "ABCDEFG" in 300 + let parity = Scc.Sync.Cltu.bch_parity data 0 in 301 + let codeblock = Bytes.create 8 in 302 + Bytes.blit data 0 codeblock 0 7; 303 + Bytes.set codeblock 7 parity; 304 + (* Flip each parity bit (bits 1-7 of the parity byte; bit 0 is filler) *) 305 + for bit_pos = 1 to 7 do 306 + let corrupted = Bytes.copy codeblock in 307 + let orig = Char.code (Bytes.get corrupted 7) in 308 + Bytes.set corrupted 7 (Char.chr (orig lxor (1 lsl bit_pos))); 309 + let result = Scc.Sync.Cltu.bch_verify corrupted 0 (Bytes.get corrupted 7) in 310 + Alcotest.(check bool) 311 + (Printf.sprintf "parity bit %d detected" bit_pos) 312 + false result 313 + done 314 + 315 + (* --- Wire format test --- *) 316 + 317 + (** Test exact wire format of encoded CLTU for a known payload. 318 + 319 + Encode "ABCDEFG" (exactly 7 bytes = 1 codeblock) and verify the complete 320 + output: start_seq(EB90) + data(41..47) + parity(B8) + tail_seq. *) 321 + let test_wire_format_one_block () = 322 + let frame = Bytes.of_string "ABCDEFG" in 323 + let encoded = Scc.Sync.Cltu.encode frame in 324 + let expected = 325 + Bytes.of_string 326 + "\xEB\x90\x41\x42\x43\x44\x45\x46\x47\xB8\xC5\xC5\xC5\xC5\xC5\xC5\xC5\x79" 327 + in 328 + Alcotest.(check int) 329 + "wire length" (Bytes.length expected) (Bytes.length encoded); 330 + Alcotest.(check string) 331 + "wire bytes" (Bytes.to_string expected) (Bytes.to_string encoded) 332 + 333 + (** Test exact wire format for a 2-codeblock CLTU. 334 + 335 + Encode "ABCDEFGH" (8 bytes = 2 codeblocks, second padded with 0x55). *) 336 + let test_wire_format_two_blocks () = 337 + let frame = Bytes.of_string "ABCDEFGH" in 338 + let encoded = Scc.Sync.Cltu.encode frame in 339 + (* start_seq + CB1(ABCDEFG + 0xB8) + CB2(H+6x0x55 + 0xF2) + tail_seq *) 340 + let expected = 341 + Bytes.of_string 342 + "\xEB\x90\x41\x42\x43\x44\x45\x46\x47\xB8\x48\x55\x55\x55\x55\x55\x55\xF2\xC5\xC5\xC5\xC5\xC5\xC5\xC5\x79" 343 + in 344 + Alcotest.(check int) 345 + "wire length" (Bytes.length expected) (Bytes.length encoded); 346 + Alcotest.(check string) 347 + "wire bytes" (Bytes.to_string expected) (Bytes.to_string encoded) 348 + 349 + (* --- Decode error paths --- *) 350 + 351 + (** Test that a corrupted tail sequence produces Invalid_tail_sequence. *) 352 + let test_decode_bad_tail () = 353 + let frame = Bytes.make 7 '\x00' in 354 + let encoded = Scc.Sync.Cltu.encode frame in 355 + let len = Bytes.length encoded in 356 + (* Corrupt the last byte of the tail sequence *) 357 + Bytes.set encoded (len - 1) '\x00'; 358 + match Scc.Sync.Cltu.decode encoded with 359 + | Error Scc.Sync.Invalid_tail_sequence -> () 360 + | Error e -> 361 + Alcotest.failf "expected Invalid_tail_sequence, got: %a" Scc.Sync.pp_error 362 + e 363 + | Ok _ -> Alcotest.fail "expected error on bad tail sequence" 364 + 365 + (** Test that a BCH parity error in a codeblock produces Bch_error. *) 366 + let test_decode_bch_error () = 367 + let frame = Bytes.of_string "ABCDEFG" in 368 + let encoded = Scc.Sync.Cltu.encode frame in 369 + (* Corrupt a data byte inside the codeblock (byte 2 = first data byte) *) 370 + let orig = Char.code (Bytes.get encoded 2) in 371 + Bytes.set encoded 2 (Char.chr (orig lxor 0xFF)); 372 + match Scc.Sync.Cltu.decode encoded with 373 + | Error (Scc.Sync.Bch_error 0) -> () 374 + | Error e -> 375 + Alcotest.failf "expected Bch_error 0, got: %a" Scc.Sync.pp_error e 376 + | Ok _ -> Alcotest.fail "expected BCH error on corrupted codeblock" 377 + 378 + (** Test Bch_error for the second codeblock in a multi-block CLTU. *) 379 + let test_decode_bch_error_second_block () = 380 + let frame = Bytes.of_string "ABCDEFGHIJKLMN" in 381 + let encoded = Scc.Sync.Cltu.encode frame in 382 + (* Codeblock 2 starts at offset 2 (start_seq) + 8 (cb1) = 10. 383 + Corrupt byte at offset 10 (first data byte of cb2). *) 384 + let orig = Char.code (Bytes.get encoded 10) in 385 + Bytes.set encoded 10 (Char.chr (orig lxor 0x01)); 386 + match Scc.Sync.Cltu.decode encoded with 387 + | Error (Scc.Sync.Bch_error 1) -> () 388 + | Error e -> 389 + Alcotest.failf "expected Bch_error 1, got: %a" Scc.Sync.pp_error e 390 + | Ok _ -> Alcotest.fail "expected BCH error on corrupted codeblock 1" 391 + 392 + (* --- Pretty-print tests --- *) 393 + 394 + (** Test pp_error produces meaningful content for each error variant. Check for 395 + key substrings (field names and values), not just non-empty output. *) 396 + let test_pp_error () = 397 + let pp e = Format.asprintf "%a" Scc.Sync.pp_error e in 398 + (* Too_short: check exact expected message *) 399 + Alcotest.(check string) "Too_short" "data too short" (pp Scc.Sync.Too_short); 400 + (* Invalid_length: check it contains "invalid" and the numeric value *) 401 + Alcotest.(check string) 402 + "Invalid_length 42" "invalid frame length: 42" 403 + (pp (Scc.Sync.Invalid_length 42)); 404 + (* Invalid_start_sequence: check it mentions start sequence *) 405 + Alcotest.(check string) 406 + "Invalid_start_sequence" "invalid CLTU start sequence" 407 + (pp Scc.Sync.Invalid_start_sequence); 408 + (* Invalid_tail_sequence: check it mentions tail sequence *) 409 + Alcotest.(check string) 410 + "Invalid_tail_sequence" "invalid CLTU tail sequence" 411 + (pp Scc.Sync.Invalid_tail_sequence); 412 + (* Asm_not_found: check it mentions ASM *) 413 + Alcotest.(check string) 414 + "Asm_not_found" "ASM marker not found" 415 + (pp Scc.Sync.Asm_not_found); 416 + (* Frame_too_large: check it mentions "too large" and the size *) 417 + Alcotest.(check string) 418 + "Frame_too_large 9999" "frame too large: 9999 bytes" 419 + (pp (Scc.Sync.Frame_too_large 9999)); 420 + (* Bch_error: check it mentions "BCH" and the codeblock index *) 421 + Alcotest.(check string) 422 + "Bch_error 3" "BCH parity error at codeblock 3" 423 + (pp (Scc.Sync.Bch_error 3)) 424 + 425 + let suite = 426 + ( "cltu", 427 + [ 428 + (* BCH parity *) 429 + Alcotest.test_case "BCH parity zeros" `Quick test_bch_parity_zeros; 430 + Alcotest.test_case "BCH parity 0xFF" `Quick test_bch_parity_all_ff; 431 + Alcotest.test_case "BCH verify correct" `Quick test_bch_verify_correct; 432 + Alcotest.test_case "BCH verify wrong" `Quick test_bch_verify_wrong; 433 + (* CLTU roundtrip *) 434 + Alcotest.test_case "CLTU roundtrip 1 block" `Quick 435 + test_cltu_roundtrip_one_block; 436 + Alcotest.test_case "CLTU roundtrip multi-block" `Quick 437 + test_cltu_roundtrip_multi_block; 438 + (* ASM *) 439 + Alcotest.test_case "ASM marker" `Quick test_asm_marker; 440 + Alcotest.test_case "ASM roundtrip" `Quick test_asm_roundtrip; 441 + Alcotest.test_case "ASM find sync" `Quick test_asm_find_sync; 442 + Alcotest.test_case "ASM find sync missing" `Quick 443 + test_asm_find_sync_missing; 444 + (* CLTU structure *) 445 + Alcotest.test_case "start sequence 0xEB90" `Quick test_cltu_start_sequence; 446 + Alcotest.test_case "tail sequence" `Quick test_cltu_tail_sequence; 447 + Alcotest.test_case "CLTU structure" `Quick test_cltu_structure; 448 + (* Fill byte *) 449 + Alcotest.test_case "fill byte 0x55" `Quick test_fill_byte_value; 450 + (* Known BCH parity values *) 451 + Alcotest.test_case "BCH known parities" `Quick test_bch_known_parities; 452 + (* Single-bit error detection *) 453 + Alcotest.test_case "BCH single-bit error (data)" `Quick 454 + test_bch_single_bit_error_data; 455 + Alcotest.test_case "BCH single-bit error (parity)" `Quick 456 + test_bch_single_bit_error_parity; 457 + (* Wire format *) 458 + Alcotest.test_case "wire format 1 block" `Quick test_wire_format_one_block; 459 + Alcotest.test_case "wire format 2 blocks" `Quick 460 + test_wire_format_two_blocks; 461 + (* Decode error paths *) 462 + Alcotest.test_case "decode bad tail" `Quick test_decode_bad_tail; 463 + Alcotest.test_case "decode BCH error cb0" `Quick test_decode_bch_error; 464 + Alcotest.test_case "decode BCH error cb1" `Quick 465 + test_decode_bch_error_second_block; 466 + (* Pretty-print *) 467 + Alcotest.test_case "pp_error content" `Quick test_pp_error; 468 + (* Sync state machines *) 469 + Alcotest.test_case "Cltu_sync complete" `Quick test_cltu_sync_complete; 470 + Alcotest.test_case "Cltu_sync byte-by-byte" `Quick 471 + test_cltu_sync_byte_by_byte; 472 + Alcotest.test_case "Asm_sync fixed" `Quick test_asm_sync_fixed; 473 + (* Errors *) 474 + Alcotest.test_case "decode too short" `Quick test_decode_too_short; 475 + Alcotest.test_case "decode bad start" `Quick test_decode_bad_start; 476 + ] )
+1
test/test_sync.mli
··· 1 + val suite : string * unit Alcotest.test_case list