├── lib ├── mqtt │ ├── Mqtt.ml │ └── dune └── mqtt_client │ ├── Mqtt_core.ml │ ├── dune │ ├── Read_buffer.mli │ ├── Read_buffer.ml │ ├── Mqtt_client.mli │ ├── Mqtt_client.ml │ └── Mqtt_packet.ml ├── .gitignore ├── examples ├── dune └── Basic.ml ├── docs ├── odoc.support │ ├── fonts │ │ ├── KaTeX_Main-Bold.woff2 │ │ ├── KaTeX_AMS-Regular.woff2 │ │ ├── KaTeX_Fraktur-Bold.woff2 │ │ ├── KaTeX_Main-Italic.woff2 │ │ ├── KaTeX_Main-Regular.woff2 │ │ ├── KaTeX_Math-Italic.woff2 │ │ ├── KaTeX_Size1-Regular.woff2 │ │ ├── KaTeX_Size2-Regular.woff2 │ │ ├── KaTeX_Size3-Regular.woff2 │ │ ├── KaTeX_Size4-Regular.woff2 │ │ ├── KaTeX_Fraktur-Regular.woff2 │ │ ├── KaTeX_Main-BoldItalic.woff2 │ │ ├── KaTeX_Math-BoldItalic.woff2 │ │ ├── KaTeX_SansSerif-Bold.woff2 │ │ ├── KaTeX_Script-Regular.woff2 │ │ ├── KaTeX_Caligraphic-Bold.woff2 │ │ ├── KaTeX_SansSerif-Italic.woff2 │ │ ├── KaTeX_SansSerif-Regular.woff2 │ │ ├── KaTeX_Typewriter-Regular.woff2 │ │ ├── fira-mono-v14-latin-500.woff2 │ │ ├── fira-sans-v17-latin-500.woff2 │ │ ├── fira-sans-v17-latin-700.woff2 │ │ ├── KaTeX_Caligraphic-Regular.woff2 │ │ ├── fira-sans-v17-latin-italic.woff2 │ │ ├── noticia-text-v15-latin-700.woff2 │ │ ├── fira-mono-v14-latin-regular.woff2 │ │ ├── fira-sans-v17-latin-500italic.woff2 │ │ ├── fira-sans-v17-latin-700italic.woff2 │ │ ├── fira-sans-v17-latin-regular.woff2 │ │ ├── noticia-text-v15-latin-italic.woff2 │ │ └── noticia-text-v15-latin-regular.woff2 │ ├── odoc_search.js │ ├── katex.min.css │ └── odoc.css ├── index.html └── mqtt │ ├── Mqtt │ └── index.html │ ├── index.html │ └── Mqtt_client │ └── index.html ├── .ocamlformat ├── onix.mk ├── tests ├── dune ├── Test_read_buffer.ml ├── subscriptions.ml └── client_test.ml ├── default.nix ├── Makefile ├── dune-project ├── mqtt.opam ├── README.md └── etc └── odoc.css /lib/mqtt/Mqtt.ml: -------------------------------------------------------------------------------- 1 | module Client = Mqtt_client 2 | -------------------------------------------------------------------------------- /lib/mqtt/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name mqtt) 3 | (libraries mqtt_client)) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.byte 3 | _build 4 | _tmp 5 | _esy 6 | setup.data 7 | setup.log 8 | !.gitignore 9 | *.install 10 | -------------------------------------------------------------------------------- /examples/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name Basic) 3 | (modules Basic) 4 | (preprocess 5 | (pps lwt_ppx)) 6 | (libraries mqtt.client lwt)) 7 | -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Main-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Main-Bold.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_AMS-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_AMS-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Fraktur-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Fraktur-Bold.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Main-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Main-Italic.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Main-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Main-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Math-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Math-Italic.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Size1-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Size1-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Size2-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Size2-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Size3-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Size3-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Size4-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Size4-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Fraktur-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Fraktur-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Main-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Main-BoldItalic.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Math-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Math-BoldItalic.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_SansSerif-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_SansSerif-Bold.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Script-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Script-Regular.woff2 -------------------------------------------------------------------------------- /lib/mqtt_client/Mqtt_core.ml: -------------------------------------------------------------------------------- 1 | type credentials = Credentials of string * string | Username of string 2 | type qos = Atmost_once | Atleast_once | Exactly_once 3 | -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Caligraphic-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Caligraphic-Bold.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_SansSerif-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_SansSerif-Italic.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_SansSerif-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_SansSerif-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Typewriter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Typewriter-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/fira-mono-v14-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/fira-mono-v14-latin-500.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/fira-sans-v17-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/fira-sans-v17-latin-500.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/fira-sans-v17-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/fira-sans-v17-latin-700.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/KaTeX_Caligraphic-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/KaTeX_Caligraphic-Regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/fira-sans-v17-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/fira-sans-v17-latin-italic.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/noticia-text-v15-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/noticia-text-v15-latin-700.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/fira-mono-v14-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/fira-mono-v14-latin-regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/fira-sans-v17-latin-500italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/fira-sans-v17-latin-500italic.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/fira-sans-v17-latin-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/fira-sans-v17-latin-700italic.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/fira-sans-v17-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/fira-sans-v17-latin-regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.support/fonts/noticia-text-v15-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/noticia-text-v15-latin-italic.woff2 -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version = 0.26.1 2 | profile = conventional 3 | 4 | ocaml-version = 4.08.0 5 | break-infix = fit-or-vertical 6 | parse-docstrings = true 7 | cases-exp-indent = 2 8 | -------------------------------------------------------------------------------- /docs/odoc.support/fonts/noticia-text-v15-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyper-systems/ocaml-mqtt/HEAD/docs/odoc.support/fonts/noticia-text-v15-latin-regular.woff2 -------------------------------------------------------------------------------- /onix.mk: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: shell 3 | shell: 4 | nix develop -f default.nix -j auto -i -k TERM -k PATH -k HOME -v shell 5 | 6 | .PHONY: lock 7 | lock: 8 | nix develop -f default.nix lock 9 | -------------------------------------------------------------------------------- /tests/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name client_test) 3 | (modules client_test) 4 | (preprocess 5 | (pps lwt_ppx)) 6 | (libraries mqtt.client lwt alcotest alcotest-lwt cohttp cohttp-lwt-unix)) 7 | -------------------------------------------------------------------------------- /lib/mqtt_client/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name mqtt_client) 3 | (public_name mqtt.client) 4 | (preprocess 5 | (pps lwt_ppx)) 6 | (libraries fmt lwt lwt.unix logs logs.lwt tls tls-lwt ocplib-endian)) 7 | -------------------------------------------------------------------------------- /lib/mqtt_client/Read_buffer.mli: -------------------------------------------------------------------------------- 1 | type t 2 | 3 | (* val empty : unit -> t *) 4 | val make : string -> t 5 | 6 | (* val add_string : t -> string -> unit *) 7 | val len : t -> int 8 | val read : t -> int -> string 9 | val read_string : t -> string 10 | val read_uint8 : t -> int 11 | val read_uint16 : t -> int 12 | val read_all : t -> (t -> 'a) -> 'a list 13 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | onix = import (builtins.fetchGit { 3 | url = "https://github.com/rizo/onix.git"; 4 | rev = "1a67fe51d4d1676f38088e635a00dfdae5bae70b"; 5 | }) { verbosity = "info"; }; 6 | 7 | in onix.env { 8 | path = ./.; 9 | vars = { 10 | "with-test" = true; 11 | "with-doc" = true; 12 | "with-dev-setup" = true; 13 | }; 14 | deps = { "ocaml-base-compiler" = "5.1.0"; }; 15 | } 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | 3 | .PHONY: build 4 | build: 5 | dune build --root=. 6 | 7 | .PHONY: rebuild 8 | rebuild: 9 | dune build --root=. -w 10 | 11 | .PHONY: test 12 | test: 13 | dune build --root=. @runtest 14 | 15 | .PHONY: retest 16 | retest: 17 | dune build --root=. -w @runtest 18 | 19 | .PHONY: doc 20 | doc: 21 | dune build --root=. @doc 22 | 23 | .PHONY: redoc 24 | redoc: 25 | dune build --root=. -w @doc 26 | 27 | .PHONY: clean 28 | clean: 29 | dune clean --root=. 30 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | index 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

OCaml package documentation

13 |
    14 |
  1. mqtt
  2. 15 |
16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /docs/mqtt/Mqtt/index.html: -------------------------------------------------------------------------------- 1 | 2 | Mqtt (mqtt.Mqtt)

Module Mqtt

module Client = Mqtt_client
3 | -------------------------------------------------------------------------------- /examples/Basic.ml: -------------------------------------------------------------------------------- 1 | module C = Mqtt_client 2 | 3 | let host = "127.0.0.1" 4 | let port = 1883 5 | 6 | let sub_example () = 7 | let on_message ~topic payload = Lwt_io.printlf "%s: %s" topic payload in 8 | let%lwt () = Lwt_io.printl "Starting subscriber..." in 9 | let%lwt client = C.connect ~on_message ~id:"client-1" ~port [ host ] in 10 | C.subscribe [ ("topic-1", C.Atmost_once) ] client 11 | 12 | let[@warning "-unused-value-declaration"] pub_example () = 13 | let%lwt () = Lwt_io.printl "Starting publisher..." in 14 | let%lwt client = C.connect ~id:"client-1" ~port [ host ] in 15 | let rec loop () = 16 | let%lwt () = Lwt_io.printl "Publishing..." in 17 | let%lwt line = Lwt_io.read_line Lwt_io.stdin in 18 | let%lwt () = C.publish ~qos:C.Atleast_once ~topic:"topic-1" line client in 19 | let%lwt () = Lwt_io.printl "Published." in 20 | loop () 21 | in 22 | loop () 23 | 24 | let () = Lwt_main.run (sub_example ()) 25 | -------------------------------------------------------------------------------- /docs/mqtt/index.html: -------------------------------------------------------------------------------- 1 | 2 | index (mqtt.index)

mqtt index

Library mqtt

The entry point of this library is the module: Mqtt.

Library mqtt.client

The entry point of this library is the module: Mqtt_client.

3 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.13) 2 | 3 | (name mqtt) 4 | 5 | (generate_opam_files true) 6 | 7 | (source 8 | (github hyper-systems/ocaml-mqtt)) 9 | 10 | (authors "Rizo I. " "Josh Allmann ") 11 | 12 | (maintainers "Rizo I. " "Leo Soares ") 13 | 14 | (license BSD-3-clause) 15 | 16 | (documentation https://hyper-systems.github.io/ocaml-mqtt) 17 | 18 | (package 19 | (name mqtt) 20 | (synopsis "OCaml MQTT client") 21 | (depends 22 | (ocaml 23 | (>= 4.08.0)) 24 | dune 25 | (ocplib-endian 26 | (>= 0.6)) 27 | (logs 28 | (>= 0.7.0)) 29 | (fmt 30 | (>= 0.8.7)) 31 | (lwt 32 | (>= 5.7.0)) 33 | (lwt_ppx 34 | (>= 2.1.0)) 35 | (tls 36 | (>= 0.17.3)) 37 | (tls-lwt 38 | (>= 0.17.3)) 39 | (alcotest 40 | (and 41 | :with-test 42 | (>= 1.5.0))) 43 | (alcotest-lwt 44 | (and 45 | :with-test 46 | (>= 1.5.0))) 47 | (cohttp 48 | (and 49 | :with-test 50 | (>= 5.0.0))) 51 | (cohttp-lwt-unix 52 | (and 53 | :with-test 54 | (>= 5.0.0))) 55 | (ocaml-lsp-server :with-dev-setup) 56 | (ocamlformat 57 | (and 58 | :with-dev-setup 59 | (>= 0.26.1))) 60 | (odoc 61 | (and 62 | :with-doc 63 | (>= 2.4.1))))) 64 | -------------------------------------------------------------------------------- /mqtt.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "OCaml MQTT client" 4 | maintainer: ["Rizo I. " "Leo Soares "] 5 | authors: ["Rizo I. " "Josh Allmann "] 6 | license: "BSD-3-clause" 7 | homepage: "https://github.com/hyper-systems/ocaml-mqtt" 8 | doc: "https://hyper-systems.github.io/ocaml-mqtt" 9 | bug-reports: "https://github.com/hyper-systems/ocaml-mqtt/issues" 10 | depends: [ 11 | "ocaml" {>= "4.08.0"} 12 | "dune" {>= "3.13"} 13 | "ocplib-endian" {>= "0.6"} 14 | "logs" {>= "0.7.0"} 15 | "fmt" {>= "0.8.7"} 16 | "lwt" {>= "5.7.0"} 17 | "lwt_ppx" {>= "2.1.0"} 18 | "tls" {>= "0.17.3"} 19 | "tls-lwt" {>= "0.17.3"} 20 | "alcotest" {with-test & >= "1.5.0"} 21 | "alcotest-lwt" {with-test & >= "1.5.0"} 22 | "cohttp" {with-test & >= "5.0.0"} 23 | "cohttp-lwt-unix" {with-test & >= "5.0.0"} 24 | "ocaml-lsp-server" {with-dev-setup} 25 | "ocamlformat" {with-dev-setup & >= "0.26.1"} 26 | "odoc" {with-doc & >= "2.4.1"} 27 | ] 28 | build: [ 29 | ["dune" "subst"] {dev} 30 | [ 31 | "dune" 32 | "build" 33 | "-p" 34 | name 35 | "-j" 36 | jobs 37 | "@install" 38 | "@runtest" {with-test} 39 | "@doc" {with-doc} 40 | ] 41 | ] 42 | dev-repo: "git+https://github.com/hyper-systems/ocaml-mqtt.git" 43 | -------------------------------------------------------------------------------- /lib/mqtt_client/Read_buffer.ml: -------------------------------------------------------------------------------- 1 | module BE = EndianBytes.BigEndian 2 | 3 | type t = { mutable pos : int; mutable buf : bytes } 4 | 5 | let create () = { pos = 0; buf = Bytes.of_string "" } 6 | 7 | let add_string rb str = 8 | let str = Bytes.of_string str in 9 | let curlen = Bytes.length rb.buf - rb.pos in 10 | let strlen = Bytes.length str in 11 | let newlen = strlen + curlen in 12 | let newbuf = Bytes.create newlen in 13 | Bytes.blit rb.buf rb.pos newbuf 0 curlen; 14 | Bytes.blit str 0 newbuf curlen strlen; 15 | rb.pos <- 0; 16 | rb.buf <- newbuf 17 | 18 | let make str = 19 | let rb = create () in 20 | add_string rb str; 21 | rb 22 | 23 | let len rb = Bytes.length rb.buf - rb.pos 24 | 25 | let read rb count = 26 | let len = Bytes.length rb.buf - rb.pos in 27 | if count < 0 || len < count then raise (Invalid_argument "buffer underflow"); 28 | let ret = Bytes.sub rb.buf rb.pos count in 29 | rb.pos <- rb.pos + count; 30 | Bytes.to_string ret 31 | 32 | let read_uint8 rb = 33 | let str = rb.buf in 34 | let slen = Bytes.length str - rb.pos in 35 | if slen < 1 then raise (Invalid_argument "string too short"); 36 | let res = BE.get_uint8 str rb.pos in 37 | rb.pos <- rb.pos + 1; 38 | res 39 | 40 | let read_uint16 rb = 41 | let str = rb.buf in 42 | let slen = Bytes.length str - rb.pos in 43 | if slen < 2 then raise (Invalid_argument "string too short"); 44 | let res = BE.get_uint16 str rb.pos in 45 | rb.pos <- rb.pos + 2; 46 | res 47 | 48 | let read_string rb = read_uint16 rb |> read rb 49 | 50 | let read_all rb f = 51 | let rec loop res = if len rb <= 0 then res else loop (f rb :: res) in 52 | loop [] 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OCaml MQTT Client 2 | 3 | [![OCaml-CI Build Status](https://img.shields.io/endpoint?url=https%3A%2F%2Fci.ocamllabs.io%2Fbadge%2Fhyper-systems%2Focaml-mqtt%2Fmaster&logo=ocaml)](https://ci.ocamllabs.io/github/hyper-systems/ocaml-mqtt) 4 | 5 | This library implements the client MQTT v3 protocol. 6 | 7 | * [API Documentation](https://hyper.systems/ocaml-mqtt/mqtt/index.html) 8 | * [Issues](https://github.com/hyper-systems/ocaml-mqtt/issues) 9 | 10 | > Originally forked from https://github.com/j0sh/ocaml-mqtt. 11 | 12 | ## Quickstart 13 | 14 | Install the packaage: 15 | ``` 16 | $ opam install mqtt 17 | ``` 18 | 19 | In your dune project add the following dependencies to your dune file: 20 | ```lisp 21 | (executable 22 | (name My_app) 23 | (public_name my_app) 24 | (libraries mqtt.client lwt) 25 | (preprocess (pps lwt_ppx))) 26 | ``` 27 | 28 | ## Examples 29 | 30 | Here is a basic example of a subscriber: 31 | ```ocaml 32 | module C = Mqtt_client 33 | 34 | let host = "127.0.0.1" 35 | let port = 1883 36 | 37 | let sub_example () = 38 | let on_message ~topic payload = Lwt_io.printlf "%s: %s" topic payload in 39 | let%lwt () = Lwt_io.printl "Starting subscriber..." in 40 | let%lwt client = C.connect ~on_message ~id:"client-1" ~port [ host ] in 41 | C.subscribe [ ("topic-1", C.Atmost_once) ] client 42 | 43 | let pub_example () = 44 | let%lwt () = Lwt_io.printl "Starting publisher..." in 45 | let%lwt client = C.connect ~id:"client-1" ~port [ host ] in 46 | let rec loop () = 47 | let%lwt () = Lwt_io.printl "Publishing..." in 48 | let%lwt line = Lwt_io.read_line Lwt_io.stdin in 49 | let%lwt () = C.publish ~qos:C.Atleast_once ~topic:"topic-1" line client in 50 | let%lwt () = Lwt_io.printl "Published." in 51 | loop () 52 | in 53 | loop () 54 | 55 | let () = Lwt_main.run (sub_example ()) 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/odoc.support/odoc_search.js: -------------------------------------------------------------------------------- 1 | /* The browsers interpretation of the CORS origin policy prevents to run 2 | webworkers from javascript files fetched from the file:// protocol. This hack 3 | is to workaround this restriction. */ 4 | function createWebWorker() { 5 | var searchs = search_urls.map((search_url) => { 6 | let parts = document.location.href.split("/"); 7 | parts[parts.length - 1] = search_url; 8 | return '"' + parts.join("/") + '"'; 9 | }); 10 | blobContents = ["importScripts(" + searchs.join(",") + ");"]; 11 | var blob = new Blob(blobContents, { type: "application/javascript" }); 12 | var blobUrl = URL.createObjectURL(blob); 13 | 14 | var worker = new Worker(blobUrl); 15 | URL.revokeObjectURL(blobUrl); 16 | 17 | return worker; 18 | } 19 | 20 | var worker; 21 | var waiting = 0; 22 | 23 | function wait() { 24 | waiting = waiting + 1; 25 | document.querySelector(".search-snake").classList.add("search-busy"); 26 | } 27 | 28 | function stop_waiting() { 29 | if (waiting > 0) waiting = waiting - 1; 30 | else waiting = 0; 31 | if (waiting == 0) { 32 | document.querySelector(".search-snake").classList.remove("search-busy"); 33 | } 34 | } 35 | 36 | document.querySelector(".search-bar").addEventListener("focus", (ev) => { 37 | if (typeof worker == "undefined") { 38 | worker = createWebWorker(); 39 | worker.onmessage = (e) => { 40 | stop_waiting(); 41 | let results = e.data; 42 | let search_results = document.querySelector(".search-result"); 43 | search_results.innerHTML = ""; 44 | let f = (entry) => { 45 | let search_result = document.createElement("a"); 46 | search_result.classList.add("search-entry"); 47 | search_result.href = base_url + entry.url; 48 | search_result.innerHTML = entry.html; 49 | search_results.appendChild(search_result); 50 | }; 51 | results.forEach(f); 52 | let search_request = document.querySelector(".search-bar").value; 53 | if (results.length == 0 && search_request != "") { 54 | let no_result = document.createElement("div"); 55 | no_result.classList.add("search-no-result"); 56 | no_result.innerText = "No result..."; 57 | search_results.appendChild(no_result); 58 | } 59 | }; 60 | } 61 | }); 62 | 63 | document.querySelector(".search-bar").addEventListener("input", (ev) => { 64 | wait(); 65 | worker.postMessage(ev.target.value); 66 | }); 67 | -------------------------------------------------------------------------------- /lib/mqtt_client/Mqtt_client.mli: -------------------------------------------------------------------------------- 1 | type t 2 | (** Represents an MQTT client. 3 | 4 | To create a new client use {!val:Mqtt_client.connect}. *) 5 | 6 | (** Client authentication credentials. 7 | 8 | MQTT supports two authentication methods: username with password, or 9 | username only. 10 | 11 | The credentials will be sent in plain text, unless TLS is used. See 12 | {{!val:Mqtt_client.connect} connection options} for more information. *) 13 | type credentials = Credentials of string * string | Username of string 14 | 15 | (** Client error & exceptions 16 | 17 | Defines the exceptions raised by the client *) 18 | 19 | exception Connection_error 20 | 21 | (** Quality of Service level. 22 | 23 | Defines the guarantee of delivery for messages. *) 24 | type qos = Atmost_once | Atleast_once | Exactly_once 25 | 26 | val connect : 27 | ?id:string -> 28 | ?tls_ca:string -> 29 | ?credentials:credentials -> 30 | ?will:string * string -> 31 | ?clean_session:bool -> 32 | ?keep_alive:int -> 33 | ?on_message:(topic:string -> string -> unit Lwt.t) -> 34 | ?on_disconnect:(t -> unit Lwt.t) -> 35 | ?on_error:(t -> exn -> unit Lwt.t) -> 36 | ?port:int -> 37 | string list -> 38 | t Lwt.t 39 | (** Connects to the MQTT broker. 40 | 41 | Multiple hosts can be provided in case the broker supports failover. The 42 | client will attempt to connect to each one of hosts sequentially until one 43 | of them is successful. 44 | 45 | [on_error] can be provided to handle errors during client's execution. By 46 | default all internal exceptions will be raised with [Lwt.fail]. 47 | 48 | {i Note:} Reconnection logic is not implemented currently. 49 | 50 | {[ 51 | let broker_hosts = [ ("host-1", "host-2") ] in 52 | 53 | let on_message ~topic payload = 54 | Lwt.printlf "topic=%S payload=%S" topic payload 55 | in 56 | 57 | Mqtt_client.connect ~id:"my-client" ~port:1883 ~on_message broker_hosts 58 | |> Lwt_main.run 59 | ]} *) 60 | 61 | val disconnect : t -> unit Lwt.t 62 | (** Disconnects the client from the MQTT broker. 63 | 64 | {[ 65 | let%lwt () = Mqtt_client.disconnect client 66 | ]} *) 67 | 68 | val publish : 69 | ?dup:bool -> 70 | ?qos:qos -> 71 | ?retain:bool -> 72 | topic:string -> 73 | string -> 74 | t -> 75 | unit Lwt.t 76 | (** Publish a message with payload to a given topic. 77 | 78 | {[ 79 | let payload = "Hello world"; 80 | let%lwt () = Mqtt_client.publish(~topic="news", payload, client); 81 | ]} *) 82 | 83 | val subscribe : (string * qos) list -> t -> unit Lwt.t 84 | (** Subscribes the client to a non-empty list of topics. 85 | 86 | {[ 87 | let topics = 88 | [ 89 | ("news/fashion", Mqtt_client.Atmost_once); 90 | ("news/science", Mqtt_client.Atleast_once); 91 | ] 92 | in 93 | Mqtt_client.subscribe topics client 94 | ]} *) 95 | -------------------------------------------------------------------------------- /tests/Test_read_buffer.ml: -------------------------------------------------------------------------------- 1 | open Mqtt_client.Read_buffer 2 | 3 | let test_create _ = 4 | let rb = create () in 5 | assert_equal 0 rb.pos; 6 | assert_equal Bytes.empty rb.buf 7 | 8 | let test_add _ = 9 | let rb = create () in 10 | add_string rb "asdf"; 11 | assert_equal (Bytes.of_string "asdf") rb.buf; 12 | add_string rb "qwerty"; 13 | assert_equal (Bytes.of_string "asdfqwerty") rb.buf; 14 | (* test appends via manually resetting pos *) 15 | rb.pos <- 4; 16 | add_string rb "poiuy"; 17 | assert_equal (Bytes.of_string "qwertypoiuy") rb.buf; 18 | assert_equal 0 rb.pos 19 | 20 | let test_make _ = 21 | let rb = make "asdf" in 22 | assert_equal 0 rb.pos; 23 | assert_equal (Bytes.of_string "asdf") rb.buf 24 | 25 | let test_len _ = 26 | let rb = create () in 27 | assert_equal 0 (len rb); 28 | add_string rb "asdf"; 29 | assert_equal 4 (len rb); 30 | let _ = read rb 2 in 31 | assert_equal 2 (len rb); 32 | let _ = read rb 2 in 33 | assert_equal 0 (len rb) 34 | 35 | let test_read _ = 36 | let rb = create () in 37 | let exn = Invalid_argument "buffer underflow" in 38 | assert_raises exn (fun () -> read rb 1); 39 | let rb = make "asdf" in 40 | assert_raises exn (fun () -> read rb (-1)); 41 | assert_equal "" (read rb 0); 42 | assert_equal "as" (read rb 2); 43 | assert_raises exn (fun () -> read rb 3); 44 | assert_equal 2 rb.pos; 45 | assert_equal "df" (read rb 2); 46 | assert_raises exn (fun () -> read rb 1); 47 | assert_equal 4 rb.pos 48 | 49 | let test_uint8 _ = 50 | let printer = string_of_int in 51 | let rb = create () in 52 | let exn = Invalid_argument "string too short" in 53 | assert_raises exn (fun () -> read_uint8 rb); 54 | let rb = make "\001\002\255" in 55 | assert_equal 1 (read_uint8 rb); 56 | assert_equal 1 rb.pos; 57 | assert_equal 2 (read_uint8 rb); 58 | assert_equal 2 rb.pos; 59 | assert_equal ~printer 255 (read_uint8 rb) 60 | 61 | let test_int16 _ = 62 | let printer = string_of_int in 63 | let rb = create () in 64 | let exn = Invalid_argument "string too short" in 65 | assert_raises exn (fun () -> read_uint16 rb); 66 | let rb = make "\001" in 67 | assert_raises exn (fun () -> read_uint16 rb); 68 | let rb = make "\001\002" in 69 | assert_equal 258 (read_uint16 rb); 70 | let rb = make "\255\255\128" in 71 | assert_equal ~printer 65535 (read_uint16 rb) 72 | 73 | let test_readstr _ = 74 | let rb = create () in 75 | let exn1 = Invalid_argument "string too short" in 76 | let exn2 = Invalid_argument "buffer underflow" in 77 | assert_raises exn1 (fun () -> read_string rb); 78 | let rb = make "\000" in 79 | assert_raises exn1 (fun () -> read_string rb); 80 | let rb = make "\000\001" in 81 | assert_raises exn2 (fun () -> read_string rb); 82 | let rb = make "\000\004asdf\000\006qwerty" in 83 | assert_equal "asdf" (read_string rb); 84 | assert_equal 6 rb.pos; 85 | assert_equal "qwerty" (read_string rb); 86 | assert_equal 14 rb.pos 87 | 88 | let test_readall _ = 89 | let rb = make "\001\002\003\004\005" in 90 | let res = read_all rb read_uint8 in 91 | assert_equal res [ 5; 4; 3; 2; 1 ]; 92 | assert_equal 0 (len rb) 93 | 94 | let tests = 95 | [ 96 | "create" >:: test_create; 97 | "add" >:: test_add; 98 | "make" >:: test_make; 99 | "rb_len" >:: test_len; 100 | "read" >:: test_read; 101 | "read_uint8" >:: test_uint8; 102 | "read_int16" >:: test_int16; 103 | "read_string" >:: test_readstr; 104 | "read_all" >:: test_readall; 105 | ] 106 | -------------------------------------------------------------------------------- /docs/mqtt/Mqtt_client/index.html: -------------------------------------------------------------------------------- 1 | 2 | Mqtt_client (mqtt.Mqtt_client)

Module Mqtt_client

type t

Represents an MQTT client.

To create a new client use Mqtt_client.connect.

type credentials =
  1. | Credentials of string * string
  2. | Username of string

Client authentication credentials.

MQTT supports two authentication methods: username with password, or username only.

The credentials will be sent in plain text, unless TLS is used. See connection options for more information.

Client error & exceptions

Defines the exceptions raised by the client

exception Connection_error
type qos =
  1. | Atmost_once
  2. | Atleast_once
  3. | Exactly_once

Quality of Service level.

Defines the guarantee of delivery for messages.

val connect : 3 | ?id:string -> 4 | ?tls_ca:string -> 5 | ?credentials:credentials -> 6 | ?will:(string * string) -> 7 | ?clean_session:bool -> 8 | ?keep_alive:int -> 9 | ?on_message:(topic:string -> string -> unit Lwt.t) -> 10 | ?on_disconnect:(t -> unit Lwt.t) -> 11 | ?on_error:(t -> exn -> unit Lwt.t) -> 12 | ?port:int -> 13 | string list -> 14 | t Lwt.t

Connects to the MQTT broker.

Multiple hosts can be provided in case the broker supports failover. The client will attempt to connect to each one of hosts sequentially until one of them is successful.

on_error can be provided to handle errors during client's execution. By default all internal exceptions will be raised with Lwt.fail.

Note: Reconnection logic is not implemented currently.

let broker_hosts = [ ("host-1", "host-2") ] in
15 | 
16 | let on_message ~topic payload =
17 |   Lwt.printlf "topic=%S payload=%S" topic payload
18 | in
19 | 
20 | Mqtt_client.connect ~id:"my-client" ~port:1883 ~on_message broker_hosts
21 | |> Lwt_main.run
val disconnect : t -> unit Lwt.t

Disconnects the client from the MQTT broker.

let%lwt () = Mqtt_client.disconnect client
val publish : 22 | ?dup:bool -> 23 | ?qos:qos -> 24 | ?retain:bool -> 25 | topic:string -> 26 | string -> 27 | t -> 28 | unit Lwt.t

Publish a message with payload to a given topic.

let payload = "Hello world";
29 | let%lwt () = Mqtt_client.publish(~topic="news", payload, client);
val subscribe : (string * qos) list -> t -> unit Lwt.t

Subscribes the client to a non-empty list of topics.

let topics =
30 |   [
31 |     ("news/fashion", Mqtt_client.Atmost_once);
32 |     ("news/science", Mqtt_client.Atleast_once);
33 |   ]
34 | in
35 | Mqtt_client.subscribe topics client
36 | -------------------------------------------------------------------------------- /lib/mqtt_client/Mqtt_client.ml: -------------------------------------------------------------------------------- 1 | let fmt = Format.asprintf 2 | 3 | type connection = Lwt_io.input_channel * Lwt_io.output_channel 4 | 5 | let decode_length inch = 6 | let rec loop value mult = 7 | let%lwt ch = Lwt_io.read_char inch in 8 | let ch = Char.code ch in 9 | let digit = ch land 127 in 10 | let value = value + (digit * mult) in 11 | let mult = mult * 128 in 12 | if ch land 128 = 0 then Lwt.return value else loop value mult 13 | in 14 | loop 0 1 15 | 16 | let read_packet inch = 17 | let%lwt header_byte = Lwt_io.read_char inch in 18 | let msgid, opts = 19 | Mqtt_packet.Decoder.decode_fixed_header (Char.code header_byte) 20 | in 21 | let%lwt count = decode_length inch in 22 | 23 | let data = Bytes.create count in 24 | let%lwt () = 25 | try Lwt_io.read_into_exactly inch data 0 count 26 | with End_of_file -> Lwt.fail (Failure "could not read bytes") 27 | in 28 | let pkt = 29 | Read_buffer.make (data |> Bytes.to_string) 30 | |> Mqtt_packet.Decoder.decode_packet opts msgid 31 | in 32 | Lwt.return (opts, pkt) 33 | 34 | module Log = (val Logs_lwt.src_log (Logs.Src.create "mqtt.client")) 35 | 36 | type t = { 37 | cxn : connection; 38 | id : string; 39 | inflight : (int, unit Lwt_condition.t * Mqtt_packet.t) Hashtbl.t; 40 | mutable reader : unit Lwt.t; 41 | on_message : topic:string -> string -> unit Lwt.t; 42 | on_disconnect : t -> unit Lwt.t; 43 | on_error : t -> exn -> unit Lwt.t; 44 | should_stop_reader : unit Lwt_condition.t; 45 | } 46 | 47 | let wrap_catch client f = Lwt.catch f (client.on_error client) 48 | 49 | let default_on_error client exn = 50 | let%lwt () = 51 | Log.err (fun log -> 52 | log "[%s]: Unhandled exception: %a" client.id Fmt.exn exn) 53 | in 54 | Lwt.fail exn 55 | 56 | let default_on_message ~topic:_ _ = Lwt.return_unit 57 | let default_on_disconnect _ = Lwt.return_unit 58 | 59 | let read_packets client = 60 | let in_chan, out_chan = client.cxn in 61 | 62 | let ack_inflight id pkt = 63 | try 64 | let cond, expected_ack_pkt = Hashtbl.find client.inflight id in 65 | if pkt = expected_ack_pkt then ( 66 | Hashtbl.remove client.inflight id; 67 | Lwt_condition.signal cond (); 68 | Lwt.return_unit) 69 | else Lwt.fail (Failure "unexpected packet in ack") 70 | with Not_found -> Lwt.fail (Failure (fmt "ack for id=%d not found" id)) 71 | in 72 | 73 | let rec loop () = 74 | let%lwt (_dup, qos, _retain), packet = read_packet in_chan in 75 | let%lwt () = 76 | match packet with 77 | (* Publish with QoS 0: push *) 78 | | Publish (None, topic, payload) when qos = Atmost_once -> 79 | client.on_message ~topic payload 80 | (* Publish with QoS 0 and packet identifier: error *) 81 | | Publish (Some _id, _topic, _payload) when qos = Atmost_once -> 82 | Lwt.fail 83 | (Failure 84 | "protocol violation: publish packet with qos 0 must not have id") 85 | (* Publish with QoS 1 *) 86 | | Publish (Some id, topic, payload) when qos = Atleast_once -> 87 | (* - Push the message to the consumer queue. 88 | - Send back the PUBACK packet. *) 89 | let%lwt () = client.on_message ~topic payload in 90 | let puback = Mqtt_packet.Encoder.puback id in 91 | Lwt_io.write out_chan puback 92 | | Publish (None, _topic, _payload) when qos = Atleast_once -> 93 | Lwt.fail 94 | (Failure 95 | "protocol violation: publish packet with qos > 0 must have id") 96 | | Publish _ -> 97 | Lwt.fail (Failure "not supported publish packet (probably qos 2)") 98 | | Suback (id, _) 99 | | Unsuback id 100 | | Puback id 101 | | Pubrec id 102 | | Pubrel id 103 | | Pubcomp id -> 104 | ack_inflight id packet 105 | | Pingresp -> Lwt.return_unit 106 | | _ -> Lwt.fail (Failure "unknown packet from server") 107 | in 108 | loop () 109 | in 110 | 111 | let%lwt () = 112 | Log.debug (fun log -> log "[%s] Starting reader loop..." client.id) 113 | in 114 | Lwt.pick 115 | [ 116 | (let%lwt () = Lwt_condition.wait client.should_stop_reader in 117 | Log.info (fun log -> log "[%s] Stopping reader loop..." client.id)); 118 | loop (); 119 | ] 120 | 121 | let disconnect client = 122 | let%lwt () = 123 | Log.info (fun log -> log "[%s] Disconnecting client..." client.id) 124 | in 125 | let _, oc = client.cxn in 126 | Lwt_condition.signal client.should_stop_reader (); 127 | let%lwt () = Lwt_io.write oc (Mqtt_packet.Encoder.disconnect ()) in 128 | let%lwt () = client.on_disconnect client in 129 | Log.info (fun log -> log "[%s] Client disconnected." client.id) 130 | 131 | let shutdown client = 132 | let%lwt () = 133 | Log.debug (fun log -> log "[%s] Shutting down the connection..." client.id) 134 | in 135 | let ic, oc = client.cxn in 136 | let%lwt () = Lwt_io.flush oc in 137 | let%lwt () = Lwt_io.close ic in 138 | let%lwt () = Lwt_io.close oc in 139 | Log.debug (fun log -> log "[%s] Client connection shut down." client.id) 140 | 141 | let open_tls_connection ~client_id ~ca_file host port = 142 | try%lwt 143 | let%lwt authenticator = X509_lwt.authenticator (`Ca_file ca_file) in 144 | Tls_lwt.connect authenticator (host, port) 145 | with exn -> 146 | let%lwt () = 147 | Log.err (fun log -> 148 | log "[%s] could not get address info for %S" client_id host) 149 | in 150 | Lwt.fail exn 151 | 152 | let run_pinger ~keep_alive client = 153 | let%lwt () = Log.debug (fun log -> log "Starting ping timer...") in 154 | let _, output = client.cxn in 155 | (* 25% leeway *) 156 | let keep_alive = 0.75 *. float_of_int keep_alive in 157 | let rec loop () = 158 | let%lwt () = Lwt_unix.sleep keep_alive in 159 | let pingreq_packet = Mqtt_packet.Encoder.pingreq () in 160 | let%lwt () = Lwt_io.write output pingreq_packet in 161 | loop () 162 | in 163 | loop () 164 | 165 | exception Connection_error 166 | 167 | let open_tcp_connection ~client_id host port = 168 | let%lwt addresses = Lwt_unix.getaddrinfo host (string_of_int port) [] in 169 | match addresses with 170 | | address :: _ -> 171 | let sockaddr = Lwt_unix.(address.ai_addr) in 172 | Lwt_io.open_connection sockaddr 173 | | _ -> 174 | let%lwt () = 175 | Log.err (fun log -> 176 | log "[%s] could not get address info for %S" client_id host) 177 | in 178 | Lwt.fail Connection_error 179 | 180 | let rec create_connection ?tls_ca ~port ~client_id hosts = 181 | match hosts with 182 | | [] -> 183 | let%lwt () = 184 | Log.err (fun log -> 185 | log "[%s] Could not connect to any of the hosts (on port %d): %a" 186 | client_id port 187 | Fmt.Dump.(list string) 188 | hosts) 189 | in 190 | Lwt.fail Connection_error 191 | | host :: hosts -> ( 192 | try%lwt 193 | let%lwt () = 194 | Log.debug (fun log -> 195 | log "[%s] Connecting to `%s:%d`..." client_id host port) 196 | in 197 | let%lwt connection = 198 | match tls_ca with 199 | | Some ca_file -> open_tls_connection ~client_id ~ca_file host port 200 | | None -> open_tcp_connection ~client_id host port 201 | in 202 | let%lwt () = 203 | Log.info (fun log -> 204 | log "[%s] Connection opened on `%s:%d`." client_id host port) 205 | in 206 | Lwt.return connection 207 | with _ -> 208 | let%lwt () = 209 | Log.debug (fun log -> 210 | log "[%s] Could not connect, trying next host..." client_id) 211 | in 212 | create_connection ?tls_ca ~port ~client_id hosts) 213 | 214 | let connect ?(id = "ocaml-mqtt") ?tls_ca ?credentials ?will 215 | ?(clean_session = true) ?(keep_alive = 30) 216 | ?(on_message = default_on_message) ?(on_disconnect = default_on_disconnect) 217 | ?(on_error = default_on_error) ?(port = 1883) hosts = 218 | let flags = 219 | if clean_session || id = "" then [ Mqtt_packet.Clean_session ] else [] 220 | in 221 | let cxn_data = 222 | { Mqtt_packet.clientid = id; credentials; will; flags; keep_alive } 223 | in 224 | 225 | let%lwt ((ic, oc) as connection) = 226 | create_connection ?tls_ca ~port ~client_id:id hosts 227 | in 228 | 229 | let connect_packet = 230 | Mqtt_packet.Encoder.connect ?credentials:cxn_data.credentials 231 | ?will:cxn_data.will ~flags:cxn_data.flags ~keep_alive:cxn_data.keep_alive 232 | cxn_data.clientid 233 | in 234 | let%lwt () = Lwt_io.write oc connect_packet in 235 | let inflight = Hashtbl.create 16 in 236 | 237 | match%lwt read_packet ic with 238 | | _, Connack { connection_status = Accepted; session_present } -> 239 | let%lwt () = 240 | Log.debug (fun log -> 241 | log "[%s] Connection acknowledged (session_present=%b)" id 242 | session_present) 243 | in 244 | 245 | let client = 246 | { 247 | cxn = connection; 248 | id; 249 | inflight; 250 | reader = Lwt.return_unit; 251 | should_stop_reader = Lwt_condition.create (); 252 | on_message; 253 | on_disconnect; 254 | on_error; 255 | } 256 | in 257 | 258 | Lwt.async (fun () -> 259 | client.reader <- wrap_catch client (fun () -> read_packets client); 260 | let%lwt () = 261 | Log.debug (fun log -> log "[%s] Packet reader started." client.id) 262 | in 263 | let%lwt () = 264 | Lwt.pick [ client.reader; run_pinger ~keep_alive client ] 265 | in 266 | let%lwt () = 267 | Log.debug (fun log -> 268 | log "[%s] Packet reader stopped, shutting down..." client.id) 269 | in 270 | shutdown client); 271 | 272 | Lwt.return client 273 | | _, Connack pkt -> 274 | let conn_status = 275 | Mqtt_packet.connection_status_to_string pkt.connection_status 276 | in 277 | let%lwt () = 278 | Log.err (fun log -> log "[%s] Connection failed: %s" id conn_status) 279 | in 280 | Lwt.fail Connection_error 281 | | _ -> 282 | let%lwt () = 283 | Log.err (fun log -> 284 | log "[%s] Invalid response from broker on connection" id) 285 | in 286 | Lwt.fail Connection_error 287 | 288 | let publish ?(dup = false) ?(qos = Mqtt_core.Atleast_once) ?(retain = false) 289 | ~topic payload client = 290 | let _, oc = client.cxn in 291 | match qos with 292 | | Atmost_once -> 293 | let pkt_data = 294 | Mqtt_packet.Encoder.publish ~dup ~qos ~retain ~id:0 ~topic payload 295 | in 296 | Lwt_io.write oc pkt_data 297 | | Atleast_once -> 298 | let id = Mqtt_packet.gen_id () in 299 | let cond = Lwt_condition.create () in 300 | let expected_ack_pkt = Mqtt_packet.puback id in 301 | Hashtbl.add client.inflight id (cond, expected_ack_pkt); 302 | let pkt_data = 303 | Mqtt_packet.Encoder.publish ~dup ~qos ~retain ~id ~topic payload 304 | in 305 | let%lwt () = Lwt_io.write oc pkt_data in 306 | Lwt_condition.wait cond 307 | | Exactly_once -> 308 | let id = Mqtt_packet.gen_id () in 309 | let cond = Lwt_condition.create () in 310 | let expected_ack_pkt = Mqtt_packet.pubrec id in 311 | Hashtbl.add client.inflight id (cond, expected_ack_pkt); 312 | let pkt_data = 313 | Mqtt_packet.Encoder.publish ~dup ~qos ~retain ~id ~topic payload 314 | in 315 | let%lwt () = Lwt_io.write oc pkt_data in 316 | let%lwt () = Lwt_condition.wait cond in 317 | let expected_ack_pkt = Mqtt_packet.pubcomp id in 318 | Hashtbl.add client.inflight id (cond, expected_ack_pkt); 319 | let pkt_data = Mqtt_packet.Encoder.pubrel id in 320 | let%lwt () = Lwt_io.write oc pkt_data in 321 | Lwt_condition.wait cond 322 | 323 | let subscribe topics client = 324 | if topics = [] then raise (Invalid_argument "empty topics"); 325 | let _, oc = client.cxn in 326 | let pkt_id = Mqtt_packet.gen_id () in 327 | let subscribe_packet = Mqtt_packet.Encoder.subscribe ~id:pkt_id topics in 328 | let qos_list = List.map (fun (_, q) -> Ok q) topics in 329 | let cond = Lwt_condition.create () in 330 | Hashtbl.add client.inflight pkt_id (cond, Suback (pkt_id, qos_list)); 331 | wrap_catch client (fun () -> 332 | let%lwt () = Lwt_io.write oc subscribe_packet in 333 | let%lwt () = Lwt_condition.wait cond in 334 | let topics = List.map fst topics in 335 | Log.info (fun log -> 336 | log "[%s] Subscribed to %a." client.id Fmt.Dump.(list string) topics)) 337 | 338 | include Mqtt_core 339 | -------------------------------------------------------------------------------- /tests/subscriptions.ml: -------------------------------------------------------------------------------- 1 | open OUnit 2 | 3 | module Subscriptions : sig 4 | type 'a t 5 | 6 | val empty : 'a t 7 | val add_node : string -> 'a -> 'a t -> 'a t 8 | val remove_node : string -> 'a -> 'a t -> 'a t 9 | val query : string -> 'a t -> 'a list 10 | val length : 'a t -> int 11 | val tests : OUnit.test list 12 | end = struct 13 | type 'a t = 14 | | E (* empty *) 15 | | NV of string * (string, 'a t) Hashtbl.t (* no value *) 16 | | V of string * (string, 'a t) Hashtbl.t * 'a list (* value *) 17 | | Pound of 'a list 18 | (* wildcard # *) 19 | 20 | let split str = 21 | let strs = ref [] in 22 | let prev = ref 0 in 23 | let split_segment i c = 24 | if c = '/' then ( 25 | let newstr = String.sub str !prev (i - !prev) in 26 | prev := i + 1; 27 | (* skip slash *) 28 | strs := newstr :: !strs) 29 | in 30 | String.iteri split_segment str; 31 | (* fixup the last element *) 32 | strs := String.sub str !prev (String.length str - !prev) :: !strs; 33 | List.rev !strs 34 | 35 | let empty = E 36 | let _tbl_keys tbl = Hashtbl.fold (fun k _ acc -> k :: acc) tbl [] 37 | let tbl_vals tbl = Hashtbl.fold (fun _ v acc -> v :: acc) tbl [] 38 | 39 | let rec length = function 40 | | E -> 0 41 | | Pound _ -> 1 42 | | NV (_, t) | V (_, t, _) -> 43 | let children = tbl_vals t in 44 | 1 + List.fold_left (fun acc x -> acc + length x) 0 children 45 | 46 | let _get_vals = function E | NV _ -> [] | V (_, _, v) | Pound v -> v 47 | 48 | let find_branches tbl k : 'a t list = 49 | let f x = try [ Hashtbl.find tbl x ] with Not_found -> [] in 50 | List.map f [ k; "+"; "#" ] |> List.concat 51 | 52 | let pound_lookahead t v = 53 | try 54 | match Hashtbl.find t "#" with 55 | | Pound z -> v @ z 56 | | _ -> failwith "should never happen" 57 | with Not_found -> v 58 | 59 | let query key tree = 60 | let k_parts = split key in 61 | let rec inner tree v p : 'a list = 62 | match p with 63 | | [] -> ( 64 | match tree with 65 | | Pound z -> v @ z 66 | | V (_, t, z) -> pound_lookahead t (v @ z) 67 | | NV (_, t) -> pound_lookahead t v 68 | | E -> v) 69 | | h :: m -> ( 70 | match tree with 71 | | E -> v 72 | | Pound z -> v @ z 73 | | NV (_, t) | V (_, t, _) -> 74 | let branches = find_branches t h in 75 | let r = List.map (fun x -> inner x v m) branches in 76 | v @ List.concat r) 77 | in 78 | inner tree [] k_parts 79 | 80 | let add_node keys v t = 81 | let new_node k v i n = 82 | let h = Hashtbl.create 10 in 83 | let j = k.(i) in 84 | if j = "#" then Pound [] else if n <> 0 then NV (j, h) else V (j, h, [ v ]) 85 | in 86 | 87 | let rec add k v i n = function 88 | | E -> new_node [| "*root*" |] v i n |> add k v i n 89 | | NV (key, h) as e -> 90 | if n = 0 then V (key, h, [ v ]) 91 | else 92 | let child = 93 | try Hashtbl.find h k.(i) with Not_found -> new_node k v i n 94 | in 95 | let child = add k v (i + 1) (n - 1) child in 96 | Hashtbl.replace h k.(i) child; 97 | e 98 | | V (key, h, v1) as e -> 99 | if n = 0 then V (key, h, v :: v1) 100 | else 101 | let child = 102 | try Hashtbl.find h k.(i) with Not_found -> new_node k v i n 103 | in 104 | let child = add k v (i + 1) (n - 1) child in 105 | Hashtbl.replace h k.(i) child; 106 | e 107 | | Pound v1 -> Pound (v :: v1) 108 | in 109 | 110 | let k = split keys |> Array.of_list in 111 | add k v 0 (Array.length k) t 112 | 113 | let get_branches = function 114 | | E | Pound _ -> [] 115 | | V (_, t, _) | NV (_, t) -> tbl_vals t 116 | 117 | let rec get_value = function 118 | | E -> [] 119 | | NV (_, t) -> List.concat (List.map get_value (tbl_vals t)) 120 | | Pound v -> v 121 | | V (_, t, v) -> v @ List.concat (List.map get_value (tbl_vals t)) 122 | 123 | let find_branch k = function 124 | | E | Pound _ -> None 125 | | V (_, t, _) | NV (_, t) -> ( 126 | try Some (Hashtbl.find t k) with Not_found -> None) 127 | 128 | let remove_branch k = function 129 | | (E as e) | (Pound _ as e) -> e 130 | | (NV (_, t) as e) | (V (_, t, _) as e) -> 131 | Hashtbl.remove t k; 132 | if 0 = Hashtbl.length t then E else e 133 | 134 | let replace_branch k g = function 135 | | (E as e) | (Pound _ as e) -> e 136 | | (NV (_, t) as e) | (V (_, t, _) as e) -> 137 | Hashtbl.replace t k g; 138 | e 139 | 140 | let remove_node key value tree = 141 | let parts = split key in 142 | let remove values v = List.filter (fun x -> x <> v) values in 143 | let rec inner tree = function 144 | | [ h ] -> ( 145 | match find_branch h tree with 146 | | None -> tree 147 | | Some b -> ( 148 | match b with 149 | | E | NV _ -> tree 150 | | Pound v -> 151 | if "#" = h then 152 | let v = remove v value in 153 | if 0 = List.length v then remove_branch h tree 154 | else replace_branch h (Pound v) tree 155 | else failwith "not pound; should never happen" 156 | | V (k, t, v) -> 157 | let v = remove v value in 158 | if 0 = List.length v then 159 | if 0 = Hashtbl.length t then remove_branch k tree 160 | else replace_branch k (NV (k, t)) tree 161 | else replace_branch k (V (k, t, v)) tree)) 162 | | h :: t -> ( 163 | match find_branch h tree with 164 | | Some b -> 165 | let e = inner b t in 166 | if E = e then remove_branch h tree else replace_branch h e tree 167 | | None -> tree) 168 | | [] -> tree 169 | in 170 | inner tree parts 171 | 172 | let rec _tree_of_string tree level = 173 | let vals = match tree with E | NV _ -> [] | V (_, _, v) | Pound v -> v in 174 | let key = 175 | match tree with 176 | | E -> "*empty*" 177 | | Pound _ -> "#" 178 | | NV (k, _) -> "nv: " ^ k 179 | | V (k, _, _) -> "v: " ^ k 180 | in 181 | let branches = 182 | match tree with 183 | | E | Pound _ -> [] 184 | | NV (_, t) | V (_, t, _) -> tbl_vals t 185 | in 186 | let vals = String.concat "," vals in 187 | let vals = if "" <> vals then ": " ^ vals else "" in 188 | let _ = Printf.printf "%*d %s %s\n" level level key vals in 189 | let func x = _tree_of_string x (level + 4) in 190 | List.iter func branches 191 | 192 | let split_test _ = 193 | let printer = String.concat "," in 194 | let ae = assert_equal ~printer in 195 | let res = split "a/b/c/d" in 196 | ae [ "a"; "b"; "c"; "d" ] res; 197 | let res = split "/abc//def/ghi/" in 198 | ae [ ""; "abc"; ""; "def"; "ghi"; "" ] res 199 | 200 | let plus_test _ = 201 | let tree = 202 | add_node "helo/happy/world!" "helosadworld" empty 203 | |> add_node "helo/pretty/wurld" "helothere" 204 | |> add_node "omg/wtf" "omgwtf" 205 | |> add_node "omg/wtf/bbq" "omgwtfbbq" 206 | |> add_node "omg/srsly" "omgsrsly" 207 | |> add_node "omg/+/bbqz" "omgwildbbq" 208 | |> add_node "omg/+" "omgwildcard" 209 | |> add_node "a/+/c/+/e/+" "abcdef" 210 | |> add_node "asdfies" "QWERTIES" 211 | in 212 | let r = 213 | [ 214 | "helo/pretty/wurld"; 215 | "helo/heh/qwert/blah"; 216 | "omg/lol/bbqz"; 217 | "helo/pretty"; 218 | "omg/wtf"; 219 | "omg/wtf!"; 220 | "a/lol/c/def/e/orly"; 221 | "alol/c/def/e"; 222 | ] 223 | in 224 | let expected = 225 | [ 226 | [ "helothere" ]; 227 | []; 228 | [ "omgwildbbq" ]; 229 | []; 230 | [ "omgwildcard"; "omgwtf" ]; 231 | [ "omgwildcard" ]; 232 | [ "abcdef" ]; 233 | []; 234 | ] 235 | in 236 | let printer = String.concat "," in 237 | let cmp a b = 238 | let s = List.sort (fun x y -> String.compare x y) in 239 | s a = s b 240 | in 241 | let ae = assert_equal ~printer ~cmp in 242 | let res = List.map (fun x -> query x tree) r in 243 | List.iter2 (fun x y -> ae x y) expected res 244 | 245 | let pound_test _ = 246 | let root = 247 | add_node "a/#" "a" empty 248 | |> add_node "a/b/#" "ab" 249 | |> add_node "a/b" "plainab" 250 | |> add_node "a/b/c/#" "abc" 251 | |> add_node "a/b/c/d/#" "abcd" 252 | |> add_node "a/b/c/d/e/#" "abcde" 253 | in 254 | let r = [ "a"; "a/b"; "a/b/c"; "a/c"; "d/e" ] in 255 | let expected = 256 | [ [ "a" ]; [ "plainab"; "ab"; "a" ]; [ "abc"; "ab"; "a" ]; [ "a" ]; [] ] 257 | in 258 | let printer = String.concat "," in 259 | let res = List.map (fun x -> query x root) r in 260 | List.iter2 (fun x y -> assert_equal ~printer x y) expected res 261 | 262 | let remove_test _ = 263 | let root = 264 | add_node "a" "a" empty 265 | |> add_node "a/+" "a+" 266 | |> add_node "a/#" "a#" 267 | |> add_node "a/#" "tst" 268 | |> add_node "a" "tst" (* to double check wilds *) 269 | |> add_node "a/b" "ab" 270 | |> add_node "a/b/#" "ab#" 271 | |> add_node "a/b" "ab2" 272 | |> add_node "a/+/c" "a+c" 273 | |> add_node "a/b/c" "abc" 274 | |> add_node "a/+/c/#" "a+c#" 275 | |> add_node "a/b/c/d" "abcd" 276 | |> add_node "a/b/c/d" "abcd2" 277 | in 278 | let printer = String.concat ", " in 279 | let cmp a b = 280 | let s = List.sort (fun x y -> String.compare x y) in 281 | s a = s b 282 | in 283 | let ae = assert_equal ~printer ~cmp in 284 | 285 | (* really should modify to return the fully qualified key 286 | of the empty node 287 | *) 288 | let rec has_empty_leaves tree = 289 | let check acc x = if acc then acc else has_empty_leaves x in 290 | if 0 = List.length (get_branches tree) && 0 = List.length (get_value tree) 291 | then true 292 | else List.fold_left check false (get_branches tree) 293 | in 294 | 295 | (* sanity check the first element *) 296 | let res = query "a" root in 297 | ae [ "tst"; "a"; "tst"; "a#" ] res; 298 | let root = remove_node "a/#" "tst" root in 299 | let res = query "a" root in 300 | ae [ "tst"; "a"; "a#" ] res; 301 | let root = remove_node "a" "tst" root in 302 | let res = query "a" root in 303 | ae [ "a"; "a#" ] res; 304 | 305 | (* check that there are no empty leaves *) 306 | let res = query "a/b" root in 307 | ae [ "ab"; "ab#"; "ab2"; "a#"; "a+" ] res; 308 | (* should produce an empty leaf; the "#" dangles *) 309 | let root = remove_node "a/b/#" "ab#" root in 310 | let res = query "a/b" root in 311 | ae [ "ab"; "ab2"; "a#"; "a+" ] res; 312 | assert_equal ~msg:"empty leaves a/b/#" false (has_empty_leaves root); 313 | 314 | (* misc things, such as removing all values from a parent *) 315 | let res = query "a/b/c" root in 316 | ae [ "a+c"; "abc"; "a+c#"; "a#" ] res; 317 | let root = remove_node "a/b/c" "abc" root in 318 | let res = query "a/b/c" root in 319 | ae [ "a+c"; "a+c#"; "a#" ] res; 320 | let root = remove_node "a/+/c" "a+c" root in 321 | let res = query "a/b/c" root in 322 | ae [ "a+c#"; "a#" ] res; 323 | let root = remove_node "a/+/c/#" "a+c#" root in 324 | let res = query "a/b/c" root in 325 | ae [ "a#" ] res; 326 | assert_equal ~msg:"empty leaves; none" false (has_empty_leaves root); 327 | 328 | (* remove a sibling value from same key *) 329 | let res = query "a/b/c/d" root in 330 | ae [ "a#"; "abcd"; "abcd2" ] res; 331 | let root = remove_node "a/b/c/d" "abcd" root in 332 | let res = query "a/b/c/d" root in 333 | ae [ "a#"; "abcd2" ] res; 334 | 335 | (* remove nonexistent value from valid key *) 336 | let root = remove_node "a/b/c/d" "nothere" root in 337 | let res = query "a/b/c/d" root in 338 | ae [ "a#"; "abcd2" ] res; 339 | 340 | (* remove nonexistent value from invalid key *) 341 | let root = remove_node "a/c/d/" "rlynothere" root in 342 | let res = query "a/c/d" root in 343 | ae [ "a#" ] res; 344 | assert_equal ~msg:"empty leaves; final" false (has_empty_leaves root); 345 | () 346 | 347 | let tests = 348 | [ 349 | "split_test" >:: split_test; 350 | "plus test" >:: plus_test; 351 | "pound_test" >:: pound_test; 352 | "remove_test" >:: remove_test; 353 | ] 354 | end 355 | -------------------------------------------------------------------------------- /lib/mqtt_client/Mqtt_packet.ml: -------------------------------------------------------------------------------- 1 | module BE = EndianBytes.BigEndian 2 | open Mqtt_core 3 | 4 | let _msgid = ref 0 5 | 6 | let gen_id () = 7 | let () = incr _msgid in 8 | if !_msgid >= 0xFFFF then _msgid := 1; 9 | !_msgid 10 | 11 | let int16be n = 12 | let s = Bytes.create 2 in 13 | BE.set_int16 s 0 n; 14 | s 15 | 16 | let int8be n = 17 | let s = Bytes.create 1 in 18 | BE.set_int8 s 0 n; 19 | s 20 | 21 | type messages = 22 | | Connect_pkt 23 | | Connack_pkt 24 | | Publish_pkt 25 | | Puback_pkt 26 | | Pubrec_pkt 27 | | Pubrel_pkt 28 | | Pubcomp_pkt 29 | | Subscribe_pkt 30 | | Suback_pkt 31 | | Unsubscribe_pkt 32 | | Unsuback_pkt 33 | | Pingreq_pkt 34 | | Pingresp_pkt 35 | | Disconnect_pkt 36 | 37 | type cxn_flags = Will_retain | Will_qos of qos | Clean_session 38 | 39 | type cxn_data = { 40 | clientid : string; 41 | credentials : credentials option; 42 | will : (string * string) option; 43 | flags : cxn_flags list; 44 | keep_alive : int; 45 | } 46 | 47 | type client_options = { ping_timeout : float; cxn_data : cxn_data } 48 | 49 | type connection_status = 50 | | Accepted 51 | | Unacceptable_protocol_version 52 | | Identifier_rejected 53 | | Server_unavailable 54 | | Bad_username_or_password 55 | | Not_authorized 56 | 57 | let connection_status_to_string = function 58 | | Accepted -> "Accepted" 59 | | Unacceptable_protocol_version -> "Unacceptable_protocol_version" 60 | | Identifier_rejected -> "Identifier_rejected" 61 | | Server_unavailable -> "Server_unavailable" 62 | | Bad_username_or_password -> "Bad_username_or_password" 63 | | Not_authorized -> "Not_authorized" 64 | 65 | let connection_status_to_int = function 66 | | Accepted -> 0 67 | | Unacceptable_protocol_version -> 1 68 | | Identifier_rejected -> 2 69 | | Server_unavailable -> 3 70 | | Bad_username_or_password -> 4 71 | | Not_authorized -> 5 72 | 73 | let connection_status_of_int = function 74 | | 0 -> Accepted 75 | | 1 -> Unacceptable_protocol_version 76 | | 2 -> Identifier_rejected 77 | | 3 -> Server_unavailable 78 | | 4 -> Bad_username_or_password 79 | | 5 -> Not_authorized 80 | | _ -> raise (Invalid_argument "Invalid connection status code") 81 | 82 | type t = 83 | | Connect of cxn_data 84 | | Connack of { session_present : bool; connection_status : connection_status } 85 | | Subscribe of (int * (string * qos) list) 86 | | Suback of (int * (qos, unit) result list) 87 | | Unsubscribe of (int * string list) 88 | | Unsuback of int 89 | | Publish of (int option * string * string) 90 | | Puback of int 91 | | Pubrec of int 92 | | Pubrel of int 93 | | Pubcomp of int 94 | | Pingreq 95 | | Pingresp 96 | | Disconnect 97 | 98 | type options = bool * qos * bool 99 | 100 | let bits_of_message = function 101 | | Connect_pkt -> 1 102 | | Connack_pkt -> 2 103 | | Publish_pkt -> 3 104 | | Puback_pkt -> 4 105 | | Pubrec_pkt -> 5 106 | | Pubrel_pkt -> 6 107 | | Pubcomp_pkt -> 7 108 | | Subscribe_pkt -> 8 109 | | Suback_pkt -> 9 110 | | Unsubscribe_pkt -> 10 111 | | Unsuback_pkt -> 11 112 | | Pingreq_pkt -> 12 113 | | Pingresp_pkt -> 13 114 | | Disconnect_pkt -> 14 115 | 116 | let message_of_bits = function 117 | | 1 -> Connect_pkt 118 | | 2 -> Connack_pkt 119 | | 3 -> Publish_pkt 120 | | 4 -> Puback_pkt 121 | | 5 -> Pubrec_pkt 122 | | 6 -> Pubrel_pkt 123 | | 7 -> Pubcomp_pkt 124 | | 8 -> Subscribe_pkt 125 | | 9 -> Suback_pkt 126 | | 10 -> Unsubscribe_pkt 127 | | 11 -> Unsuback_pkt 128 | | 12 -> Pingreq_pkt 129 | | 13 -> Pingresp_pkt 130 | | 14 -> Disconnect_pkt 131 | | _ -> raise (Invalid_argument "invalid bits in message") 132 | 133 | let bits_of_qos = function 134 | | Atmost_once -> 0 135 | | Atleast_once -> 1 136 | | Exactly_once -> 2 137 | 138 | let qos_of_bits = function 139 | | 0 -> Atmost_once 140 | | 1 -> Atleast_once 141 | | 2 -> Exactly_once 142 | | b -> raise (Invalid_argument ("invalid qos number: " ^ string_of_int b)) 143 | 144 | let suback_qos_of_bits = function 0x80 -> Error () | b -> Ok (qos_of_bits b) 145 | let bit_of_bool = function true -> 1 | false -> 0 146 | 147 | let bool_of_bit = function 148 | | 1 -> true 149 | | 0 -> false 150 | | n -> 151 | raise 152 | (Invalid_argument ("expected zero or one, but got " ^ string_of_int n)) 153 | 154 | let trunc str = 155 | (* truncate leading zeroes *) 156 | let len = String.length str in 157 | let rec loop count = 158 | if count >= len || str.[count] <> '\000' then count else loop (count + 1) 159 | in 160 | let leading = loop 0 in 161 | if leading = len then "\000" else String.sub str leading (len - leading) 162 | 163 | let addlen s = 164 | let len = String.length s in 165 | if len > 0xFFFF then raise (Invalid_argument "string too long"); 166 | Bytes.to_string (int16be len) ^ s 167 | 168 | let opt_with s n = function Some a -> s a | None -> n 169 | let puback id = Puback id 170 | let pubrec id = Pubrec id 171 | let pubcomp id = Pubcomp id 172 | 173 | module Encoder = struct 174 | let encode_length len = 175 | let rec loop ll digits = 176 | if ll <= 0 then digits 177 | else 178 | let incr = Int32.logor (Int32.of_int 0x80) in 179 | let shft = Int32.logor (Int32.shift_left digits 8) in 180 | let getdig x dig = if x > 0 then incr dig else dig in 181 | let quotient = ll / 128 in 182 | let digit = getdig quotient (Int32.of_int (ll mod 128)) in 183 | let digits = shft digit in 184 | loop quotient digits 185 | in 186 | loop len 0l 187 | 188 | let fixed_header typ ?(flags = 0) body_len = 189 | let msgid = bits_of_message typ lsl 4 in 190 | let hdr = Bytes.create 1 in 191 | let len = Bytes.create 4 in 192 | BE.set_int8 hdr 0 (msgid + flags); 193 | BE.set_int32 len 0 (encode_length body_len); 194 | let len = trunc (Bytes.to_string len) in 195 | Bytes.to_string hdr ^ len 196 | 197 | let unsubscribe ~id topics = 198 | let accum acc i = acc + 2 + String.length i in 199 | let tl = List.fold_left accum 2 topics in 200 | (* +2 for msgid *) 201 | let buf = Buffer.create (tl + 5) in 202 | (* ~5 for fixed header *) 203 | let addtopic t = addlen t |> Buffer.add_string buf in 204 | let msgid = int16be id |> Bytes.to_string in 205 | let hdr = fixed_header Unsubscribe_pkt ~flags:2 tl in 206 | Buffer.add_string buf hdr; 207 | Buffer.add_string buf msgid; 208 | List.iter addtopic topics; 209 | Buffer.contents buf 210 | 211 | let unsuback id = 212 | let msgid = int16be id |> Bytes.to_string in 213 | let hdr = fixed_header Unsuback_pkt 2 in 214 | hdr ^ msgid 215 | 216 | let simple_pkt typ = fixed_header typ 0 217 | let pingreq () = simple_pkt Pingreq_pkt 218 | let pingresp () = simple_pkt Pingresp_pkt 219 | 220 | let pubpkt ?flags typ id = 221 | let hdr = fixed_header ?flags typ 2 in 222 | let msgid = int16be id |> Bytes.to_string in 223 | let buf = Buffer.create 4 in 224 | Buffer.add_string buf hdr; 225 | Buffer.add_string buf msgid; 226 | Buffer.contents buf 227 | 228 | let pubrec = pubpkt Pubrec_pkt 229 | let pubrel = pubpkt ~flags:2 Pubrel_pkt 230 | let pubcomp = pubpkt Pubcomp_pkt 231 | 232 | let suback id qoses = 233 | let paylen = List.length qoses + 2 in 234 | let buf = Buffer.create (paylen + 5) in 235 | let msgid = int16be id |> Bytes.to_string in 236 | let q2i q = bits_of_qos q |> int8be |> Bytes.to_string in 237 | let blit q = Buffer.add_string buf (q2i q) in 238 | let hdr = fixed_header Suback_pkt paylen in 239 | Buffer.add_string buf hdr; 240 | Buffer.add_string buf msgid; 241 | List.iter blit qoses; 242 | Buffer.contents buf 243 | 244 | let puback = pubpkt Puback_pkt 245 | let disconnect () = simple_pkt Disconnect_pkt 246 | 247 | let subscribe ~id topics = 248 | let accum acc (i, _) = acc + 3 + String.length i in 249 | let tl = List.fold_left accum 0 topics in 250 | let tl = tl + 2 in 251 | (* add msgid to total len *) 252 | let buf = Buffer.create (tl + 5) in 253 | (* ~5 for fixed header *) 254 | let addtopic (t, q) = 255 | Buffer.add_string buf (addlen t); 256 | Buffer.add_string buf (Bytes.to_string @@ int8be (bits_of_qos q)) 257 | in 258 | let msgid = int16be id |> Bytes.to_string in 259 | let hdr = fixed_header Subscribe_pkt ~flags:2 tl in 260 | Buffer.add_string buf hdr; 261 | Buffer.add_string buf msgid; 262 | List.iter addtopic topics; 263 | Buffer.contents buf 264 | 265 | let publish ~dup ~qos ~retain ~id ~topic payload = 266 | let id_data = 267 | if qos = Atleast_once || qos = Exactly_once then 268 | Bytes.to_string (int16be id) 269 | else "" 270 | in 271 | let dup = if qos = Atmost_once then false else dup in 272 | let topic = addlen topic in 273 | let sl = String.length in 274 | let tl = sl topic + sl payload + sl id_data in 275 | let buf = Buffer.create (tl + 5) in 276 | let flags = 277 | let dup = bit_of_bool dup lsl 3 in 278 | let qos = bits_of_qos qos lsl 1 in 279 | let retain = bit_of_bool retain in 280 | dup + qos + retain 281 | in 282 | let hdr = fixed_header Publish_pkt ~flags tl in 283 | Buffer.add_string buf hdr; 284 | Buffer.add_string buf topic; 285 | Buffer.add_string buf id_data; 286 | Buffer.add_string buf payload; 287 | Buffer.contents buf 288 | 289 | let connect_payload ?credentials ?will ?(flags = []) ?(keep_alive = 10) id = 290 | let name = addlen "MQTT" in 291 | let version = "\004" in 292 | if keep_alive > 0xFFFF then raise (Invalid_argument "keep_alive too large"); 293 | let addhdr2 flag term (flags, hdr) = 294 | match term with 295 | | None -> (flags, hdr) 296 | | Some (a, b) -> (flags lor flag, hdr ^ addlen a ^ addlen b) 297 | in 298 | let adduserpass term (flags, hdr) = 299 | match term with 300 | | None -> (flags, hdr) 301 | | Some (Username s) -> (flags lor 0x80, hdr ^ addlen s) 302 | | Some (Credentials (u, p)) -> addhdr2 0xC0 (Some (u, p)) (flags, hdr) 303 | in 304 | let flag_nbr = function 305 | | Clean_session -> 0x02 306 | | Will_qos qos -> bits_of_qos qos lsl 3 307 | | Will_retain -> 0x20 308 | in 309 | let accum a acc = acc lor flag_nbr a in 310 | let flags, pay = 311 | (List.fold_right accum flags 0, addlen id) 312 | |> addhdr2 0x04 will 313 | |> adduserpass credentials 314 | in 315 | let tbuf = int16be keep_alive in 316 | let fbuf = Bytes.create 1 in 317 | BE.set_int8 fbuf 0 flags; 318 | let accum acc a = acc + String.length a in 319 | let fields = 320 | [ name; version; Bytes.to_string fbuf; Bytes.to_string tbuf; pay ] 321 | in 322 | let lens = List.fold_left accum 0 fields in 323 | let buf = Buffer.create lens in 324 | List.iter (Buffer.add_string buf) fields; 325 | Buffer.contents buf 326 | 327 | let connect ?credentials ?will ?flags ?keep_alive id = 328 | let cxn_pay = connect_payload ?credentials ?will ?flags ?keep_alive id in 329 | let hdr = fixed_header Connect_pkt (String.length cxn_pay) in 330 | hdr ^ cxn_pay 331 | 332 | let connect_data d = 333 | let clientid = d.clientid in 334 | let credentials = d.credentials in 335 | let will = d.will in 336 | let flags = d.flags in 337 | let keep_alive = d.keep_alive in 338 | connect_payload ?credentials ?will ~flags ~keep_alive clientid 339 | 340 | let connack ~session_present status = 341 | let fixed_header = fixed_header Connack_pkt 2 in 342 | let flags = Bytes.to_string (int8be (bit_of_bool session_present)) in 343 | let connection_status = 344 | Bytes.to_string (int8be (connection_status_to_int status)) 345 | in 346 | let variable_header = flags ^ connection_status in 347 | fixed_header ^ variable_header 348 | end 349 | 350 | module Decoder = struct 351 | let decode_connect rb = 352 | let lead = Read_buffer.read rb 9 in 353 | if "\000\004MQTT\004" <> lead then 354 | raise (Invalid_argument "invalid MQTT or version"); 355 | let hdr = Read_buffer.read_uint8 rb in 356 | let keep_alive = Read_buffer.read_uint16 rb in 357 | let has_username = 0 <> hdr land 0x80 in 358 | let has_password = 0 <> hdr land 0xC0 in 359 | let will_flag = bool_of_bit ((hdr land 0x04) lsr 2) in 360 | let will_retain = will_flag && 0 <> hdr land 0x20 in 361 | let will_qos = 362 | if will_flag then Some (qos_of_bits ((hdr land 0x18) lsr 3)) else None 363 | in 364 | let clean_session = bool_of_bit ((hdr land 0x02) lsr 1) in 365 | let rs = Read_buffer.read_string in 366 | let clientid = rs rb in 367 | let will = 368 | if will_flag then 369 | let t = rs rb in 370 | let m = rs rb in 371 | Some (t, m) 372 | else None 373 | in 374 | let credentials = 375 | if has_password then 376 | let u = rs rb in 377 | let p = rs rb in 378 | Some (Credentials (u, p)) 379 | else if has_username then Some (Username (rs rb)) 380 | else None 381 | in 382 | let flags = if clean_session then [ Clean_session ] else [] in 383 | let flags = opt_with (fun qos -> Will_qos qos :: flags) flags will_qos in 384 | let flags = if will_retain then Will_retain :: flags else flags in 385 | Connect { clientid; credentials; will; flags; keep_alive } 386 | 387 | let decode_connack rb = 388 | let flags = Read_buffer.read_uint8 rb in 389 | let session_present = bool_of_bit flags in 390 | let connection_status = 391 | connection_status_of_int (Read_buffer.read_uint8 rb) 392 | in 393 | Connack { session_present; connection_status } 394 | 395 | let decode_publish (_, qos, _) rb = 396 | let topic = Read_buffer.read_string rb in 397 | let msgid = 398 | if qos = Atleast_once || qos = Exactly_once then 399 | Some (Read_buffer.read_uint16 rb) 400 | else None 401 | in 402 | let payload = Read_buffer.len rb |> Read_buffer.read rb in 403 | Publish (msgid, topic, payload) 404 | 405 | let decode_puback rb = Puback (Read_buffer.read_uint16 rb) 406 | let decode_pubrec rb = Pubrec (Read_buffer.read_uint16 rb) 407 | let decode_pubrel rb = Pubrel (Read_buffer.read_uint16 rb) 408 | let decode_pubcomp rb = Pubcomp (Read_buffer.read_uint16 rb) 409 | 410 | let decode_subscribe rb = 411 | let id = Read_buffer.read_uint16 rb in 412 | let get_topic rb = 413 | let topic = Read_buffer.read_string rb in 414 | let qos = Read_buffer.read_uint8 rb |> qos_of_bits in 415 | (topic, qos) 416 | in 417 | let topics = Read_buffer.read_all rb get_topic in 418 | Subscribe (id, topics) 419 | 420 | let decode_suback rb = 421 | let id = Read_buffer.read_uint16 rb in 422 | let get_qos rb = Read_buffer.read_uint8 rb |> suback_qos_of_bits in 423 | let qoses = Read_buffer.read_all rb get_qos in 424 | Suback (id, List.rev qoses) 425 | 426 | let decode_unsub rb = 427 | let id = Read_buffer.read_uint16 rb in 428 | let topics = Read_buffer.read_all rb Read_buffer.read_string in 429 | Unsubscribe (id, topics) 430 | 431 | let decode_unsuback rb = Unsuback (Read_buffer.read_uint16 rb) 432 | let decode_pingreq _rb = Pingreq 433 | let decode_pingresp _rb = Pingresp 434 | let decode_disconnect _rb = Disconnect 435 | 436 | let decode_packet opts = function 437 | | Connect_pkt -> decode_connect 438 | | Connack_pkt -> decode_connack 439 | | Publish_pkt -> decode_publish opts 440 | | Puback_pkt -> decode_puback 441 | | Pubrec_pkt -> decode_pubrec 442 | | Pubrel_pkt -> decode_pubrel 443 | | Pubcomp_pkt -> decode_pubcomp 444 | | Subscribe_pkt -> decode_subscribe 445 | | Suback_pkt -> decode_suback 446 | | Unsubscribe_pkt -> decode_unsub 447 | | Unsuback_pkt -> decode_unsuback 448 | | Pingreq_pkt -> decode_pingreq 449 | | Pingresp_pkt -> decode_pingresp 450 | | Disconnect_pkt -> decode_disconnect 451 | 452 | let decode_fixed_header byte : messages * options = 453 | let typ = (byte land 0xF0) lsr 4 in 454 | let dup = (byte land 0x08) lsr 3 in 455 | let qos = (byte land 0x06) lsr 1 in 456 | let retain = byte land 0x01 in 457 | let typ = message_of_bits typ in 458 | let dup = bool_of_bit dup in 459 | let qos = qos_of_bits qos in 460 | let retain = bool_of_bit retain in 461 | (typ, (dup, qos, retain)) 462 | end 463 | -------------------------------------------------------------------------------- /etc/odoc.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* Copyright (c) 2016 The odoc contributors. All rights reserved. 3 | Distributed under the ISC license, see terms at the end of the file. 4 | %%NAME%% %%VERSION%% */ 5 | 6 | /* Fonts */ 7 | @import url('https://fonts.googleapis.com/css?family=Fira+Mono:400,500'); 8 | @import url('https://fonts.googleapis.com/css?family=Noticia+Text:400,400i,700'); 9 | @import url('https://fonts.googleapis.com/css?family=Fira+Sans:400,400i,500,500i,600,600i,700,700i'); 10 | 11 | 12 | /* Reset a few things. */ 13 | 14 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 15 | margin: 0; 16 | padding: 0; 17 | border: 0; 18 | font-size: inherit; 19 | font: inherit; 20 | line-height: inherit; 21 | vertical-align: baseline; 22 | text-align: inherit; 23 | color: inherit; 24 | background: transparent; 25 | } 26 | 27 | table { 28 | border-collapse: collapse; 29 | border-spacing: 0; 30 | } 31 | 32 | *, *:before, *:after { 33 | box-sizing: border-box; 34 | } 35 | 36 | html { 37 | font-size: 15px; 38 | } 39 | 40 | body { 41 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; 42 | text-align: left; 43 | color: #333; 44 | } 45 | 46 | .content { 47 | max-width: 90ex; 48 | margin-left: calc(10vw + 20ex); 49 | margin-right: 4ex; 50 | margin-top: 20px; 51 | margin-bottom: 50px; 52 | line-height: 1.5; 53 | } 54 | 55 | .content>header { 56 | margin-bottom: 30px; 57 | } 58 | 59 | /* Basic markup elements */ 60 | 61 | b, strong { 62 | font-weight: 500; 63 | } 64 | 65 | i, em { 66 | font-style: italic; 67 | } 68 | 69 | sup { 70 | vertical-align: super; 71 | } 72 | 73 | sub { 74 | vertical-align: sub; 75 | } 76 | 77 | sup, sub { 78 | font-size: 12px; 79 | line-height: 0; 80 | margin-left: 0.2ex; 81 | } 82 | 83 | pre { 84 | margin-top: 0.8em; 85 | margin-bottom: 1.2em; 86 | } 87 | 88 | p, ul, ol { 89 | margin-top: 0.5em; 90 | margin-bottom: 1em; 91 | } 92 | ul, ol { 93 | list-style-position: outside 94 | } 95 | 96 | ul>li { 97 | margin-left: 22px; 98 | } 99 | 100 | ol>li { 101 | margin-left: 27.2px; 102 | } 103 | 104 | li>*:first-child { 105 | margin-top: 0 106 | } 107 | 108 | /* Text alignements, this should be forbidden. */ 109 | 110 | .left { 111 | text-align: left; 112 | } 113 | 114 | .right { 115 | text-align: right; 116 | } 117 | 118 | .center { 119 | text-align: center; 120 | } 121 | 122 | /* Links and anchors */ 123 | 124 | a { 125 | text-decoration: none; 126 | color: #2C5CBD; 127 | } 128 | 129 | a:hover { 130 | text-decoration: underline; 131 | } 132 | 133 | /* Linked highlight */ 134 | *:target { 135 | background-color: rgba(187,239,253,0.3) !important; 136 | box-shadow: 0 0px 0 1px rgba(187,239,253,0.8) !important; 137 | border-radius: 1px; 138 | } 139 | 140 | *:hover>a.anchor { 141 | visibility: visible; 142 | } 143 | 144 | a.anchor:before { 145 | content: "#" 146 | } 147 | 148 | a.anchor:hover { 149 | box-shadow: none; 150 | text-decoration: none; 151 | color: #555; 152 | } 153 | 154 | a.anchor { 155 | visibility: hidden; 156 | position: absolute; 157 | /* top: 0px; */ 158 | /* margin-left: -3ex; */ 159 | margin-left: -1.3em; 160 | font-weight: normal; 161 | font-style: normal; 162 | padding-right: 0.4em; 163 | padding-left: 0.4em; 164 | /* To remain selectable */ 165 | color: #d5d5d5; 166 | } 167 | 168 | .spec > a.anchor { 169 | margin-left: -2.3em; 170 | padding-right: 0.9em; 171 | } 172 | 173 | .xref-unresolved { 174 | color: #2C5CBD; 175 | } 176 | .xref-unresolved:hover { 177 | box-shadow: 0 1px 0 0 #CC6666; 178 | } 179 | 180 | /* Section and document divisions. 181 | Until at least 4.03 many of the modules of the stdlib start at .h7, 182 | we restart the sequence there like h2 */ 183 | 184 | h1, h2, h3, h4, h5, h6, .h7, .h8, .h9, .h10 { 185 | font-family: "Fira Sans", Helvetica, Arial, sans-serif; 186 | font-weight: 400; 187 | margin: 0.5em 0 0.5em 0; 188 | padding-top: 0.1em; 189 | line-height: 1.2; 190 | overflow-wrap: break-word; 191 | } 192 | 193 | h1 { 194 | font-weight: 500; 195 | font-size: 2.441em; 196 | margin-top: 1.214em; 197 | } 198 | 199 | h1 { 200 | font-weight: 500; 201 | font-size: 1.953em; 202 | box-shadow: 0 1px 0 0 #ddd; 203 | } 204 | 205 | h2 { 206 | font-size: 1.563em; 207 | } 208 | 209 | h3 { 210 | font-size: 1.25em; 211 | } 212 | 213 | small, .font_small { 214 | font-size: 0.8em; 215 | } 216 | 217 | h1 code, h1 tt { 218 | font-size: inherit; 219 | font-weight: inherit; 220 | } 221 | 222 | h2 code, h2 tt { 223 | font-size: inherit; 224 | font-weight: inherit; 225 | } 226 | 227 | h3 code, h3 tt { 228 | font-size: inherit; 229 | font-weight: inherit; 230 | } 231 | 232 | h3 code, h3 tt { 233 | font-size: inherit; 234 | font-weight: inherit; 235 | } 236 | 237 | h4 { 238 | font-size: 1.12em; 239 | } 240 | 241 | 242 | /* Preformatted and code */ 243 | 244 | tt, code, pre { 245 | font-family: "Fira Mono", courier; 246 | font-weight: 400; 247 | } 248 | 249 | pre { 250 | padding: 0.1em; 251 | border: 1px solid #eee; 252 | border-radius: 5px; 253 | overflow-x: auto; 254 | } 255 | 256 | p code, li code { 257 | background-color: #f6f8fa; 258 | color: #0d2b3e; 259 | border-radius: 3px; 260 | padding: 0 0.3ex; 261 | } 262 | 263 | p a > code { 264 | color: #2C5CBD; 265 | } 266 | 267 | /* Code blocks (e.g. Examples) */ 268 | 269 | pre code { 270 | font-size: 0.893rem; 271 | } 272 | 273 | /* Code lexemes */ 274 | 275 | .keyword { 276 | font-weight: 500; 277 | } 278 | 279 | /* Module member specification */ 280 | 281 | .spec:not(.include), .spec.include details summary { 282 | background-color: #f6f8fa; 283 | border-radius: 3px; 284 | border-left: 4px solid #5c9cf5; 285 | border-right: 5px solid transparent; 286 | padding: 0.35em 0.5em; 287 | } 288 | 289 | .spec.include details summary:hover { 290 | background-color: #ebeff2; 291 | } 292 | 293 | dl, div.spec, .doc, aside { 294 | margin-bottom: 20px; 295 | } 296 | 297 | dl > dd { 298 | padding: 0.5em; 299 | } 300 | 301 | dd> :first-child { 302 | margin-top: 0; 303 | } 304 | 305 | dl:last-child, dd> :last-child, aside:last-child, article:last-child { 306 | margin-bottom: 0; 307 | } 308 | 309 | dt+dt { 310 | margin-top: 15px; 311 | } 312 | 313 | section+section, section > header + dl { 314 | margin-top: 25px; 315 | } 316 | 317 | .spec.type .variant { 318 | margin-left: 2ch; 319 | } 320 | .spec.type .variant p { 321 | margin: 0; 322 | font-style: italic; 323 | } 324 | .spec.type .record { 325 | margin-left: 2ch; 326 | } 327 | .spec.type .record p { 328 | margin: 0; 329 | font-style: italic; 330 | } 331 | .spec.value code { 332 | display: inline-block; 333 | } 334 | .spec.value code .label { 335 | font-style: italic; 336 | font-weight: 500; 337 | } 338 | 339 | .arg:nth-child(2):nth-last-child(n + 5), 340 | .arg:nth-child(2):nth-last-child(n + 5) ~ .arg, 341 | .arg:nth-child(2):nth-last-child(n + 5) ~ span:last-child { 342 | display: block; 343 | margin-left: 1.2em; 344 | } 345 | 346 | div.def { 347 | margin-top: 0; 348 | text-indent: -2ex; 349 | padding-left: 2ex; 350 | } 351 | 352 | div.def+div.doc { 353 | margin-left: 1ex; 354 | margin-top: 2.5px 355 | } 356 | 357 | div.doc>*:first-child { 358 | margin-top: 0; 359 | } 360 | 361 | /* The elements other than heading should be wrapped in