diff --git a/.gitignore b/.gitignore index 213bb85..fd6a732 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ target *.sublime-* *.swp + +node_modules diff --git a/.travis.yml b/.travis.yml index 60f08da..a483ff0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: clojure lein: lein2 -script: lein2 test +script: lein2 cleantest jdk: - openjdk7 - oraclejdk7 diff --git a/README.md b/README.md index 06629aa..2109ba2 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ # clj-gpio A basic library for reading, writing and watching GPIO signals on a Raspberry -Pi, in a Clojure REPL-friendly way. +Pi, in a Clojure REPL-friendly way. Now, also targets ClojureScript. ## Usage Add the following to your `project.clj` - [clj-gpio "0.1.0"] + [clj-gpio 0.2.0] Fire up a REPL, and require `gpio.core`. @@ -28,11 +28,6 @@ To read the value of the port, we can do the following: user=> (read-value port) :low -Or, more conveniently, we can deref it: - - user=> @port - :low - To set values on the port, The port needs to be configured for `out` mode: user=> (set-direction! port :out) @@ -45,6 +40,12 @@ as follows: With our LED connected to gpio 17, we should see it turned on. We can also read back the value and see that `(= :high @port)`. +We can also toggle the state, for convenience: + + user=> (toggle! port) + +which will flip the state from `:low` to `:high` or vice versa. + ### GPIO Listening. We can also pull events off of a gpio port by using `open-channel-port`. In @@ -57,14 +58,13 @@ For example (if we have a push button on GPIO 18): user=> (def ch-port (open-channel-port 18)) #'user/ch-port user=> (set-direction! ch-port :in) - nil + ... user=> (set-edge! ch-port :both) ; or :falling, :rising, and :none to disable - nil + ... We'll also set the bit to :high when the button pressed: user=> (set-active-low! ch-port true) - nil Let's turn on the LED we defined in the Read/Write example above when our button is pressed: diff --git a/project.clj b/project.clj index 0b8620f..5a2c26c 100644 --- a/project.clj +++ b/project.clj @@ -1,15 +1,22 @@ -(defproject clj-gpio "0.1.0" - :description "A lightweight Clojure library for Raspberry PI GPIO" - :url "https://github.com/peterschwarz/clj-gpio" - :license {:name "Eclipse Public License" - :url "http://www.eclipse.org/legal/epl-v10.html"} - :min-lein-version "2.0.0" - :source-paths ["src/main/clojure"] - :test-paths ["src/test/clojure"] - :java-source-paths ["src/main/java"] - :javac-options ["-target" "1.6" "-source" "1.6"] +(defproject clj-gpio "0.2.0" + :description "A lightweight Clojure library for Raspberry PI GPIO" + :url "http://peterschwarz.github.io/clj-gpio" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + :min-lein-version "2.0.0" + :source-paths ["src/main/clojure"] + :test-paths ["src/test/clojure"] + :java-source-paths ["src/main/java"] + :javac-options ["-target" "1.6" "-source" "1.6"] - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/core.async "0.1.346.0-17112a-alpha"] - [net.java.dev.jna/jna "4.1.0"]]) + :dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/clojurescript "1.7.170"] + [org.clojure/core.async "0.2.374"] + [net.java.dev.jna/jna "4.2.1"]] + + :aliases {"cljs:test" ["trampoline" "run" "-m" "clojure.main" "./scripts/test.clj"] + "cljs:repl" ["trampoline" "run" "-m" "clojure.main" "./scripts/repl.clj"] + "cljs:dev" ["trampoline" "run" "-m" "clojure.main" "./scripts/build.clj"] + "cljs:dev:watch" ["trampoline" "run" "-m" "clojure.main" "./scripts/watch.clj"] + "cleantest" ["do" ["clean"] ["test"] ["cljs:test"]]}) diff --git a/scripts/build.clj b/scripts/build.clj new file mode 100644 index 0000000..f07da7b --- /dev/null +++ b/scripts/build.clj @@ -0,0 +1,8 @@ +(require 'cljs.build.api) + +(cljs.build.api/build + (cljs.build.api/inputs "src/main/clojure" "src/dev/clojure") + {:main 'gpio.dev + :output-to "target/out/dev.js" + :output-dir "target/out" + :target :nodejs }) diff --git a/scripts/repl.clj b/scripts/repl.clj new file mode 100644 index 0000000..8c43420 --- /dev/null +++ b/scripts/repl.clj @@ -0,0 +1,12 @@ +(require 'cljs.repl) +(require 'cljs.build.api) +(require 'cljs.repl.node) + +(cljs.build.api/build "src/main/clojure" + {:output-to "target/out/main.js" + :output-dir "target/out" + :verbose true}) + +(cljs.repl/repl (cljs.repl.node/repl-env) + :watch "src/main/clojure" + :output-dir "target/out") diff --git a/scripts/test.clj b/scripts/test.clj new file mode 100644 index 0000000..a6a56b2 --- /dev/null +++ b/scripts/test.clj @@ -0,0 +1,22 @@ +(require 'cljs.build.api) + +(def test-inputs (cljs.build.api/inputs "src/main/clojure" "src/test/clojure")) + +(def test-opts + {:main 'gpio.test-runner + :output-to "target/out/test.js" + :output-dir "target/out" + :target :nodejs }) + +(require 'clojure.java.shell) + +(defn run-tests [] + (let [result (clojure.java.shell/sh "node" (:output-to test-opts))] + (println (:out result)) + (.println *err* (:err result)))) + +(cljs.build.api/build test-inputs test-opts) + +(run-tests) + +(System/exit 0) diff --git a/scripts/watch.clj b/scripts/watch.clj new file mode 100644 index 0000000..ec77bfa --- /dev/null +++ b/scripts/watch.clj @@ -0,0 +1,8 @@ +(require 'cljs.build.api) + +(cljs.build.api/watch + (cljs.build.api/inputs "src/main/clojure" "src/dev/clojure") + {:main 'gpio.dev + :output-to "target/out/dev.js" + :output-dir "target/out" + :target :nodejs }) diff --git a/src/dev/clojure/gpio/dev.cljs b/src/dev/clojure/gpio/dev.cljs new file mode 100644 index 0000000..c4c28bb --- /dev/null +++ b/src/dev/clojure/gpio/dev.cljs @@ -0,0 +1,52 @@ +(ns gpio.dev + (:require [cljs.nodejs :as nodejs] + [gpio.core :as gpio] + [cljs.core.async :as a :refer [! >!! chan sliding-buffer tap]] - [clojure.core.async.impl.protocols :as p]) - (:import [java.io RandomAccessFile FileOutputStream PrintStream] - [java.nio.channels FileChannel FileChannel$MapMode] - [io.bicycle.epoll EventPolling EventPoller PollEvent])) + (:require [gpio.poll :as poll] + [gpio.io :refer [write-file read-file]] + #?(:clj [clojure.core.async :as a + :refer [go ! >!! chan sliding-buffer tap]] + :cljs [cljs.core.async :as a + :refer [! chan sliding-buffer tap]]) + #?(:clj [clojure.core.async.impl.protocols :as p] + :cljs [cljs.core.async.impl.protocols :as p])) + #?(:cljs (:require-macros [cljs.core.async.macros :refer [go]]))) (defn export! [port] - (spit "/sys/class/gpio/export" (str port))) + (write-file "/sys/class/gpio/export" (str port))) (defn unexport! [port] - (spit "/sys/class/gpio/unexport" (str port))) + (write-file "/sys/class/gpio/unexport" (str port))) (defn- do-set-direction! [port direction] {:pre [(some #(= direction %) [:in :out 'in 'out "in" "out"])]} - (spit (str "/sys/class/gpio/gpio" port "/direction") (name direction))) + (write-file (str "/sys/class/gpio/gpio" port "/direction") (name direction))) (defn- do-set-edge! [port setting] {:pre [(some #(= setting %) [:none, :falling, :rising, :both, 'none, 'falling, 'rising, 'both "none", "falling", "rising","both"])]} - (spit (str "/sys/class/gpio/gpio" port "/edge") (name setting))) + (write-file (str "/sys/class/gpio/gpio" port "/edge") (name setting))) (defn- do-set-active-low! [port-num active-low?] - (spit (str "/sys/class/gpio/gpio" port-num "/active_low") (if active-low? "1" "0"))) + (write-file (str "/sys/class/gpio/gpio" port-num "/active_low") (if active-low? "1" "0"))) (defn high-low-value [value] {:pre [(not (nil? (#{:high :low 1 0 'high 'low true false "1" "0" \1 \0} value)))]} - (byte (condp = value + (char (condp = value :high \1 1 \1 'high \1 @@ -44,7 +47,7 @@ (defn- do-format [raw-value high low] - (if (= \1 raw-value) high low)) + (if (= \1 (first raw-value)) high low)) (defmulti format-raw-digital "Formats the raw values received from digital reads of pin state, @@ -59,7 +62,7 @@ (defmethod format-raw-digital :boolean [_ raw-value] - (= \1 raw-value)) + (= \1 (first raw-value))) (defmethod format-raw-digital :symbol [_ raw-value] @@ -71,11 +74,11 @@ (defmethod format-raw-digital :char [_ raw-value] - raw-value) + (first raw-value)) (defmethod format-raw-digital :default [_ raw-value] - raw-value) + (first raw-value)) (defprotocol Closeable (close! [self] "Closes this object")) @@ -84,7 +87,8 @@ (set-direction! [port direction] "Sets the direction of this port: in or out.") (set-active-low! [port active-low?] "Invert the logic of the value pin for both reading and writing so that a high == 0 and low == 1. ") (read-value [port] "Return the value of the port") - (write-value! [port value] "Writes the value to the port. The value may be specified as `:high`, `:low` (and symbol or string variations), \1, \0, or 1, 0")) + (write-value! [port value] "Writes the value to the port. The value may be specified as `:high`, `:low` (and symbol or string variations), \1, \0, or 1, 0") + (toggle! [port] "Flips the value of the port")) (defprotocol GpioChannelProvider (set-edge! [providor setting]) @@ -94,36 +98,45 @@ (defn- value-file [port] (str "/sys/class/gpio/gpio" port "/value")) -(defn random-access [filename] - (RandomAccessFile. filename "rw")) - -(defrecord BasicGpioPort [port filename file formatter] +(defrecord BasicGpioPort [port filename formatter] GpioPort - (set-direction! [_ direction] - (do-set-direction! port direction)) + (set-direction! [this direction] + (do-set-direction! port direction) + this) - (set-active-low! [_ active-low?] - (do-set-active-low! port active-low?)) + (set-active-low! [this active-low?] + (do-set-active-low! port active-low?) + this) (read-value [_] - (.seek file 0) - (formatter (char (.read file)))) + (formatter (read-file filename))) (write-value! - [_ value] - (.seek file 0) - (.writeByte file (high-low-value value))) + [this value] + (write-file filename (high-low-value value)) + this) - clojure.lang.IDeref - (deref [this] (read-value this)) + (toggle! [this] + (let [x (read-file filename)] + (write-value! this (do-format x 0 1)))) Closeable (close! [_] - (.close file) (unexport! port))) +(defn- preconfigure [gpio-port opts] + (let [{:keys [direction active-low? initial-value]} opts] + (try + (cond-> gpio-port + direction (set-direction! direction) + active-low? (set-active-low! active-low?) + initial-value (write-value! initial-value)) + (catch #?(:clj Exception :cljs :default) e + (close! gpio-port) + (throw e))))) + (defn open-port "Opens a port from which values may be read or written. Args: @@ -139,21 +152,19 @@ Overrides the default formatter" [port & opts] (export! port) - (let [{:keys [digital-result-format from-raw-fn direction active-low? initial-value] + (let [{:keys [digital-result-format from-raw-fn] :or {digital-result-format :keyword}} opts formatter (or from-raw-fn (partial format-raw-digital digital-result-format)) filename (value-file port) - raf (random-access filename) - gpio-port (BasicGpioPort. port filename raf formatter)] - (try - (when direction (set-direction! gpio-port direction)) - (when active-low? (set-active-low! gpio-port active-low?)) - (when initial-value (write-value! gpio-port initial-value)) - gpio-port - (catch Exception e - (close! gpio-port) - (throw e))))) + gpio-port (BasicGpioPort. port filename formatter)] + ; Need to wait for the direction file to be available + #?(:clj (do + (Thread/sleep 100) + (preconfigure gpio-port opts)) + :cljs (do + (js/setTimeout #(preconfigure gpio-port opts) 100) + gpio-port)))) (defn- tap-and-wrap-chan [mult-ch out-ch] (a/tap mult-ch out-ch) @@ -169,34 +180,40 @@ (p/take! [_ fn1-handler] (p/take! out-ch fn1-handler)))) -(def ^:private POLLING_CONFIG - (bit-or EventPolling/EPOLLIN EventPolling/EPOLLET EventPolling/EPOLLPRI)) (defrecord EdgeGpioPort [port gpio-port event-poller read-ch write-ch mult-ch chan-factory-fn] GpioPort - (set-direction! [_ direction] (set-direction! gpio-port direction)) - (set-active-low! [_ active-low?] (set-active-low! gpio-port active-low?)) + (set-direction! [this direction] + (set-direction! gpio-port direction) + this) + + (set-active-low! [this active-low?] + (set-active-low! gpio-port active-low?) + this) + (read-value [_] (read-value gpio-port)) - (write-value! [this value] (>!! write-ch value)) + (write-value! [this value] + (a/put! write-ch value) + this) + + (toggle! [_] (toggle! gpio-port)) GpioChannelProvider - (set-edge! [_ setting] - (do-set-edge! port setting)) + (set-edge! [this setting] + (do-set-edge! port setting) + this) (create-edge-channel [_] (tap-and-wrap-chan mult-ch (chan-factory-fn))) - clojure.lang.IDeref - (deref [this] (read-value this)) - Closeable (close! [_] (a/close! write-ch) (a/close! read-ch) - (.close event-poller) + (poll/cancel-watch event-poller) (close! gpio-port))) (defn open-channel-port @@ -210,14 +227,13 @@ :or {event-buffer-size 1, timeout -1}} opts create-channel (fn [] (chan (sliding-buffer event-buffer-size))) gpio-port (apply open-port port opts) - poller (EventPolling/create) write-ch (chan 1) read-ch (create-channel) - mult-ch (a/mult read-ch)] + mult-ch (a/mult read-ch) + poller (poll/watch-port gpio-port timeout #(a/put! read-ch (read-value gpio-port))) ] - (.addFile poller (:file gpio-port) POLLING_CONFIG gpio-port) - - (when edge (do-set-edge! port edge)) + #?(:clj (when edge (do-set-edge! port edge)) + :cljs (js/setTimeout #(when edge (do-set-edge! port edge)) 100)) ; Serialize the write loop (go (loop [] @@ -225,10 +241,4 @@ (write-value! gpio-port data) (recur)))) - (go (loop [] - (when-let [events (.poll poller timeout)] - (doseq [_ (filter #(= gpio-port (.getData %)) events)] - (>! read-ch (read-value gpio-port))) - (recur)))) - (EdgeGpioPort. port gpio-port poller read-ch write-ch mult-ch create-channel))) diff --git a/src/main/clojure/gpio/io.cljc b/src/main/clojure/gpio/io.cljc new file mode 100644 index 0000000..839c2f3 --- /dev/null +++ b/src/main/clojure/gpio/io.cljc @@ -0,0 +1,12 @@ +(ns gpio.io + #?(:cljs (:require [cljs.nodejs :as nodejs]))) + +#?(:cljs (defonce ^:private fs (nodejs/require "fs"))) + +(defn write-file [filename content] + #?(:clj (spit filename content) + :cljs (.writeFileSync fs filename content #js {:encoding "ascii"}))) + +(defn read-file [filename] + #?(:clj (slurp filename) + :cljs (.readFileSync fs filename #js {:encoding "ascii"}))) diff --git a/src/main/clojure/gpio/poll.cljc b/src/main/clojure/gpio/poll.cljc new file mode 100644 index 0000000..1535e5c --- /dev/null +++ b/src/main/clojure/gpio/poll.cljc @@ -0,0 +1,43 @@ +(ns gpio.poll + #?(:clj (:require [clojure.core.async :as a :refer [go-loop]] + [clojure.core.async.impl.protocols :as p]) + :cljs (:require [cljs.nodejs :as nodejs])) + #?(:clj (:import [io.bicycle.epoll EventPolling EventPoller PollEvent]))) + +#?( +:clj (do + + (def ^:private POLLING_CONFIG + (bit-or EventPolling/EPOLLIN EventPolling/EPOLLET EventPolling/EPOLLPRI)) + + (defn watch-port [port ^Integer timeout on-change-fn] + (let [poller (EventPolling/create)] + (.addFile poller (:filename port) POLLING_CONFIG port) + + (go-loop [] + (when-let [events (.poll poller timeout)] + (doseq [_ (filter #(= port (.getData %)) events)] + (on-change-fn)) + (recur))) + + poller)) + + (defn cancel-watch [^EventPoller poller] + (.close poller)) + +) + +:cljs (do + + (def ^:private fs (nodejs/require "fs")) + + (defn watch-port [port _ on-change-fn] + (.watch fs (:filename port) + (fn [event filename] + (when (and (= "change" event)) + (on-change-fn))))) + + (defn cancel-watch [poller] + (.close poller)) + +)) diff --git a/src/main/java/io/bicycle/epoll/EventPoller.java b/src/main/java/io/bicycle/epoll/EventPoller.java index a956af0..046d21f 100644 --- a/src/main/java/io/bicycle/epoll/EventPoller.java +++ b/src/main/java/io/bicycle/epoll/EventPoller.java @@ -10,17 +10,17 @@ */ public interface EventPoller { - void addFile(RandomAccessFile file, int flags); + void addFile(String filename, int flags); - void addFile(RandomAccessFile file, int flags, Object data); + void addFile(String filename, int flags, Object data); - void modifyFile(RandomAccessFile file, int flags); + void modifyFile(String filename, int flags); - void modifyFile(RandomAccessFile file, int flags, Object data); + void modifyFile(String filename, int flags, Object data); List poll(int timeout); - void removeFile(RandomAccessFile file); + void removeFile(String filename); void close(); } diff --git a/src/main/java/io/bicycle/epoll/EventPolling.java b/src/main/java/io/bicycle/epoll/EventPolling.java index d6e0f24..7f6caf1 100644 --- a/src/main/java/io/bicycle/epoll/EventPolling.java +++ b/src/main/java/io/bicycle/epoll/EventPolling.java @@ -1,9 +1,13 @@ package io.bicycle.epoll; -import com.sun.jna.*; +import com.sun.jna.Native; +import com.sun.jna.Platform; +import com.sun.jna.Pointer; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; -import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -43,19 +47,22 @@ public static EventPoller create(int maxEvents) { private static final class FileFDTuple { - final RandomAccessFile file; + final String filename; + final FileInputStream fileInputStream; final int fd; final NativePollEvent event; private Object data; - private FileFDTuple(RandomAccessFile file, int fd, NativePollEvent event, Object data) { - this.file = file; + private FileFDTuple(String filename, FileInputStream fileInputStream, int fd, NativePollEvent event, Object data) { + this.filename = filename; + this.fileInputStream = fileInputStream; this.fd = fd; this.event = event; this.data = data; } } + @SuppressWarnings("Convert2Diamond") private static class EventPollerImpl implements EventPoller { private final int epfd; @@ -73,13 +80,19 @@ private static class EventPollerImpl implements EventPoller { } @Override - public void addFile(RandomAccessFile file, int flags) { - this.addFile(file, flags, null); + public void addFile(String filename, int flags) { + this.addFile(filename, flags, null); } @Override - public void addFile(RandomAccessFile file, int flags, Object data) { - int fd = nativeFd(file); + public void addFile(String filename, int flags, Object data) { + final FileInputStream fileInputStream; + try { + fileInputStream = new FileInputStream(new File(filename)); + } catch (FileNotFoundException e) { + throw new RuntimeException("Unable to open filename."); + } + int fd = nativeFd(fileInputStream); final NativePollEvent event = new NativePollEvent(flags, new NativePollEventData(fd)); event.write(); @@ -87,17 +100,17 @@ public void addFile(RandomAccessFile file, int flags, Object data) { throw new RuntimeException("Unable to add to epoll set"); } - fileFDTuples.add(new FileFDTuple(file, fd, event, data)); + fileFDTuples.add(new FileFDTuple(filename, fileInputStream, fd, event, data)); } @Override - public void modifyFile(RandomAccessFile file, int flags) { - modifyFile(file, flags, null); + public void modifyFile(String filename, int flags) { + modifyFile(filename, flags, null); } @Override - public void modifyFile(RandomAccessFile file, int flags, Object data) { - final FileFDTuple tuple = tupleFor(file); + public void modifyFile(String filename, int flags, Object data) { + final FileFDTuple tuple = tupleFor(filename); final NativePollEvent event = tuple.event; event.events = flags; tuple.data = data; @@ -109,11 +122,16 @@ public void modifyFile(RandomAccessFile file, int flags, Object data) { } @Override - public void removeFile(RandomAccessFile file) { - final FileFDTuple tuple = tupleFor(file); + public void removeFile(String filename) { + final FileFDTuple tuple = tupleFor(filename); deregister(tuple); + try { + tuple.fileInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } fileFDTuples.remove(tuple); } @@ -137,7 +155,7 @@ public List poll(int timeout) { // System.out.println(this.events[i]); final FileFDTuple tuple = tupleFor(this.events[i].data.fd); events.add(new PollEvent(this, - tuple != null ? tuple.file : null, + tuple != null ? tuple.filename : null, PollEvent.Type.fromRawType(this.events[0].events), tuple != null ? tuple.data : null)); } @@ -169,25 +187,25 @@ private FileFDTuple tupleFor(int fd) { return null; } - private FileFDTuple tupleFor(RandomAccessFile file) { - for (FileFDTuple pair : fileFDTuples) { - if (pair.file.equals(file)) { - return pair; + private FileFDTuple tupleFor(String filename) { + for (FileFDTuple tuple : fileFDTuples) { + if (tuple.filename.equals(filename)) { + return tuple; } } throw new RuntimeException("File not already associated with this epoller."); } - private int nativeFd(RandomAccessFile file) { + private int nativeFd(FileInputStream fileInputStream) { final int fd; try { - fd = NativeFileUtils.getFileHandle(file.getFD()); + fd = NativeFileUtils.getFileHandle(fileInputStream.getFD()); } catch (IOException e) { - throw new RuntimeException("Unable to get native file descriptor", e); + throw new RuntimeException("Unable to get native fileInputStream descriptor", e); } if (fd == -1) { - throw new RuntimeException("Unable to get native file descriptor"); + throw new RuntimeException("Unable to get native fileInputStream descriptor"); } return fd; } diff --git a/src/main/java/io/bicycle/epoll/PollEvent.java b/src/main/java/io/bicycle/epoll/PollEvent.java index 6674dfc..dac9988 100644 --- a/src/main/java/io/bicycle/epoll/PollEvent.java +++ b/src/main/java/io/bicycle/epoll/PollEvent.java @@ -1,6 +1,6 @@ package io.bicycle.epoll; -import java.io.RandomAccessFile; +import java.io.File; import java.util.HashSet; import java.util.Set; @@ -11,7 +11,7 @@ */ public final class PollEvent { private final EventPoller source; - private final RandomAccessFile file; + private final String filename; private final Set types; private final Object data; @@ -72,9 +72,9 @@ static Set fromRawType(final int raw) { } - PollEvent(EventPoller source, RandomAccessFile file, Set types, Object data) { + PollEvent(EventPoller source, String filename, Set types, Object data) { this.source = source; - this.file = file; + this.filename = filename; this.types = types; this.data = data; } @@ -83,8 +83,8 @@ public EventPoller getSource() { return source; } - public RandomAccessFile getFile() { - return file; + public String getFilename() { + return filename; } public Set getType() { @@ -99,7 +99,7 @@ public Object getData() { public String toString() { final StringBuffer sb = new StringBuffer("(EpollEvent. "); sb.append(source); - sb.append(", ").append(file); + sb.append(", ").append(filename); sb.append(", #{"); int c = types.size() - 1; diff --git a/src/test/clojure/gpio/core_tests.cljc b/src/test/clojure/gpio/core_tests.cljc new file mode 100644 index 0000000..680f935 --- /dev/null +++ b/src/test/clojure/gpio/core_tests.cljc @@ -0,0 +1,169 @@ +(ns gpio.core-tests + (:require #?(:clj [clojure.test :refer :all] + :cljs [cljs.test :refer-macros [deftest is testing use-fixtures]]) + [gpio.mock-files :refer [mock-file-fixture]] + [gpio.io :refer [read-file write-file]] + [gpio.core :refer [high-low-value + export! unexport! close! + open-port read-value write-value! toggle! + set-direction! set-active-low! + open-channel-port set-edge!]])) + +(deftest test-high-low-value + (testing "high" + (is (= \1 (high-low-value :high))) + (is (= \1 (high-low-value 'high))) + (is (= \1 (high-low-value 1))) + (is (= \1 (high-low-value "1"))) + (is (= \1 (high-low-value \1))) + (is (= \1 (high-low-value true)))) + + (testing "low" + (is (= \0 (high-low-value :low))) + (is (= \0 (high-low-value 'low))) + (is (= \0 (high-low-value 0))) + (is (= \0 (high-low-value "0"))) + (is (= \0 (high-low-value \0))) + (is (= \0 (high-low-value false)))) + + (testing "invalid values" + (is (thrown? #?(:clj AssertionError :cljs js/Error) (high-low-value 3))) + (is (thrown? #?(:clj AssertionError :cljs js/Error) (high-low-value :foo))))) + +(use-fixtures :each mock-file-fixture) + +(deftest test-export + (export! 17) + (is (= "17" (read-file "/sys/class/gpio/export")))) + +(deftest test-unexport + (unexport! 18) + (is (= "18" (read-file "/sys/class/gpio/unexport")))) + +(deftest test-open-port + (write-file "/sys/class/gpio/gpio17/value" \0) + (let [port (open-port 17)] + (is (= :low (read-value port))) + (write-value! port :high) + (is (= "1" (read-file "/sys/class/gpio/gpio17/value"))))) + +(deftest test-read-symbol + (write-file "/sys/class/gpio/gpio2/value" \1) + (let [port (open-port 2 :digital-result-format :symbol)] + (is (= 'high (read-value port))) + (write-value! port :low) + (is (= 'low (read-value port))))) + +(deftest test-read-boolean + (write-file "/sys/class/gpio/gpio2/value" \1) + (let [port (open-port 2 :digital-result-format :boolean)] + (is (= true (read-value port))) + (write-value! port :low) + (is (= false (read-value port))))) + +(deftest test-read-integer + (write-file "/sys/class/gpio/gpio2/value" \1) + (let [port (open-port 2 :digital-result-format :integer)] + (is (= 1 (read-value port))) + (write-value! port :low) + (is (= 0 (read-value port))))) + +(deftest test-read-char + (write-file "/sys/class/gpio/gpio2/value" \1) + (let [port (open-port 2 :digital-result-format :char)] + (is (= \1 (read-value port))) + (write-value! port :low) + (is (= \0 (read-value port))))) + +(deftest test-read-custom + (write-file "/sys/class/gpio/gpio2/value" \1) + (let [port (open-port 2 :from-raw-fn #(if (= \1 (first %)) :foo :bar))] + (is (= :foo (read-value port))) + (write-value! port :low) + (is (= :bar (read-value port))))) + +(deftest test-write-keyword + (write-file "/sys/class/gpio/gpio17/value" \0) + (let [port (open-port 17)] + (write-value! port :high) + (is (= "1" (read-file "/sys/class/gpio/gpio17/value"))) + (write-value! port :low) + (is (= "0" (read-file "/sys/class/gpio/gpio17/value"))))) + +(deftest test-write-symbol + (write-file "/sys/class/gpio/gpio17/value" \0) + (let [port (open-port 17)] + (write-value! port 'high) + (is (= "1" (read-file "/sys/class/gpio/gpio17/value"))) + (write-value! port 'low) + (is (= "0" (read-file "/sys/class/gpio/gpio17/value"))))) + +(deftest test-write-character + (write-file "/sys/class/gpio/gpio17/value" \0) + (let [port (open-port 17)] + (write-value! port \1) + (is (= "1" (read-file "/sys/class/gpio/gpio17/value"))) + (write-value! port \0) + (is (= "0" (read-file "/sys/class/gpio/gpio17/value"))))) + +(deftest test-write-integer + (write-file "/sys/class/gpio/gpio17/value" \0) + (let [port (open-port 17)] + (write-value! port 1) + (is (= "1" (read-file "/sys/class/gpio/gpio17/value"))) + (write-value! port 0) + (is (= "0" (read-file "/sys/class/gpio/gpio17/value"))))) + +(deftest test-write-string + (write-file "/sys/class/gpio/gpio17/value" \0) + (let [port (open-port 17)] + (write-value! port "1") + (is (= "1" (read-file "/sys/class/gpio/gpio17/value"))) + (write-value! port "0") + (is (= "0" (read-file "/sys/class/gpio/gpio17/value"))))) + +(deftest test-write-boolean + (write-file "/sys/class/gpio/gpio17/value" \0) + (let [port (open-port 17)] + (write-value! port true) + (is (= "1" (read-file "/sys/class/gpio/gpio17/value"))) + (write-value! port false) + (is (= "0" (read-file "/sys/class/gpio/gpio17/value"))))) + +(deftest test-set-direction + (write-file "/sys/class/gpio/gpio19/value" \0) + (let [port (open-port 19)] + (set-direction! port :in) + (is (= "in" (read-file "/sys/class/gpio/gpio19/direction"))))) + +(deftest test-set-active-low + (write-file "/sys/class/gpio/gpio21/value" \0) + (let [port (open-port 21)] + (set-active-low! port true) + (is (= "1" (read-file "/sys/class/gpio/gpio21/active_low"))))) + +(deftest test-toggle + (write-file "/sys/class/gpio/gpio21/value" \0) + (let [port (open-port 21)] + (toggle! port) + (is (= "1" (read-file "/sys/class/gpio/gpio21/value"))) + (toggle! port) + (is (= "0" (read-file "/sys/class/gpio/gpio21/value"))))) + +(deftest test-close! + (testing "close GpioPort" + (let [port (open-port 21)] + (close! port) + (is (= "21" (read-file "/sys/class/gpio/unexport"))))) + + #_(testing "close EdgeGpioPort" + (let [port (open-channel-port 20)] + (close! port) + (is (= "20" (read-file "/sys/class/gpio/unexport")))))) + +; Needs a platform-independent method for file watching +#_(deftest test-open-channel-port + (write-file "/sys/class/gpio/gpio1/value" \0) + (let [port (open-channel-port 1)] + (set-edge! port :rising) + (is (= "rising" (read-file "/sys/class/gpio/gpio1/edge"))))) diff --git a/src/test/clojure/gpio/mock_files.cljc b/src/test/clojure/gpio/mock_files.cljc new file mode 100644 index 0000000..278fd0b --- /dev/null +++ b/src/test/clojure/gpio/mock_files.cljc @@ -0,0 +1,95 @@ +(ns gpio.mock-files + (:require [gpio.io] + #?(:clj [clojure.java.io :refer [file delete-file]] + :cljs [cljs.nodejs :as nodejs]))) + +#?(:cljs + (do + (defonce ^:private fs (nodejs/require "fs")) + (defonce ^:private path (nodejs/require "path")))) + +(defn- dir? [f] + #?(:clj (.isDirectory (file f)) + :cljs (let [stats (.statSync fs f)] + (.isDirectory stats)))) + +(defn- in-parent [parent filename] + (let [f (subs filename 1)] + #?(:clj (file parent f) + :cljs (.join path parent f)))) + +(defn- get-parent [f] + #?(:clj (.getParentFile f) + :cljs (.dirname path f))) + +(defn- list-files [f] + #?(:clj (.listFiles (file f)) + :cljs (->> (.readdirSync fs f) + (map #(.join path f %))))) + +(defn- fexists? [f] + #?(:clj (.exists (file f)) + :cljs (try + (.closeSync fs (.openSync fs f "r")) + true + (catch :default e + false)))) + +(defn- del-file [f] + #?(:clj (delete-file (file f)) + :cljs (when (fexists? f) + (if (dir? f) + (.rmdirSync fs f) + (.unlinkSync fs f))))) + +(defn delete-recursively [fname] + (let [func (fn [func f] + (when (and (fexists? f) (dir? f)) + (doseq [f2 (list-files f)] + (func func f2))) + (del-file f))] + (func func fname))) + +(defn- mkdirs [f] + #?(:clj (.mkdirs (file f)) + :cljs (let [parts (.split f (.-sep path))] + (loop [dir (first parts) + remaining (rest parts)] + (if (fexists? dir) + (if (not (empty? remaining)) + (recur (.join path dir (first remaining)) + (rest remaining)) + true) + (try + (.mkdirSync fs dir) + (if (not (empty? remaining)) + (recur (.join path dir (first remaining)) + (rest remaining)) + true) + (catch :default e + false))))))) + +(defn- spitp [spit-fn parent filename content] + (let [f (in-parent parent filename)] + (-> (get-parent f) + (mkdirs)) + (spit-fn f content))) + +(defn- slurpp [slurp-fn parent filename] + (slurp-fn (in-parent parent filename))) + + +(defn with-mock-files [f] + (let [test-dir "target/test-files" + has-test-dir? (or (fexists? test-dir) (mkdirs test-dir))] + (assert has-test-dir? "unable to create test directory") + + (let [orig-spit gpio.io/write-file + orig-slurp gpio.io/read-file] + (with-redefs [gpio.io/write-file (partial spitp orig-spit test-dir) + gpio.io/read-file (partial slurpp orig-slurp test-dir)] + (f))))) + +(defn mock-file-fixture [f] + (with-mock-files f) + (delete-recursively "target/test-files")) diff --git a/src/test/clojure/gpio/test_runner.cljs b/src/test/clojure/gpio/test_runner.cljs new file mode 100644 index 0000000..fa9389d --- /dev/null +++ b/src/test/clojure/gpio/test_runner.cljs @@ -0,0 +1,35 @@ +(ns gpio.test-runner + (:require [cljs.nodejs :as nodejs] + [cljs.test :refer-macros [run-tests]] + ; Insert more test ns's here + [gpio.core-tests])) + +(nodejs/enable-util-print!) + +(defn run-suite [] + (run-tests + ; Insert more test ns's here + 'gpio.core-tests)) + +(defn- report-display [m] + {:title (str "CLJS tests: " (if (cljs.test/successful? m) "Success" "Failure")) + :message (str "Ran " (:test m) " tests containing " (:pass m) " assertions.\n" + (:fail m) " failures, " (:error m) " errors.")}) + +(defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] + (let [report (report-display m)] + (try + (let [notifier (nodejs/require "node-notifier")] + (.notify notifier + #js {:title (:title report) + :message (:message report)})) + (catch :default e + (println "WARN Unable to use node-notifier") + (println "\nTo install run: \n$ npm install --save-dev node-notifier") + )))) + +(defn -main [& args] + (println "\nRunning CLJS tests...") + (run-suite)) + +(set! *main-cli-fn* -main) diff --git a/src/test/clojure/gpio_core_test.clj b/src/test/clojure/gpio_core_test.clj deleted file mode 100644 index ae350ad..0000000 --- a/src/test/clojure/gpio_core_test.clj +++ /dev/null @@ -1,151 +0,0 @@ -(ns gpio-core-test - (:require [clojure.test :refer :all] - [mock-files :refer :all] - [gpio.core :refer :all])) - -(deftest test-high-low-value - (testing "high" - (is (= (byte \1) (high-low-value :high))) - (is (= (byte \1) (high-low-value 'high))) - (is (= (byte \1) (high-low-value 1))) - (is (= (byte \1) (high-low-value "1"))) - (is (= (byte \1) (high-low-value \1))) - (is (= (byte \1) (high-low-value true)))) - - (testing "low" - (is (= (byte \0) (high-low-value :low))) - (is (= (byte \0) (high-low-value 'low))) - (is (= (byte \0) (high-low-value 0))) - (is (= (byte \0) (high-low-value "0"))) - (is (= (byte \0) (high-low-value \0))) - (is (= (byte \0) (high-low-value false)))) - - (testing "invalid values" - (is (thrown? AssertionError (high-low-value 3))) - (is (thrown? AssertionError (high-low-value :foo))))) - -(use-fixtures :each mock-file-fixture) - -(deftest test-export - (export! 17) - (is (= "17" (slurp "/sys/class/gpio/export")))) - -(deftest test-unexport - (unexport! 18) - (is (= "18" (slurp "/sys/class/gpio/unexport")))) - -(deftest test-open-port - (spit "/sys/class/gpio/gpio17/value" \0) - (let [port (open-port 17)] - (is (= :low (read-value port))) - (write-value! port :high) - (is (= "1" (slurp "/sys/class/gpio/gpio17/value"))))) - -(deftest test-read-symbol - (spit "/sys/class/gpio/gpio2/value" \1) - (let [port (open-port 2 :digital-result-format :symbol)] - (is (= 'high (read-value port))) - (write-value! port :low) - (is (= 'low (read-value port))))) - -(deftest test-read-boolean - (spit "/sys/class/gpio/gpio2/value" \1) - (let [port (open-port 2 :digital-result-format :boolean)] - (is (= true (read-value port))) - (write-value! port :low) - (is (= false (read-value port))))) - -(deftest test-read-integer - (spit "/sys/class/gpio/gpio2/value" \1) - (let [port (open-port 2 :digital-result-format :integer)] - (is (= 1 (read-value port))) - (write-value! port :low) - (is (= 0 (read-value port))))) - -(deftest test-read-char - (spit "/sys/class/gpio/gpio2/value" \1) - (let [port (open-port 2 :digital-result-format :char)] - (is (= \1 (read-value port))) - (write-value! port :low) - (is (= \0 (read-value port))))) - -(deftest test-read-custom - (spit "/sys/class/gpio/gpio2/value" \1) - (let [port (open-port 2 :from-raw-fn #(if (= \1 %) :foo :bar))] - (is (= :foo (read-value port))) - (write-value! port :low) - (is (= :bar (read-value port))))) - -(deftest test-read-deref - (spit "/sys/class/gpio/gpio17/value" \0) - (let [port (open-port 17)] - (is (= :low @port)) - (write-value! port :high) - (is (= :high @port)))) - -(deftest test-write-keyword - (spit "/sys/class/gpio/gpio17/value" \0) - (let [port (open-port 17)] - (write-value! port :high) - (is (= "1" (slurp "/sys/class/gpio/gpio17/value"))) - (write-value! port :low) - (is (= "0" (slurp "/sys/class/gpio/gpio17/value"))))) - -(deftest test-write-symbol - (spit "/sys/class/gpio/gpio17/value" \0) - (let [port (open-port 17)] - (write-value! port 'high) - (is (= "1" (slurp "/sys/class/gpio/gpio17/value"))) - (write-value! port 'low) - (is (= "0" (slurp "/sys/class/gpio/gpio17/value"))))) - -(deftest test-write-character - (spit "/sys/class/gpio/gpio17/value" \0) - (let [port (open-port 17)] - (write-value! port \1) - (is (= "1" (slurp "/sys/class/gpio/gpio17/value"))) - (write-value! port \0) - (is (= "0" (slurp "/sys/class/gpio/gpio17/value"))))) - -(deftest test-write-integer - (spit "/sys/class/gpio/gpio17/value" \0) - (let [port (open-port 17)] - (write-value! port 1) - (is (= "1" (slurp "/sys/class/gpio/gpio17/value"))) - (write-value! port 0) - (is (= "0" (slurp "/sys/class/gpio/gpio17/value"))))) - -(deftest test-write-string - (spit "/sys/class/gpio/gpio17/value" \0) - (let [port (open-port 17)] - (write-value! port "1") - (is (= "1" (slurp "/sys/class/gpio/gpio17/value"))) - (write-value! port "0") - (is (= "0" (slurp "/sys/class/gpio/gpio17/value"))))) - -(deftest test-write-boolean - (spit "/sys/class/gpio/gpio17/value" \0) - (let [port (open-port 17)] - (write-value! port true) - (is (= "1" (slurp "/sys/class/gpio/gpio17/value"))) - (write-value! port false) - (is (= "0" (slurp "/sys/class/gpio/gpio17/value"))))) - -(deftest test-set-direction - (spit "/sys/class/gpio/gpio19/value" \0) - (let [port (open-port 19)] - (set-direction! port :in) - (is (= "in" (slurp "/sys/class/gpio/gpio19/direction"))))) - -(deftest test-set-active-low - (spit "/sys/class/gpio/gpio21/value" \0) - (let [port (open-port 21)] - (set-active-low! port true) - (is (= "1" (slurp "/sys/class/gpio/gpio21/active_low"))))) - -; Needs a platform-independent method for file watching -#_(deftest test-open-channel-port - (spit "/sys/class/gpio/gpio1/value" \0) - (let [port (open-channel-port 1)] - (set-edge! port :rising) - (is (= "rising" (slurp "/sys/class/gpio/gpio1/edge"))))) diff --git a/src/test/clojure/mock_files.clj b/src/test/clojure/mock_files.clj deleted file mode 100644 index f681613..0000000 --- a/src/test/clojure/mock_files.clj +++ /dev/null @@ -1,47 +0,0 @@ -(ns mock-files - (:require [clojure.java.io - :refer [file delete-file]] - [gpio.core :refer [random-access]])) - -(defn delete-recursively [fname] - (let [func (fn [func f] - (when (.isDirectory f) - (doseq [f2 (.listFiles f)] - (func func f2))) - (delete-file f))] - (func func (file fname)))) - -(defn- in-parent [parent filename] - (file parent (subs filename 1))) - -(defn- spitp [spit-fn parent filename content] - (let [f (in-parent parent filename)] - (-> (.getParentFile f) - (.mkdirs)) - (spit-fn f content))) - -(defn- slurpp [slurp-fn parent filename] - (slurp-fn (in-parent parent filename))) - -(defn- random-accessp [random-access-fn parent filename] - (random-access-fn (in-parent parent filename))) - - -(defmacro with-mock-files [& body] - `(let [test-dir# (file "target/test-files") - exists-or-created# (or (.exists test-dir#) (.mkdirs test-dir#))] - (assert exists-or-created# "unable to create test directory") - - (let [orig-spit# spit - orig-slurp# slurp - orig-random-access# random-access] - (with-redefs [spit (partial spitp orig-spit# test-dir#) - slurp (partial slurpp orig-slurp# test-dir#) - random-access (partial random-accessp orig-random-access# test-dir#)] - ~@body)))) - -(defn mock-file-fixture [f] - (with-mock-files - (f) - ) - (delete-recursively "target/test-files"))