Skip to content

Instantly share code, notes, and snippets.

@WhittlesJr
Created November 27, 2018 22:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save WhittlesJr/b5bdc6880d7639ec795a1e293af70a34 to your computer and use it in GitHub Desktop.
Save WhittlesJr/b5bdc6880d7639ec795a1e293af70a34 to your computer and use it in GitHub Desktop.
(ns clj-bacnet.binary-util
(:require [org.clojars.smee.binary.core :as binary]
[com.rpl.specter :refer :all])
(:import org.clojars.smee.binary.core.BinaryIO))
(defn hexify "Convert byte sequence to hex string"
[coll-or-num]
(let [coll (cond (coll? coll-or-num) coll-or-num
(number? coll-or-num) [coll-or-num]
(nil? coll-or-num) [])
hex [\0 \1 \2 \3 \4 \5 \6 \7 \8 \9 \a \b \c \d \e \f]]
(letfn [(hexify-byte [b]
(let [v (bit-and b 0xFF)]
[(hex (bit-shift-right v 4)) (hex (bit-and v 0x0F))]))]
(apply str (mapcat hexify-byte coll)))))
(defn hexify-pretty
[coll-or-num]
(->> coll-or-num
hexify
clojure.string/upper-case
(partition 2)
(map #(apply str %))
(clojure.string/join " ")))
(defn bits->int
[data]
(first (reduce (fn [[returned index] val]
(if (or (true? val) (= val 1))
[(bit-set returned index) (inc index)]
[returned (inc index)]))
[0 0]
(reverse data))))
(defn bit-ints->byte-ints
[ints]
(->> ints
(into [])
(partition 8)
(map #(map {0 false 1 true} %))
(map bits->int)))
(defn min-octets-signed-int
"Optimized version of `min-octets-signed-int-any-length` for typical values"
[value]
(cond (<= -128 value 127) 1
(<= -32768 value 32767) 2
(<= -8388608 value 8388607) 3
(<= -2147483648 value 2147483647) 4))
(defn min-octets-signed-int-any-length
"Slower version of `min-octets-signed-int` because I'm too lazy to hardcode more
ranges"
[value]
(-> (if (pos-int? value)
(inc value)
value)
(* 2)
Math/abs
(as-> n (/ (Math/log n) (Math/log 2)))
(/ 8)
Math/ceil
int))
(defn min-octets-unsigned-int
[value]
(some-> value
Math/abs
Long/toHexString
count
(/ 2)
Math/ceil
int))
(defn min-octets-string
[value]
(some-> value
(.getBytes "UTF-8")
count))
(defn int->bit-array
[value & [bit-count]]
(let [min-bits (* 8 (min-octets-unsigned-int value))
bit-count (or bit-count min-bits)]
(reduce (fn [returned index]
(cons (-> value
(bit-shift-right index)
(bit-and 1))
returned))
[]
(range bit-count))))
(defn int->byte-array
[int-value & [length]]
(let [length (or length (min-octets-unsigned-int int-value))]
(->> (-> int-value
BigInteger/valueOf
.toByteArray
reverse
(concat (repeat 0)))
(take length)
(into [])
reverse)))
(defn bit-bools->bytes [bools]
(-> (bits->int bools)
BigInteger/valueOf
.toByteArray))
(defn bytes->num
"https://gist.github.com/pingles/1235344"
[data]
(reduce bit-or
(map-indexed (fn [i x]
(bit-shift-left (bit-and x 0x0FF)
(* 8 (- (count data) i 1))))
data)))
(defn bit-count->byte-count
[bit-count]
(int (Math/ceil (/ bit-count 8))))
(defn decode-config-map
[value {:keys [bits enum-map lenient?] :as config-item}]
(cond enum-map ((#'binary/strict-map (clojure.set/map-invert enum-map) lenient?)
value)
(= bits 1) ({1 true 0 false} value)
:else value))
(defn encode-config-map
[given-value {:keys [bits enum-map lenient? bits] :as config-item}]
(let [value (cond enum-map ((#'binary/strict-map enum-map lenient?)
given-value)
(= bits 1) ({true 1 false 0} given-value given-value)
:else given-value)]
(when-not (number? value)
(throw (ex-info "Cannot cast to bit-array!" {:value value
:config-item config-item})))
(int->bit-array value bits)))
(defn encode-complex-bytes
[given-map config-list]
(reduce (fn [encoded-bits {:keys [name] :as config-item}]
(->> (cond (map? config-item)
(let [given-value (get given-map name)]
(encode-config-map given-value config-item))
(keyword? config-item)
({false [0] true [1]} (get given-map config-item))
(nil? config-item) [0]
:else [])
(concat encoded-bits)))
[]
config-list))
(defn get-bit-count
[config]
(let [bool-bit-count (->> config (filter keyword?) count)
complex-bit-count (->> config (filter map?) (map :bits) (apply +))
skipped-bit-count (->> config (filter nil?) count)]
(+ bool-bit-count complex-bit-count skipped-bit-count)))
(defn decode-complex-bytes
[given-bits config]
(-> (reduce (fn [[remaining-bits decoded] {:keys [bits name] :as config-item}]
(cond (map? config-item)
(let [value (-> (take bits remaining-bits)
bits->int
(decode-config-map config-item))
remaining-bits (nthrest remaining-bits bits)
decoded (merge decoded {name value})]
[remaining-bits decoded])
(keyword? config-item)
[(rest remaining-bits)
(assoc decoded config-item (= 1 (first remaining-bits)))]
(nil? config-item) [(rest remaining-bits) decoded]
:else [remaining-bits decoded]))
[given-bits {}]
config)
second))
(defn post-decode-complex-bytes
[config]
(fn [bytes]
(let [bit-count (get-bit-count config)
bits (int->bit-array bytes bit-count)]
(decode-complex-bytes bits config))))
(defn pre-encode-complex-bytes
[config]
(fn [given-map]
(-> (encode-complex-bytes given-map config)
bits->int)))
(defn complex-bytes
[codec config]
(binary/compile-codec codec
(pre-encode-complex-bytes config)
(post-decode-complex-bytes config)))
(defn map-header-codec [codec header]
(binary/compile-codec codec
#(dissoc % (keys header))
#(merge % header)))
(defn decode-bytes
[codec value length]
(->> (int->byte-array value length)
byte-array
java.io.ByteArrayInputStream.
(binary/decode codec)))
(defn encode-bytes
[codec value]
(let [baos (java.io.ByteArrayOutputStream.)
_ (binary/encode codec baos value)
arr (.toByteArray baos)]
[arr (mapv binary/byte->ubyte (seq arr))]))
(def encode-ubytes (comp second encode-bytes))
(def encode-byte-array (comp first encode-bytes))
(defn decode-byte-array [codec arr] (binary/decode codec (java.io.ByteArrayInputStream. arr)))
(defn get-byte-length [codec value] (-> (encode-byte-array codec value) count))
(defn do-roundtrip [codec value & [do-not-decode?]]
(let [[arr encoded-ubytes] (encode-bytes codec value)
decoded (if (not do-not-decode?) (decode-byte-array codec arr))]
{:encoded encoded-ubytes
:value value
:decoded decoded}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment