Canonical repository: https://gitlab.com/eval/otarta
An MQTT-library for ClojureScript.
NOTE: this is pre-alpha software with an API that will change (see the CHANGELOG for breaking changes)
Leiningen:
[eval/otarta "0.3.1"]
Deps:
eval/otarta {:mvn/version "0.3.1"}
The following code assumes:
- being in a browser (ie
js/WebSockets
exists. For Node.js see below.) - a websocket-enabled MQTT-broker on
localhost:9001
(eg viadocker run --rm -ti -p 9001:9001 toke/mosquitto
)
(ns example.core
(:require-macros [cljs.core.async.macros :refer [go go-loop]])
(:require [cljs.core.async :as a :refer [<!]]
[otarta.core :as mqtt]))
(defonce client (mqtt/client "ws://localhost:9001/mqtt#weather-sensor"))
(defn subscription-handler [ch]
(go-loop []
(when-let [m (<! ch)]
;; example m: {:topic "temperature/current" :payload "12.1" :retain? false :qos 0}
(prn "Received:" m)
(recur))))
(go
(let [[err {sub-ch :ch}] (<! (mqtt/subscribe client "temperature/#"))]
(if err
(println "Failed to subscribe:" err)
(do
(println "Subscribed!")
(subscription-handler sub-ch))))
(mqtt/publish client "temperature/current" "12.1"))
The first argument (the broker-url
) should be of the form ws(s):://(user:pass@)host.org:1234/path(#some/root/topic)
.
The fragment contains the root-topic
and indicates the topic relative to which the client publishes and subscribes. This allows for pointing the client to a specific subtree of the broker (eg where it has read/write-permissions, or where it makes sense given the stage: ws://some-broker/mqtt#acceptance/sensor1
).
When you write a client that receives its broker-url from outside (ie as an environment variable), it might lack a root-topic. In order to prevent unwanted effects in that case (eg the client subscribing to "#" essentially subscribing to the root of the broker) you can provide a default-root-topic
:
(mqtt/client config.broker-url {:default-root-topic "weather-sensor"})
The client will then treat the broker-url ws://localhost:9001/mqtt
like ws://localhost:9001/mqtt#weather-sensor
.
When config.broker-url
does contain a root-topic
, the default-root-topic
is ignored (but gives a nice hint as to what the root-topic
could look like, eg acceptance/weather-sensor
).
Messages have the following shape:
{:topic "temperature/current" ;; topic relative to `root-topic`
:payload "12.1" ;; formatted payload
:retain? false ;; whether this message was from the broker's store or 'real-time' from publisher
:qos 0} ;; quality of service (0: at most once, 1: at least once, 2: exactly once)
NOTE: retain?
is not so much a property of the sent message, but tells you when you received it: typically you receive messages with {:retain? true}
directly after subscribing. But when you're subscribed and a message is published with the retain-flag set, the message you'll received has {:retain? false}
. This as you received it 'first hand' from the publisher, not from the broker's store.
When publishing or subscribing you can specify a format. Available formats are: string
(default), raw
, json
, edn
and transit
:
(go
(let [[err {sub-ch :ch}] (<! (mqtt/subscribe client "temperature/#" {:format :transit}))]
(if err
(println "Failed to subscribe:" err)
(do
(prn (<! sub-ch))))) ;; prints: {:created-at #inst "2018-09-27T13:13:21.932-00:00", :value 12.1}
(mqtt/publish client "temperature/current" {:created-at (js/Date.) :value 12.1} {:format :transit}))
Incoming messages with a payload that is not readable, won't appear on the subscription-channel.
Similarly, when formatting fails when publishing, you'll receive an error:
(let [[err _] (<! (mqtt/publish client "foo" #"not transit!" {:format :transit}))]
(when err
(println err)))
You can provide your own format:
(ns example.core
(:require [otarta.format :as mqtt-fmt]))
(defn extract-temperature []
...)
;; this format piggybacks on the string-format
;; after which extract-temperature will get the relevant data.
;; Otarta will catch any exceptions that occur when reading/writing.
(def custom-format
(reify mqtt-fmt/PayloadFormat
(-read [_fmt buff]
(->> buff (mqtt-fmt/-read mqtt-fmt/string) extract-temperature))
(-write [_fmt v]
(->> v (mqtt-fmt/-write mqtt-fmt/string)))))
You should provide a W3C compatible websocket when running via Node.js.
I've had good experience with this websocket-library (>= v1.0.28).
With the library included in your project (see https://clojurescript.org/guides/webpack for details), the following will initialize js/WebSocket
:
(ns example.core
(:require [websocket]))
(set! js/WebSocket (.-w3cwebsocket websocket))
- only QoS 0
- only clean-session
- no reconnect built-in
- untested for large payloads (ie more than a couple of KB)
Via cljs-test-runner:
Via cljs-test-runner:
# once
$ clojure -Atest
# watching
$ clojure -Atest-watch
# specific tests
(deftest ^{:focus true} only-this-test ...)
$ clojure -Atest-watch -i :focus
# more options:
$ clojure -Atest-watch --help
# start figwheel
$ make figwheel
# wait till compiled and then from other shell:
$ node target/app.js
# then from emacs:
# M-x cider-connect with host: localhost and port: 7890
# from repl:
user> (figwheel/cljs-repl)
;; prompt changes to:
cljs.user>
;; to quickly see what otarta can do:
;; - evaluate the otarta.main namespace
;; - then eval the comment-section of otarta.main line by line
See CIDER docs what you can do.
- (ensure no CLJ_CONFIG and MAVEN_OPTS env variables are set - this to target ~/.m2)
- ensure dependencies in pom.xml up to date
- clj -Spom
- ensure pom.xml with new version
- cp pom.xml{.template,}
- gsed -i 's/$RELEASE_VERSION/1.2.3/' pom.xml
- make mvn-install
- testdrive locally
- create (pre-)tag
- push to CI
Copyright (c) 2018 Gert Goet, ThinkCreate
Copyright (c) 2018 Alliander N.V.
See LICENSE.
For licenses of third-party software that this software uses, see LICENSE-3RD-PARTY.