···11+MIT License
22+33+Copyright (c) 2025 Thomas Gazagnaire
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+61
README.md
···11+# matter
22+33+Matter protocol implementation for OCaml.
44+55+## Overview
66+77+This library provides an implementation of the Matter smart home protocol
88+as specified in the [Matter Core Specification](https://csa-iot.org/developer-resource/specifications-download-request/).
99+1010+## Features
1111+1212+- TLV (Tag-Length-Value) encoding and decoding
1313+- Support for all Matter TLV types: integers, booleans, floats, strings, bytes, null
1414+- Nested containers: structures, arrays, lists
1515+- All tag forms: anonymous, context-specific, common/implicit profile, fully-qualified
1616+1717+## Installation
1818+1919+```
2020+opam install matter
2121+```
2222+2323+## Usage
2424+2525+```ocaml
2626+open Matter.Tlv
2727+2828+(* Create elements *)
2929+let elem = structure [
3030+ ctx_int 1 42;
3131+ ctx_string 2 "hello";
3232+ ctx_bool 3 true;
3333+]
3434+3535+(* Encode to binary *)
3636+let binary = encode_one elem
3737+3838+(* Decode from binary *)
3939+match decode binary with
4040+| Ok elements -> (* process elements *)
4141+| Error msg -> (* handle error *)
4242+```
4343+4444+## API
4545+4646+### TLV Module
4747+4848+- `Matter.Tlv.encode` - Encode elements to binary TLV
4949+- `Matter.Tlv.decode` - Decode binary TLV to elements
5050+- `Matter.Tlv.int`, `uint`, `bool`, `string`, `bytes`, `null` - Value constructors
5151+- `Matter.Tlv.structure`, `array`, `list` - Container constructors
5252+- `Matter.Tlv.ctx_int`, `ctx_uint`, etc. - Context-tagged constructors
5353+5454+## Related Work
5555+5656+- [matter-go](https://github.com/project-chip/connectedhomeip) - Reference implementation in C++
5757+- [matter-rs](https://github.com/project-chip/matter-rs) - Rust implementation
5858+5959+## License
6060+6161+MIT License. See [LICENSE.md](LICENSE.md) for details.
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** matter - Matter device discovery and control CLI *)
77+88+open Cmdliner
99+1010+(* Exit code for errors *)
1111+let exit_error = 1
1212+1313+(* Device filter for discover command *)
1414+type device_filter = Operational | Commissionable | All
1515+1616+let device_filter_conv =
1717+ let parse = function
1818+ | "operational" | "op" -> Ok Operational
1919+ | "commissionable" | "comm" -> Ok Commissionable
2020+ | "all" -> Ok All
2121+ | s ->
2222+ Error
2323+ (`Msg
2424+ (Fmt.str
2525+ "unknown filter %S, expected: operational, commissionable, all"
2626+ s))
2727+ in
2828+ let print ppf = function
2929+ | Operational -> Fmt.string ppf "operational"
3030+ | Commissionable -> Fmt.string ppf "commissionable"
3131+ | All -> Fmt.string ppf "all"
3232+ in
3333+ Arg.conv (parse, print)
3434+3535+(* Power action for control command *)
3636+type power_action = On | Off | Toggle
3737+3838+let power_action_conv =
3939+ let parse = function
4040+ | "on" -> Ok On
4141+ | "off" -> Ok Off
4242+ | "toggle" -> Ok Toggle
4343+ | s ->
4444+ Error (`Msg (Fmt.str "unknown action %S, expected: on, off, toggle" s))
4545+ in
4646+ let print ppf = function
4747+ | On -> Fmt.string ppf "on"
4848+ | Off -> Fmt.string ppf "off"
4949+ | Toggle -> Fmt.string ppf "toggle"
5050+ in
5151+ Arg.conv (parse, print)
5252+5353+(* Common arguments *)
5454+let timeout_arg =
5555+ let doc = "Timeout in seconds for network operations." in
5656+ Arg.(value & opt float 3.0 & info [ "t"; "timeout" ] ~docv:"SECONDS" ~doc)
5757+5858+let quiet_arg =
5959+ let doc = "Suppress informational messages." in
6060+ Arg.(value & flag & info [ "q"; "quiet" ] ~doc)
6161+6262+(* Discover command *)
6363+let discover_cmd =
6464+ let doc = "Discover Matter devices on the local network" in
6565+ let man =
6666+ [
6767+ `S Manpage.s_description;
6868+ `P
6969+ "Discovers Matter devices using mDNS/DNS-SD. By default, shows \
7070+ operational devices (already commissioned). Use $(b,--filter) to \
7171+ change which devices are shown.";
7272+ `P "Exit status is 0 if devices were found, 1 otherwise.";
7373+ `S Manpage.s_examples;
7474+ `P "Find operational devices:";
7575+ `Pre " $(mname) $(tname)";
7676+ `P "Find devices in pairing mode:";
7777+ `Pre " $(mname) $(tname) --filter commissionable";
7878+ `P "Find all Matter devices:";
7979+ `Pre " $(mname) $(tname) -f all";
8080+ ]
8181+ in
8282+ let info = Cmd.info "discover" ~doc ~man in
8383+ let filter_arg =
8484+ let doc =
8585+ "Filter devices: $(b,operational) (default) shows commissioned devices, \
8686+ $(b,commissionable) shows devices in pairing mode, $(b,all) shows both."
8787+ in
8888+ Arg.(
8989+ value
9090+ & opt device_filter_conv Operational
9191+ & info [ "f"; "filter" ] ~docv:"FILTER" ~doc)
9292+ in
9393+ let run timeout quiet filter =
9494+ Eio_main.run @@ fun env ->
9595+ Eio.Switch.run @@ fun sw ->
9696+ let net = Eio.Stdenv.net env in
9797+ let clock = Eio.Stdenv.clock env in
9898+ let devices =
9999+ match filter with
100100+ | Operational ->
101101+ Matter.Discovery.discover_operational ~sw ~net ~clock ~timeout ()
102102+ | Commissionable ->
103103+ Matter.Discovery.discover_commissionable ~sw ~net ~clock ~timeout ()
104104+ | All -> Matter.Discovery.discover_all ~sw ~net ~clock ~timeout ()
105105+ in
106106+ if devices = [] then begin
107107+ if not quiet then Fmt.pr "No devices found.@.";
108108+ exit exit_error
109109+ end
110110+ else Fmt.pr "%a@." Matter.Discovery.pp devices
111111+ in
112112+ Cmd.v info Term.(const run $ timeout_arg $ quiet_arg $ filter_arg)
113113+114114+(* Commission command *)
115115+let commission_cmd =
116116+ let doc = "Establish a PASE session with a Matter device" in
117117+ let man =
118118+ [
119119+ `S Manpage.s_description;
120120+ `P
121121+ "Establishes a secure session with a Matter device using the device's \
122122+ passcode (PASE protocol). This is the first step in commissioning a \
123123+ new device.";
124124+ `P
125125+ "The passcode is typically an 8-digit number printed on the device or \
126126+ its packaging (e.g., 20202021).";
127127+ `S Manpage.s_examples;
128128+ `P "Commission a device:";
129129+ `Pre " $(mname) $(tname) 192.168.1.100 --passcode 20202021";
130130+ `P "With non-default port:";
131131+ `Pre " $(mname) $(tname) 192.168.1.100 -p 5541 --passcode 20202021";
132132+ ]
133133+ in
134134+ let info = Cmd.info "commission" ~doc ~man in
135135+ let ip_arg =
136136+ let doc = "IP address of the Matter device." in
137137+ Arg.(required & pos 0 (some string) None & info [] ~docv:"IP" ~doc)
138138+ in
139139+ let port_arg =
140140+ let doc = "UDP port of the Matter device." in
141141+ Arg.(value & opt int 5540 & info [ "p"; "port" ] ~docv:"PORT" ~doc)
142142+ in
143143+ let passcode_arg =
144144+ let doc = "Device passcode (8-digit number)." in
145145+ Arg.(required & opt (some int) None & info [ "passcode" ] ~docv:"CODE" ~doc)
146146+ in
147147+ let run quiet ip port passcode =
148148+ Eio_main.run @@ fun env ->
149149+ Eio.Switch.run @@ fun sw ->
150150+ let net = Eio.Stdenv.net env in
151151+ let clock = Eio.Stdenv.clock env in
152152+ if not quiet then Fmt.pr "Commissioning %s:%d...@." ip port;
153153+ match Matter.Session.establish_pase ~net ~sw ~clock ~ip ~port ~passcode with
154154+ | Ok conn ->
155155+ if not quiet then begin
156156+ Fmt.pr "PASE successful.@.";
157157+ Fmt.pr "Session: %a@." Matter.Session.pp_session conn.session
158158+ end
159159+ | Error (`Msg e) ->
160160+ Fmt.epr "Error: %s@." e;
161161+ exit exit_error
162162+ in
163163+ Cmd.v info Term.(const run $ quiet_arg $ ip_arg $ port_arg $ passcode_arg)
164164+165165+(* Control command - unified on/off/toggle *)
166166+let control_cmd =
167167+ let doc = "Control power state of a Matter device" in
168168+ let man =
169169+ [
170170+ `S Manpage.s_description;
171171+ `P
172172+ "Sends a power control command (on, off, or toggle) to a Matter \
173173+ device. Requires the device passcode for authentication.";
174174+ `S Manpage.s_examples;
175175+ `P "Turn on a device:";
176176+ `Pre " $(mname) $(tname) on 192.168.1.100 --passcode 20202021";
177177+ `P "Turn off with specific endpoint:";
178178+ `Pre " $(mname) $(tname) off 192.168.1.100 --passcode 20202021 -e 2";
179179+ `P "Toggle device state:";
180180+ `Pre " $(mname) $(tname) toggle 192.168.1.100 --passcode 20202021";
181181+ ]
182182+ in
183183+ let info = Cmd.info "control" ~doc ~man in
184184+ let action_arg =
185185+ let doc = "Power action: $(b,on), $(b,off), or $(b,toggle)." in
186186+ Arg.(
187187+ required
188188+ & pos 0 (some power_action_conv) None
189189+ & info [] ~docv:"ACTION" ~doc)
190190+ in
191191+ let ip_arg =
192192+ let doc = "IP address of the Matter device." in
193193+ Arg.(required & pos 1 (some string) None & info [] ~docv:"IP" ~doc)
194194+ in
195195+ let port_arg =
196196+ let doc = "UDP port of the Matter device." in
197197+ Arg.(value & opt int 5540 & info [ "p"; "port" ] ~docv:"PORT" ~doc)
198198+ in
199199+ let passcode_arg =
200200+ let doc = "Device passcode (8-digit number)." in
201201+ Arg.(required & opt (some int) None & info [ "passcode" ] ~docv:"CODE" ~doc)
202202+ in
203203+ let endpoint_arg =
204204+ let doc = "Endpoint ID for the on/off cluster." in
205205+ Arg.(value & opt int 1 & info [ "e"; "endpoint" ] ~docv:"ID" ~doc)
206206+ in
207207+ let run quiet action ip port passcode endpoint =
208208+ Eio_main.run @@ fun env ->
209209+ Eio.Switch.run @@ fun sw ->
210210+ let net = Eio.Stdenv.net env in
211211+ let clock = Eio.Stdenv.clock env in
212212+ match Matter.Session.establish_pase ~net ~sw ~clock ~ip ~port ~passcode with
213213+ | Error (`Msg e) ->
214214+ Fmt.epr "PASE error: %s@." e;
215215+ exit exit_error
216216+ | Ok conn -> (
217217+ let cmd_fn =
218218+ match action with
219219+ | On -> Matter.Session.turn_on
220220+ | Off -> Matter.Session.turn_off
221221+ | Toggle -> Matter.Session.toggle
222222+ in
223223+ let action_str =
224224+ match action with On -> "on" | Off -> "off" | Toggle -> "toggled"
225225+ in
226226+ match cmd_fn ~clock conn ~endpoint_id:endpoint with
227227+ | Ok _ -> if not quiet then Fmt.pr "Device turned %s.@." action_str
228228+ | Error `Timeout ->
229229+ Fmt.epr "Error: timeout waiting for response.@.";
230230+ exit exit_error)
231231+ in
232232+ Cmd.v info
233233+ Term.(
234234+ const run $ quiet_arg $ action_arg $ ip_arg $ port_arg $ passcode_arg
235235+ $ endpoint_arg)
236236+237237+(* Main command group *)
238238+let cmd =
239239+ let doc = "Matter device discovery and control" in
240240+ let man =
241241+ [
242242+ `S Manpage.s_description;
243243+ `P
244244+ "$(tname) provides tools for discovering and controlling Matter smart \
245245+ home devices on the local network.";
246246+ `S Manpage.s_commands;
247247+ `P "$(b,discover) - Find Matter devices using mDNS";
248248+ `P "$(b,commission) - Establish PASE session with a device";
249249+ `P "$(b,control) - Send on/off/toggle commands";
250250+ `S Manpage.s_bugs;
251251+ `P "Report issues at https://github.com/samoht/ocaml-matter/issues";
252252+ ]
253253+ in
254254+ let info = Cmd.info "matter" ~version:"%%VERSION%%" ~doc ~man in
255255+ Cmd.group info [ discover_cmd; commission_cmd; control_cmd ]
256256+257257+let () = exit (Cmd.eval cmd)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Matter TLV (Tag-Length-Value) encoding and decoding *)
77+88+(* Result monad operators *)
99+let ( let* ) = Result.bind
1010+1111+(** {1 TLV Types} *)
1212+1313+(** Tag forms (bits 5-6 of control byte) *)
1414+type tag_form =
1515+ | Anonymous (* 0 *)
1616+ | Context_specific of int (* 1: 1 byte tag *)
1717+ | Common_profile_2 of int (* 2: 2 byte tag *)
1818+ | Common_profile_4 of int32 (* 3: 4 byte tag *)
1919+ | Implicit_profile_2 of int (* 4: 2 byte tag *)
2020+ | Implicit_profile_4 of int32 (* 5: 4 byte tag *)
2121+ | Fully_qualified_6 of int32 * int (* 6: 4 byte vendor + 2 byte tag *)
2222+ | Fully_qualified_8 of int32 * int32 (* 7: 4 byte vendor + 4 byte tag *)
2323+2424+(** TLV value *)
2525+type value =
2626+ | Int of int64
2727+ | Uint of int64
2828+ | Bool of bool
2929+ | Float32 of float
3030+ | Float64 of float
3131+ | String of string
3232+ | Bytes of string
3333+ | Null
3434+ | Structure of element list
3535+ | Array of element list
3636+ | List of element list
3737+3838+and element = { tag : tag_form; value : value }
3939+4040+(** {1 Encoding} *)
4141+4242+let put_byte buf b = Buffer.add_char buf (Char.chr (b land 0xff))
4343+4444+let put_uint16_le buf v =
4545+ put_byte buf v;
4646+ put_byte buf (v lsr 8)
4747+4848+let put_uint32_le buf v =
4949+ put_byte buf (Int32.to_int v);
5050+ put_byte buf (Int32.to_int (Int32.shift_right_logical v 8));
5151+ put_byte buf (Int32.to_int (Int32.shift_right_logical v 16));
5252+ put_byte buf (Int32.to_int (Int32.shift_right_logical v 24))
5353+5454+let put_uint64_le buf v =
5555+ put_byte buf (Int64.to_int v);
5656+ put_byte buf (Int64.to_int (Int64.shift_right_logical v 8));
5757+ put_byte buf (Int64.to_int (Int64.shift_right_logical v 16));
5858+ put_byte buf (Int64.to_int (Int64.shift_right_logical v 24));
5959+ put_byte buf (Int64.to_int (Int64.shift_right_logical v 32));
6060+ put_byte buf (Int64.to_int (Int64.shift_right_logical v 40));
6161+ put_byte buf (Int64.to_int (Int64.shift_right_logical v 48));
6262+ put_byte buf (Int64.to_int (Int64.shift_right_logical v 56))
6363+6464+let tag_form_code = function
6565+ | Anonymous -> 0
6666+ | Context_specific _ -> 1
6767+ | Common_profile_2 _ -> 2
6868+ | Common_profile_4 _ -> 3
6969+ | Implicit_profile_2 _ -> 4
7070+ | Implicit_profile_4 _ -> 5
7171+ | Fully_qualified_6 _ -> 6
7272+ | Fully_qualified_8 _ -> 7
7373+7474+let encode_tag buf = function
7575+ | Anonymous -> ()
7676+ | Context_specific t -> put_byte buf t
7777+ | Common_profile_2 t -> put_uint16_le buf t
7878+ | Common_profile_4 t -> put_uint32_le buf t
7979+ | Implicit_profile_2 t -> put_uint16_le buf t
8080+ | Implicit_profile_4 t -> put_uint32_le buf t
8181+ | Fully_qualified_6 (v, t) ->
8282+ put_uint32_le buf v;
8383+ put_uint16_le buf t
8484+ | Fully_qualified_8 (v, t) ->
8585+ put_uint32_le buf v;
8686+ put_uint32_le buf t
8787+8888+(* Determine integer size code (0-3) based on value *)
8989+let int_size_code v =
9090+ if v >= -128L && v <= 127L then 0
9191+ else if v >= -32768L && v <= 32767L then 1
9292+ else if v >= -2147483648L && v <= 2147483647L then 2
9393+ else 3
9494+9595+let uint_size_code v =
9696+ (* Use unsigned comparison - negative int64 values are large unsigned values *)
9797+ if Int64.unsigned_compare v 0xFFL <= 0 then 0
9898+ else if Int64.unsigned_compare v 0xFFFFL <= 0 then 1
9999+ else if Int64.unsigned_compare v 0xFFFFFFFFL <= 0 then 2
100100+ else 3
101101+102102+let encode_int buf v size_code =
103103+ match size_code with
104104+ | 0 -> put_byte buf (Int64.to_int v)
105105+ | 1 -> put_uint16_le buf (Int64.to_int v)
106106+ | 2 -> put_uint32_le buf (Int64.of_int32 (Int64.to_int32 v) |> Int64.to_int32)
107107+ | 3 -> put_uint64_le buf v
108108+ | _ -> failwith "Invalid size code"
109109+110110+let rec encode_element buf elem =
111111+ let encode_control tag_form type_code =
112112+ let tag_code = tag_form_code tag_form in
113113+ put_byte buf ((tag_code lsl 5) lor type_code)
114114+ in
115115+ match elem.value with
116116+ | Int v ->
117117+ let size_code = int_size_code v in
118118+ encode_control elem.tag size_code;
119119+ encode_tag buf elem.tag;
120120+ encode_int buf v size_code
121121+ | Uint v ->
122122+ let size_code = uint_size_code v in
123123+ encode_control elem.tag (4 + size_code);
124124+ encode_tag buf elem.tag;
125125+ encode_int buf v size_code
126126+ | Bool false ->
127127+ encode_control elem.tag 8;
128128+ encode_tag buf elem.tag
129129+ | Bool true ->
130130+ encode_control elem.tag 9;
131131+ encode_tag buf elem.tag
132132+ | Float32 f ->
133133+ encode_control elem.tag 10;
134134+ encode_tag buf elem.tag;
135135+ put_uint32_le buf (Int32.bits_of_float f)
136136+ | Float64 f ->
137137+ encode_control elem.tag 11;
138138+ encode_tag buf elem.tag;
139139+ put_uint64_le buf (Int64.bits_of_float f)
140140+ | String s ->
141141+ let len = String.length s in
142142+ let size_code = uint_size_code (Int64.of_int len) in
143143+ encode_control elem.tag (12 + size_code);
144144+ encode_tag buf elem.tag;
145145+ encode_int buf (Int64.of_int len) size_code;
146146+ Buffer.add_string buf s
147147+ | Bytes s ->
148148+ let len = String.length s in
149149+ let size_code = uint_size_code (Int64.of_int len) in
150150+ encode_control elem.tag (16 + size_code);
151151+ encode_tag buf elem.tag;
152152+ encode_int buf (Int64.of_int len) size_code;
153153+ Buffer.add_string buf s
154154+ | Null ->
155155+ encode_control elem.tag 20;
156156+ encode_tag buf elem.tag
157157+ | Structure elems ->
158158+ encode_control elem.tag 21;
159159+ encode_tag buf elem.tag;
160160+ List.iter (encode_element buf) elems;
161161+ put_byte buf 0x18 (* End of container *)
162162+ | Array elems ->
163163+ encode_control elem.tag 22;
164164+ encode_tag buf elem.tag;
165165+ List.iter (encode_element buf) elems;
166166+ put_byte buf 0x18
167167+ | List elems ->
168168+ encode_control elem.tag 23;
169169+ encode_tag buf elem.tag;
170170+ List.iter (encode_element buf) elems;
171171+ put_byte buf 0x18
172172+173173+let encode elems =
174174+ let buf = Buffer.create 256 in
175175+ List.iter (encode_element buf) elems;
176176+ Buffer.contents buf
177177+178178+let encode_one elem = encode [ elem ]
179179+180180+(** {1 Decoding} *)
181181+182182+let get_byte data offset =
183183+ if offset >= String.length data then Error "Unexpected end of data"
184184+ else Ok (Char.code data.[offset], offset + 1)
185185+186186+let get_uint16_le data offset =
187187+ if offset + 2 > String.length data then Error "Unexpected end of data"
188188+ else
189189+ let b0 = Char.code data.[offset] in
190190+ let b1 = Char.code data.[offset + 1] in
191191+ Ok (b0 lor (b1 lsl 8), offset + 2)
192192+193193+let get_uint32_le data offset =
194194+ if offset + 4 > String.length data then Error "Unexpected end of data"
195195+ else
196196+ let b0 = Int32.of_int (Char.code data.[offset]) in
197197+ let b1 = Int32.of_int (Char.code data.[offset + 1]) in
198198+ let b2 = Int32.of_int (Char.code data.[offset + 2]) in
199199+ let b3 = Int32.of_int (Char.code data.[offset + 3]) in
200200+ Ok
201201+ ( Int32.(
202202+ add
203203+ (add b0 (shift_left b1 8))
204204+ (add (shift_left b2 16) (shift_left b3 24))),
205205+ offset + 4 )
206206+207207+let get_uint64_le data offset =
208208+ if offset + 8 > String.length data then Error "Unexpected end of data"
209209+ else
210210+ let get_byte i = Int64.of_int (Char.code data.[offset + i]) in
211211+ let v = ref 0L in
212212+ for i = 7 downto 0 do
213213+ v := Int64.add (Int64.shift_left !v 8) (get_byte i)
214214+ done;
215215+ Ok (!v, offset + 8)
216216+217217+let get_int data offset size_code =
218218+ match size_code with
219219+ | 0 ->
220220+ let* b, off = get_byte data offset in
221221+ let v = if b >= 128 then b - 256 else b in
222222+ Ok (Int64.of_int v, off)
223223+ | 1 ->
224224+ let* v, off = get_uint16_le data offset in
225225+ let v = if v >= 32768 then v - 65536 else v in
226226+ Ok (Int64.of_int v, off)
227227+ | 2 ->
228228+ let* v, off = get_uint32_le data offset in
229229+ Ok (Int64.of_int32 v, off)
230230+ | 3 -> get_uint64_le data offset
231231+ | _ -> Error "Invalid size code"
232232+233233+let get_uint data offset size_code =
234234+ match size_code with
235235+ | 0 ->
236236+ let* b, off = get_byte data offset in
237237+ Ok (Int64.of_int b, off)
238238+ | 1 ->
239239+ let* v, off = get_uint16_le data offset in
240240+ Ok (Int64.of_int v, off)
241241+ | 2 ->
242242+ let* v, off = get_uint32_le data offset in
243243+ Ok (Int64.of_int32 v |> Int64.logand 0xFFFFFFFFL, off)
244244+ | 3 -> get_uint64_le data offset
245245+ | _ -> Error "Invalid size code"
246246+247247+let get_bytes data offset len =
248248+ if len < 0 || offset < 0 || offset + len > String.length data then
249249+ Error "Unexpected end of data"
250250+ else Ok (String.sub data offset len, offset + len)
251251+252252+let decode_tag data offset tag_form_code =
253253+ match tag_form_code with
254254+ | 0 -> Ok (Anonymous, offset)
255255+ | 1 ->
256256+ let* t, off = get_byte data offset in
257257+ Ok (Context_specific t, off)
258258+ | 2 ->
259259+ let* t, off = get_uint16_le data offset in
260260+ Ok (Common_profile_2 t, off)
261261+ | 3 ->
262262+ let* t, off = get_uint32_le data offset in
263263+ Ok (Common_profile_4 t, off)
264264+ | 4 ->
265265+ let* t, off = get_uint16_le data offset in
266266+ Ok (Implicit_profile_2 t, off)
267267+ | 5 ->
268268+ let* t, off = get_uint32_le data offset in
269269+ Ok (Implicit_profile_4 t, off)
270270+ | 6 ->
271271+ let* v, off = get_uint32_le data offset in
272272+ let* t, off = get_uint16_le data off in
273273+ Ok (Fully_qualified_6 (v, t), off)
274274+ | 7 ->
275275+ let* v, off = get_uint32_le data offset in
276276+ let* t, off = get_uint32_le data off in
277277+ Ok (Fully_qualified_8 (v, t), off)
278278+ | _ -> Error "Invalid tag form"
279279+280280+let rec decode_element data offset =
281281+ let* control, offset = get_byte data offset in
282282+ let type_code = control land 0x1f in
283283+ let tag_form_code = (control lsr 5) land 0x7 in
284284+ if type_code = 24 then (* End of container *)
285285+ Ok (None, offset)
286286+ else
287287+ let* tag, offset = decode_tag data offset tag_form_code in
288288+ let* value, offset = decode_value data offset type_code in
289289+ Ok (Some { tag; value }, offset)
290290+291291+and decode_value data offset type_code =
292292+ match type_code with
293293+ | 0 | 1 | 2 | 3 ->
294294+ (* Signed int *)
295295+ let size_code = type_code in
296296+ let* v, off = get_int data offset size_code in
297297+ Ok (Int v, off)
298298+ | 4 | 5 | 6 | 7 ->
299299+ (* Unsigned int *)
300300+ let size_code = type_code - 4 in
301301+ let* v, off = get_uint data offset size_code in
302302+ Ok (Uint v, off)
303303+ | 8 -> Ok (Bool false, offset)
304304+ | 9 -> Ok (Bool true, offset)
305305+ | 10 ->
306306+ (* Float32 *)
307307+ let* bits, off = get_uint32_le data offset in
308308+ Ok (Float32 (Int32.float_of_bits bits), off)
309309+ | 11 ->
310310+ (* Float64 *)
311311+ let* bits, off = get_uint64_le data offset in
312312+ Ok (Float64 (Int64.float_of_bits bits), off)
313313+ | 12 | 13 | 14 | 15 ->
314314+ (* UTF8 string *)
315315+ let size_code = type_code - 12 in
316316+ let* len, off = get_uint data offset size_code in
317317+ let* s, off = get_bytes data off (Int64.to_int len) in
318318+ Ok (String s, off)
319319+ | 16 | 17 | 18 | 19 ->
320320+ (* Byte string *)
321321+ let size_code = type_code - 16 in
322322+ let* len, off = get_uint data offset size_code in
323323+ let* s, off = get_bytes data off (Int64.to_int len) in
324324+ Ok (Bytes s, off)
325325+ | 20 -> Ok (Null, offset)
326326+ | 21 ->
327327+ (* Structure *)
328328+ let* elems, off = decode_container data offset in
329329+ Ok (Structure elems, off)
330330+ | 22 ->
331331+ (* Array *)
332332+ let* elems, off = decode_container data offset in
333333+ Ok (Array elems, off)
334334+ | 23 ->
335335+ (* List *)
336336+ let* elems, off = decode_container data offset in
337337+ Ok (List elems, off)
338338+ | _ -> Error (Printf.sprintf "Unknown type code: %d" type_code)
339339+340340+and decode_container data offset =
341341+ let rec loop acc offset =
342342+ let* elem_opt, offset = decode_element data offset in
343343+ match elem_opt with
344344+ | None -> Ok (List.rev acc, offset)
345345+ | Some elem -> loop (elem :: acc) offset
346346+ in
347347+ loop [] offset
348348+349349+let decode data =
350350+ let rec loop acc offset =
351351+ if offset >= String.length data then Ok (List.rev acc)
352352+ else
353353+ let* elem_opt, offset = decode_element data offset in
354354+ match elem_opt with
355355+ | None -> Ok (List.rev acc)
356356+ | Some elem -> loop (elem :: acc) offset
357357+ in
358358+ loop [] 0
359359+360360+(** {1 Helper constructors} *)
361361+362362+let int ?(tag = Anonymous) v = { tag; value = Int v }
363363+let uint ?(tag = Anonymous) v = { tag; value = Uint v }
364364+let bool ?(tag = Anonymous) v = { tag; value = Bool v }
365365+let string ?(tag = Anonymous) v = { tag; value = String v }
366366+let bytes ?(tag = Anonymous) v = { tag; value = Bytes v }
367367+let null ?(tag = Anonymous) () = { tag; value = Null }
368368+let structure ?(tag = Anonymous) elems = { tag; value = Structure elems }
369369+let array ?(tag = Anonymous) elems = { tag; value = Array elems }
370370+let list ?(tag = Anonymous) elems = { tag; value = List elems }
371371+372372+(* Context-tagged helpers *)
373373+let ctx_int tag v = { tag = Context_specific tag; value = Int (Int64.of_int v) }
374374+375375+let ctx_uint tag v =
376376+ { tag = Context_specific tag; value = Uint (Int64.of_int v) }
377377+378378+let ctx_bool tag v = { tag = Context_specific tag; value = Bool v }
379379+let ctx_string tag v = { tag = Context_specific tag; value = String v }
380380+let ctx_bytes tag v = { tag = Context_specific tag; value = Bytes v }
381381+382382+let ctx_struct tag elems =
383383+ { tag = Context_specific tag; value = Structure elems }
384384+385385+let ctx_array tag elems = { tag = Context_specific tag; value = Array elems }
386386+387387+(** {1 Pretty printing} *)
388388+389389+let rec pp_value ppf = function
390390+ | Int v -> Fmt.pf ppf "%Ld" v
391391+ | Uint v -> Fmt.pf ppf "%Lu" v
392392+ | Bool b -> Fmt.pf ppf "%b" b
393393+ | Float32 f -> Fmt.pf ppf "%f" f
394394+ | Float64 f -> Fmt.pf ppf "%f" f
395395+ | String s -> Fmt.pf ppf "%S" s
396396+ | Bytes s -> Fmt.pf ppf "<bytes:%d>" (String.length s)
397397+ | Null -> Fmt.pf ppf "null"
398398+ | Structure elems -> Fmt.pf ppf "{%a}" pp_elements elems
399399+ | Array elems -> Fmt.pf ppf "[%a]" pp_elements elems
400400+ | List elems -> Fmt.pf ppf "(%a)" pp_elements elems
401401+402402+and pp_tag ppf = function
403403+ | Anonymous -> ()
404404+ | Context_specific t -> Fmt.pf ppf "%d: " t
405405+ | Common_profile_2 t -> Fmt.pf ppf "CP2(%d): " t
406406+ | Common_profile_4 t -> Fmt.pf ppf "CP4(%ld): " t
407407+ | Implicit_profile_2 t -> Fmt.pf ppf "IP2(%d): " t
408408+ | Implicit_profile_4 t -> Fmt.pf ppf "IP4(%ld): " t
409409+ | Fully_qualified_6 (v, t) -> Fmt.pf ppf "FQ6(%ld,%d): " v t
410410+ | Fully_qualified_8 (v, t) -> Fmt.pf ppf "FQ8(%ld,%ld): " v t
411411+412412+and pp_element ppf elem = Fmt.pf ppf "%a%a" pp_tag elem.tag pp_value elem.value
413413+414414+and pp_elements ppf elems =
415415+ Fmt.pf ppf "%a" (Fmt.list ~sep:(Fmt.any ", ") pp_element) elems
416416+417417+let pp ppf elems =
418418+ Fmt.pf ppf "%a" (Fmt.list ~sep:(Fmt.any "; ") pp_element) elems
+124
lib/tlv.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Matter TLV (Tag-Length-Value) encoding and decoding.
77+88+ Binary serialization format used by Matter for protocol messages. Supports
99+ integers, booleans, strings, bytes, floats, and nested containers
1010+ (structures, arrays, lists).
1111+1212+ See
1313+ {{:https://csa-iot.org/developer-resource/specifications-download-request/}
1414+ Matter Core Specification} Appendix A for the full TLV format
1515+ specification. *)
1616+1717+(** {1:types Types} *)
1818+1919+(** Tag forms for element identification. *)
2020+type tag_form =
2121+ | Anonymous
2222+ | Context_specific of int
2323+ | Common_profile_2 of int
2424+ | Common_profile_4 of int32
2525+ | Implicit_profile_2 of int
2626+ | Implicit_profile_4 of int32
2727+ | Fully_qualified_6 of int32 * int
2828+ | Fully_qualified_8 of int32 * int32
2929+3030+(** TLV value. *)
3131+type value =
3232+ | Int of int64
3333+ | Uint of int64
3434+ | Bool of bool
3535+ | Float32 of float
3636+ | Float64 of float
3737+ | String of string
3838+ | Bytes of string
3939+ | Null
4040+ | Structure of element list
4141+ | Array of element list
4242+ | List of element list
4343+4444+and element = { tag : tag_form; value : value }
4545+(** TLV element (tagged value). *)
4646+4747+(** {1:encoding Encoding} *)
4848+4949+val encode : element list -> string
5050+(** [encode elems] encodes elements to binary TLV format. *)
5151+5252+val encode_one : element -> string
5353+(** [encode_one elem] encodes a single element to binary TLV format. *)
5454+5555+(** {1:decoding Decoding} *)
5656+5757+val decode : string -> (element list, string) result
5858+(** [decode data] decodes binary TLV data to elements. *)
5959+6060+(** {1:constructors Element constructors} *)
6161+6262+val int : ?tag:tag_form -> int64 -> element
6363+(** [int ?tag v] creates a signed integer element. *)
6464+6565+val uint : ?tag:tag_form -> int64 -> element
6666+(** [uint ?tag v] creates an unsigned integer element. *)
6767+6868+val bool : ?tag:tag_form -> bool -> element
6969+(** [bool ?tag v] creates a boolean element. *)
7070+7171+val string : ?tag:tag_form -> string -> element
7272+(** [string ?tag v] creates a UTF-8 string element. *)
7373+7474+val bytes : ?tag:tag_form -> string -> element
7575+(** [bytes ?tag v] creates a byte string element. *)
7676+7777+val null : ?tag:tag_form -> unit -> element
7878+(** [null ?tag ()] creates a null element. *)
7979+8080+val structure : ?tag:tag_form -> element list -> element
8181+(** [structure ?tag elems] creates a structure container. *)
8282+8383+val array : ?tag:tag_form -> element list -> element
8484+(** [array ?tag elems] creates an array container. *)
8585+8686+val list : ?tag:tag_form -> element list -> element
8787+(** [list ?tag elems] creates a list container. *)
8888+8989+(** {2:context Context-tagged constructors} *)
9090+9191+val ctx_int : int -> int -> element
9292+(** [ctx_int tag v] creates a context-tagged signed integer. *)
9393+9494+val ctx_uint : int -> int -> element
9595+(** [ctx_uint tag v] creates a context-tagged unsigned integer. *)
9696+9797+val ctx_bool : int -> bool -> element
9898+(** [ctx_bool tag v] creates a context-tagged boolean. *)
9999+100100+val ctx_string : int -> string -> element
101101+(** [ctx_string tag v] creates a context-tagged UTF-8 string. *)
102102+103103+val ctx_bytes : int -> string -> element
104104+(** [ctx_bytes tag v] creates a context-tagged byte string. *)
105105+106106+val ctx_struct : int -> element list -> element
107107+(** [ctx_struct tag elems] creates a context-tagged structure. *)
108108+109109+val ctx_array : int -> element list -> element
110110+(** [ctx_array tag elems] creates a context-tagged array. *)
111111+112112+(** {1:pp Pretty printing} *)
113113+114114+val pp_value : value Fmt.t
115115+(** [pp_value] pretty-prints a TLV value. *)
116116+117117+val pp_tag : tag_form Fmt.t
118118+(** [pp_tag] pretty-prints a tag form. *)
119119+120120+val pp_element : element Fmt.t
121121+(** [pp_element] pretty-prints an element. *)
122122+123123+val pp : element list Fmt.t
124124+(** [pp] pretty-prints a list of elements. *)