CSDS Key-Value Notation parser
0
fork

Configure Feed

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

Add ocaml-kvn and ocaml-ocm: KVN parser and OCM message support

ocaml-kvn: CCSDS Key-Value Notation parser.
ocaml-ocm: CCSDS 505.0-B-2 Orbit Comprehensive Message parser.
Tested against TraCSS LANDSAT 5 OCM file.

+308
+1
.ocamlformat
··· 1 + version = 0.28.1
+21
dune-project
··· 1 + (lang dune 3.21) 2 + (name kvn) 3 + (source (tangled gazagnaire.org/ocaml-kvn)) 4 + 5 + (generate_opam_files true) 6 + 7 + (license ISC) 8 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 9 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 10 + 11 + (package 12 + (name kvn) 13 + (synopsis "CCSDS Key-Value Notation parser") 14 + (description 15 + "Typed parser for CCSDS Key-Value Notation (KVN), the text format \ 16 + used by OEM, OCM, TDM, ADM, CDM, and other CCSDS navigation data \ 17 + messages.") 18 + (depends 19 + (ocaml (>= 4.14)) 20 + ptime 21 + fmt))
+33
kvn.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "CCSDS Key-Value Notation parser" 4 + description: 5 + "Typed parser for CCSDS Key-Value Notation (KVN), the text format used by OEM, OCM, TDM, ADM, CDM, and other CCSDS navigation data messages." 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-kvn" 10 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-kvn/issues" 11 + depends: [ 12 + "dune" {>= "3.21"} 13 + "ocaml" {>= "4.14"} 14 + "ptime" 15 + "fmt" 16 + "odoc" {with-doc} 17 + ] 18 + build: [ 19 + ["dune" "subst"] {dev} 20 + [ 21 + "dune" 22 + "build" 23 + "-p" 24 + name 25 + "-j" 26 + jobs 27 + "@install" 28 + "@runtest" {with-test} 29 + "@doc" {with-doc} 30 + ] 31 + ] 32 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-kvn" 33 + x-maintenance-intent: ["(latest)"]
+4
lib/dune
··· 1 + (library 2 + (name kvn) 3 + (public_name kvn) 4 + (libraries ptime fmt))
+160
lib/kvn.ml
··· 1 + (** CCSDS Key-Value Notation (KVN) parser. 2 + 3 + {b Reference}: CCSDS 502.0-B-3 Annex A *) 4 + 5 + (* {1 Line classification} *) 6 + 7 + type line = 8 + | Keyword of string * string 9 + | Data of string 10 + | Comment of string 11 + | Blank 12 + 13 + let classify s = 14 + let s = String.trim s in 15 + if String.length s = 0 || s.[0] = '#' then Blank 16 + else if String.length s >= 7 && String.sub s 0 7 = "COMMENT" then 17 + Comment (String.trim (String.sub s 7 (String.length s - 7))) 18 + else 19 + match String.index_opt s '=' with 20 + | Some i -> 21 + let key = String.trim (String.sub s 0 i) in 22 + let value = 23 + String.trim (String.sub s (i + 1) (String.length s - i - 1)) 24 + in 25 + Keyword (key, value) 26 + | None -> 27 + (* Check for block delimiters (no = sign): META_START, TRAJ_STOP, etc. *) 28 + if 29 + String.length s > 4 30 + && 31 + let suffix = String.sub s (String.length s - 5) 5 in 32 + suffix = "START" || suffix = "_STOP" 33 + then Keyword (s, "") 34 + else Data s 35 + 36 + (* {1 Epoch parsing} *) 37 + 38 + let parse_epoch s = 39 + let s = String.trim s in 40 + let rfc = 41 + if String.contains s 'Z' || String.contains s '+' then s else s ^ "Z" 42 + in 43 + match Ptime.of_rfc3339 ~strict:false rfc with 44 + | Ok (t, _, _) -> Some t 45 + | Error _ -> None 46 + 47 + (* {1 Line-by-line parser} *) 48 + 49 + type state = { lines : (int * string) array; mutable pos : int } 50 + 51 + let of_lines lines = { lines; pos = 0 } 52 + 53 + let of_string s = 54 + let lines = String.split_on_char '\n' s in 55 + let numbered = List.mapi (fun i l -> (i + 1, l)) lines |> Array.of_list in 56 + of_lines numbered 57 + 58 + let of_channel ic = 59 + let lines = ref [] in 60 + let n = ref 0 in 61 + (try 62 + while true do 63 + incr n; 64 + lines := (!n, input_line ic) :: !lines 65 + done 66 + with End_of_file -> ()); 67 + of_lines (Array.of_list (List.rev !lines)) 68 + 69 + let peek st = 70 + if st.pos >= Array.length st.lines then None else Some st.lines.(st.pos) 71 + 72 + let advance st = if st.pos < Array.length st.lines then st.pos <- st.pos + 1 73 + let eof st = st.pos >= Array.length st.lines 74 + 75 + let next st = 76 + match peek st with 77 + | None -> None 78 + | Some l -> 79 + advance st; 80 + Some l 81 + 82 + let skip_blanks st = 83 + let cont = ref true in 84 + while !cont do 85 + match peek st with 86 + | Some (_, s) -> ( 87 + match classify s with 88 + | Blank | Comment _ -> advance st 89 + | _ -> cont := false) 90 + | None -> cont := false 91 + done 92 + 93 + (* {1 Block iteration} *) 94 + 95 + let iter_block ~start ~stop st ~on_keyword ~on_data = 96 + skip_blanks st; 97 + (* Skip the start delimiter if present *) 98 + (match peek st with 99 + | Some (_, s) when String.trim s = start -> advance st 100 + | _ -> ()); 101 + let cont = ref true in 102 + while !cont do 103 + match peek st with 104 + | Some (n, s) -> ( 105 + let trimmed = String.trim s in 106 + if trimmed = stop then begin 107 + advance st; 108 + cont := false 109 + end 110 + else 111 + match classify s with 112 + | Keyword (k, v) -> 113 + on_keyword k v; 114 + advance st 115 + | Data d -> 116 + on_data n d; 117 + advance st 118 + | Comment _ | Blank -> advance st) 119 + | None -> cont := false 120 + done 121 + 122 + let expect_keyword st key = 123 + skip_blanks st; 124 + match peek st with 125 + | Some (_, s) -> ( 126 + match classify s with 127 + | Keyword (k, v) when k = key -> 128 + advance st; 129 + Some v 130 + | _ -> None) 131 + | None -> None 132 + 133 + (* {1 Value parsing} *) 134 + 135 + let parse_float s = 136 + match float_of_string_opt (String.trim s) with 137 + | Some f -> Some f 138 + | None -> None 139 + 140 + let parse_int s = 141 + match int_of_string_opt (String.trim s) with Some i -> Some i | None -> None 142 + 143 + let split_data s = 144 + String.split_on_char ' ' s 145 + |> List.filter (fun s -> String.length (String.trim s) > 0) 146 + |> List.map String.trim 147 + 148 + let parse_floats s = 149 + split_data s 150 + |> List.map (fun s -> 151 + match float_of_string_opt s with Some f -> f | None -> 0.0) 152 + |> Array.of_list 153 + 154 + (* {1 Pretty-printing} *) 155 + 156 + let pp_line ppf = function 157 + | Keyword (k, v) -> Fmt.pf ppf "%s = %s" k v 158 + | Data d -> Fmt.pf ppf "DATA: %s" d 159 + | Comment c -> Fmt.pf ppf "COMMENT %s" c 160 + | Blank -> Fmt.pf ppf "(blank)"
+89
lib/kvn.mli
··· 1 + (** CCSDS Key-Value Notation (KVN) parser. 2 + 3 + KVN is the text format used by CCSDS navigation data messages: OEM, OCM, 4 + TDM, ADM, CDM, and others. This library provides low-level parsing 5 + primitives and block iteration. 6 + 7 + {b Reference}: CCSDS 502.0-B-3 Annex A (KVN format definition) *) 8 + 9 + (** {1 Line classification} *) 10 + 11 + type line = 12 + | Keyword of string * string (** [KEY = VALUE] *) 13 + | Data of string (** Epoch + numeric data (no [=] sign) *) 14 + | Comment of string (** [COMMENT ...] *) 15 + | Blank (** Empty line or whitespace *) 16 + 17 + val classify : string -> line 18 + (** [classify s] classifies a KVN line. *) 19 + 20 + (** {1 Epoch parsing} *) 21 + 22 + val parse_epoch : string -> Ptime.t option 23 + (** [parse_epoch s] parses a CCSDS epoch string to [Ptime.t]. Accepts 24 + ["YYYY-MM-DDThh:mm:ss.sss"] and space-separated variants. *) 25 + 26 + (** {1 Line-by-line parser} *) 27 + 28 + type state 29 + (** Mutable parser state over numbered lines. *) 30 + 31 + val of_string : string -> state 32 + (** [of_string s] creates a parser from a KVN string. *) 33 + 34 + val of_channel : in_channel -> state 35 + (** [of_channel ic] creates a parser from a channel. *) 36 + 37 + val peek : state -> (int * string) option 38 + (** [peek st] returns the current line (number, text) without advancing. *) 39 + 40 + val advance : state -> unit 41 + (** [advance st] moves to the next line. *) 42 + 43 + val next : state -> (int * string) option 44 + (** [next st] returns the current line and advances. *) 45 + 46 + val skip_blanks : state -> unit 47 + (** [skip_blanks st] skips blank and comment lines. *) 48 + 49 + val eof : state -> bool 50 + (** [eof st] is [true] if all lines have been consumed. *) 51 + 52 + (** {1 Block iteration} *) 53 + 54 + val iter_block : 55 + start:string -> 56 + stop:string -> 57 + state -> 58 + on_keyword:(string -> string -> unit) -> 59 + on_data:(int -> string -> unit) -> 60 + unit 61 + (** [iter_block ~start ~stop st ~on_keyword ~on_data] iterates over a KVN block 62 + delimited by [start] and [stop] keywords (e.g., ["META_START"] / 63 + ["META_STOP"]). Calls [on_keyword key value] for keyword lines and 64 + [on_data line_num text] for data lines. Advances [st] past the stop 65 + delimiter. *) 66 + 67 + val expect_keyword : state -> string -> string option 68 + (** [expect_keyword st key] checks if the next non-blank line is [key = value] 69 + and returns [Some value], or [None] if it's something else. Advances past 70 + the keyword if found. *) 71 + 72 + (** {1 Value parsing} *) 73 + 74 + val parse_float : string -> float option 75 + (** [parse_float s] parses a float, returning [None] on failure. *) 76 + 77 + val parse_int : string -> int option 78 + (** [parse_int s] parses an integer, returning [None] on failure. *) 79 + 80 + val split_data : string -> string list 81 + (** [split_data s] splits a data line on whitespace, filtering empty tokens. *) 82 + 83 + val parse_floats : string -> float array 84 + (** [parse_floats s] splits a data line and parses all tokens as floats. 85 + Non-parseable tokens become [0.0]. *) 86 + 87 + (** {1 Pretty-printing} *) 88 + 89 + val pp_line : line Fmt.t