October 6, 2019
By: Kevin

clojureScript的WebBle初步尝试

  1. web ble with cljs
    1. JS版本的设备扫描
    2. cljs版本的设备扫描
    3. js的Promise链
    4. cljs的Promie实现
    5. cljs的WebBle链
  2. 参考

web ble with cljs

本文不对webble本身做分析,仅仅进行了使用cljs调用webble接口的尝试,浏览器环境仅支持chrome。

JS版本的设备扫描

JS里promise用cljs实现,以web蓝牙为例:

以下代码来自webble官方demo

下面的代码可以扫描周围任意以字母或者数字开头的BLE服务。

function anyDeviceFilter() {
  // This is the closest we can get for now to get all devices.
  return Array.from('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
    .map(c => ({namePrefix: c}))
    .concat({name: ''});
}

navigator.bluetooth.requestDevice({
  filters: anyDeviceFilter(),
})

cljs版本的设备扫描

初步验证一下

;;下面这两种方式不work, 因为this指针的问题, bluetooth需要绑定到navigator上才行
;; https://stackoverflow.com/questions/31481396/uncaught-typeerror-illegal-invocation-when-trying-to-support-cross-browser-pr/31523066#31523066
;;(.-requestDevice (.-bluetooth js/navigator))
;;(.. js/navigator -bluetooth -requestDevice)
(def anyDeviceFilter
  (mapv (fn [e] {"namePrefix" e})
        "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"))
(js/navigator.bluetooth.requestDevice (clj->js {"filters" anyDeviceFilter}))

js的Promise链

仔细看一下代码,我们会发现Promise..then的频繁使用:

connectButton.addEventListener('click', function () {
progress.hidden = false;
disableInput();
Promise.resolve()
.then(_ => {
  if (!navigator.bluetooth)
    throw "No Web Bluetooth support.";
  return navigator.bluetooth.requestDevice({
    filters: anyDeviceFilter(),
    optionalServices: ['generic_access']
  })
})
.then(logProgress)
.then(device => {
  nameInput.value = "";
  return device.gatt.connect().catch(error => {
    logProgress(error);
    throw "Unable to connect. Some devices refuse connections.";
  });
})
.then(logProgress)
.then(server => server.getPrimaryService("generic_access"))
.then(logProgress)
.then(service => service.getCharacteristic("gap.device_name"))
.then(logProgress)
.then(characteristic => {
  window.deviceName = characteristic;
  return characteristic.readValue();
})
.then(logProgress)
.then(value => {
  window.value = value;
  nameInput.value = new TextDecoder("utf-8").decode(value);
  if (window.deviceName.properties.write)
    enableInput();
  else
    throw "Name is not writable on this device.";
})
.catch(handleError)
.then(_ => progress.hidden = true)
;

cljs的Promie实现

我们怎么使用cljs实现同样的功能呢:


(refer-clojure :exclude '[resolve])(refer-clojure :exclude '[resolve])

(defn every [& args]
  (js/Promise.all (into-array args)))

(defn soon
  "Simulate an asynchronous result"
  ([v] (soon v identity))
  ([v f] (js/Promise. (fn [resolve]
                        (js/setTimeout #(resolve (f v))
                                       500)))))

(defn resolve [v]
  (js/Promise.resolve v))



;; helpers

(defn square [n] (* n n))

;; test0
(defn test0
  "Synchronous version - for comparison
  The code has three steps:
  -get value for n
  -get square of n
  -get sum of n and n-squared
  Note that step 3 requires access to the original value, n, and to the computed
  value, n-squared."
  []
  (let [n 5
        n-squared (square 5)
        result (+ n n-squared)]
    (js/alert result)))

;; test1

(defn square-step [n]
  (soon (every n (soon n square))))

(defn sum-step [[n squared-n]] ;; Note: CLJS destructuring works with JS arrays
  (soon (+ n squared-n)))

(defn test1
  "Array approach, flat chain: thread multiple values through promise chain by using Promise.all"
  []
  (-> (resolve 5)
      (.then square-step)
      (.then sum-step)
      (.then js/alert)))

;; test2

(defn to-map-step [array]
  (zipmap [:n :n-squared] array))

(defn sum2-step [{:keys [n n-squared] :as m}]
  (soon (assoc m :result (+ n n-squared))))

(defn test2
  "Accumulative map approach, flat chain: add values to CLJS map in each `then` step, making
  it possible for later members of the chain to access previous results"
  []
  (-> (resolve 5)
      (.then square-step)
      (.then to-map-step)
      (.then sum2-step)
      ;; Note: `(.then :result)` doesn't work because `:result` is not
      ;; recognized as a function. So we need to wrap it in an anon fn.
      ;; This could be easily fixed by adding a CLJS `then` function that
      ;; has a more inclusive notion of what a function is.
      (.then #(:result %))
      (.then js/alert)))

;; test3

(defn square-step-fn [n]
  ;; This could be called a "resolver factory" fn. It's a higher-order function
  ;; that returns a resolve function. `n` is captured in a closure.
  (fn [n-squared]
    (soon (+ n n-squared))))

(defn square-and-sum-step [n]
  (-> (soon (square n))
      ;; note that square-step-fn is _called_ here, not referenced, in order to
      ;; provide its inner body with access to the previous result, `n`.
      (.then (square-step-fn n))))

(defn test3
  "Nested chain approach: instead of a flat list, use a hierarchy, nesting one Promise chain in another.
  Uses a closure to capture the intermediate result, `n`, making it available to the nested chain."
  []
  (-> (resolve 5)
      (.then square-and-sum-step)
      (.then js/alert)))

cljs的WebBle链

实现了

  1. Promise
  2. then
  3. exception
  4. BLE的scan和connect,其他可以继续添加

注意:使用JS函数的时候,this的bind是个坑,可以看一下参考资料中js-cljs interop的部分

(require '[goog.string :as gstring])
(refer-clojure :exclude '[resolve])


;;device.gatt.connect()

(defn conn [device]
  (. 
   (.. device -gatt -connect) 
   call 
   (. device -gatt)))

(defn af [] 
  (throw "good day"))
(-> (resolve (js/navigator.bluetooth.requestDevice (clj->js {"filters" anyDeviceFilter})) )
    (.then conn)
    (.then af)
    (.catch (fn [e]
              (js/console.info 
               (gstring/format (str e)))))
    (.then js/console.info))   

参考

https://blog.jeaye.com/2017/09/30/clojurescript-promesa/ https://gist.github.com/pesterhazy/74dd6dc1246f47eb2b9cd48a1eafe649 https://github.com/WebBluetoothCG/demos/blob/gh-pages/bluetooth-rename/index.html https://lwhorton.github.io/2018/10/20/clojurescript-interop-with-javascript.html

Tags: clojure clojurescript