     (if (= (take 5 (drop 4 (:data ident))) '(0 32 41 54 0))
       (aget (:data ident) 1)
       (timbre/error "Device does not identify as a Launchpad Mini:" port-in))))
+
+(defn bind-to-show
+  "Establish a connection to the Novation Launchpad Mini, for managing
+  the given show.
+
+  Makes sure it identifies as a Launchpad Mini, then initializes the
+  display and starts the UI updater thread. Since SysEx messages are
+  required for communicating with it, if you are on a Mac, you must
+  install [CoreMIDI4J](https://github.com/DerekCook/CoreMidi4J) to
+  provide a working implementation. (If you need to work with Java
+  1.6, you can instead
+  use [mmj](http://www.humatic.de/htools/mmj.htm), but that is no
+  longer developed, and does not support connecting or disconnecting
+  MIDI devices after Java has started.)
+
+  If you have more than one Launchpad Mini connected, or have renamed
+  how it appears in your list of MIDI devices, you need to supply a
+  value after `:device-filter` which identifies the ports to be used
+  to communicate with the Launchpad you want this function to use. The
+  values returned by [[afterglow.midi/open-inputs-if-needed!]]
+  and [[afterglow.midi/open-outputs-if-needed!]] will be searched, and
+  the first port that matches with [[filter-devices]] will be used.
+
+  The controller will be identified in the user interface (for the
+  purposes of linking it to the web cue grid) as \"Launchpad Mini\".
+  If you would like to use a different name (for example, if have more
+  than one Launchpad), you can pass in a custom value after
+  `:display-name`.
+
+  If you want the user interface to be refreshed at a different rate
+  than the default of fifteen times per second, pass your desired
+  number of milliseconds after `:refresh-interval`.
+
+  If you would like to skip the startup animation (for example because
+  the device has just powered on and run its own animation), pass
+  `true` after `:skip-animation`."
+  [show & {:keys [device-filter refresh-interval display-name skip-animation]
+           :or   {device-filter    "Mini"
+                  refresh-interval (/ 1000 15)
+                  display-name     "Launchpad Mini"}}]
+  {:pre [(some? show)]}
+  (let [port-in  (first (amidi/filter-devices device-filter (amidi/open-inputs-if-needed!)))
+        port-out (first (amidi/filter-devices device-filter (amidi/open-outputs-if-needed!)))]
+    (if (and (every? some? [port-in port-out]) (valid-identity port-in port-out))
+      (let [shift-mode (atom false)
+            controller
+            (with-meta
+              {:id                   (swap! controller-counter inc)
+               :display-name         display-name
+               :show                 show
+               :origin               (atom [0 0])
+               :refresh-interval     refresh-interval
+               :port-in              port-in
+               :port-out             port-out
+               :task                 (atom nil)
+               :last-control-buttons (atom {})
+               :next-control-buttons (atom {})
+               :last-grid-pads       (atom nil)
+               :next-grid-pads       (atom nil)
+               :shift-mode           shift-mode
+               :stop-mode            (atom false)
+               :midi-handler         (atom nil)
+               :tempo-tap-handler    (tempo/create-show-tempo-tap-handler show :shift-fn (fn [] @shift-mode))
+               :last-marker          (atom nil)
+               :overlays             (controllers/create-overlay-state)
+               :move-listeners       (atom #{})
+               :grid-controller-impl (atom nil)}
+              {:type :launchpad-mini})]
+        (reset! (:midi-handler controller) (partial midi-received controller))
+        (reset! (:grid-controller-impl controller)
+                (reify controllers/IGridController
+                  (display-name [this] (:display-name controller))
+                  (physical-height [this] 8)
+                  (physical-width [this] 8)
+                  (current-bottom [this] (@(:origin controller) 1))
+                  (current-bottom [this y] (move-origin controller (assoc @(:origin controller) 1 y)))
+                  (current-left [this] (@(:origin controller) 0))
+                  (current-left [this x] (move-origin controller (assoc @(:origin controller) 0 x)))
+                  (add-move-listener [this f] (swap! (:move-listeners controller) conj f))
+                  (remove-move-listener [this f] (swap! (:move-listeners controller) disj f))))
+        (clear-interface controller)
+        #_(if skip-animation
+          (start-interface controller)
+          (welcome-animation controller))
+        (swap! active-bindings assoc (:id controller) controller)
+        (show/register-grid-controller @(:grid-controller-impl controller))
+        (amidi/add-disconnected-device-handler! port-in #(deactivate controller :disconnected true))
+        controller)
+      (timbre/error "Unable to find Launchpad Mini" (amidi/describe-device-filter device-filter)))))
+
+(defn auto-bind
+  "Watches for a Novation Launchpad Mini controller to be connected,
+  and as soon as it is, binds it to the specified show
+  using [[bind-to-show]]. If that controller ever gets disconnected,
+  it will be re-bound once it reappears. Returns a watcher structure
+  which can be passed to [[deactivate]] if you would like to stop it
+  watching for reconnections. The underlying controller mapping, once
+  bound, can be accessed through the watcher's `:controller` key.
+
+  If you have more than one Launchpad Mini that might beconnected, or
+  have renamed how it appears in your list of MIDI devices, you need
+  to supply a value after `:device-filter` which identifies the ports
+  to be used to communicate with the Launchpad you want this function
+  to use. The values returned
+  by [[afterglow.midi/open-inputs-if-needed!]]
+  and [[afterglow.midi/open-outputs-if-needed!]] will be searched, and
+  the first port that matches using [[filter-devices]] will be used.
+
+  Once bound, the controller will be identified in the user
+  interface (for the purposes of linking it to the web cue grid) as
+  \"Launchpad Pro\". If you would like to use a different name (for
+  example, if you have more than one Launchpad), you can pass in a
+  custom value after `:display-name`.
+
+  If you want the user interface to be refreshed at a different rate
+  than the default of fifteen times per second, pass your desired
+  number of milliseconds after `:refresh-interval`."
+  [show & {:keys [device-filter refresh-interval display-name]
+           :or {device-filter "Mini"
+                refresh-interval (/ 1000 15)
+                display-name "Launchpad Mini"}}]
+  {:pre [(some? show)]}
+  (let [idle (atom true)
+        controller (atom nil)]
+    (letfn [(disconnection-handler []
+              (reset! controller nil)
+              (reset! idle true))
+            (connection-handler
+              ([device]
+               (connection-handler device true))
+              ([device wait-for-boot]
+               (when (compare-and-set! idle true false)
+                 (if (and (nil? @controller) (seq (amidi/filter-devices device-filter [device])))
+                   (let [port-in (first (amidi/filter-devices device-filter (amidi/open-inputs-if-needed!)))
+                         port-out (first (amidi/filter-devices device-filter (amidi/open-outputs-if-needed!)))]
+                     (when (every? some? [port-in port-out])  ; We found our Launchpad! Bind to it in the background.
+                       (timbre/info "Auto-binding to Launchpad Mini" device)
+                       (future
+                         (when wait-for-boot (Thread/sleep 3000)) ; Allow for firmware's own welcome animation
+                         (reset! controller (bind-to-show show :device-filter device-filter
+                                                          :refresh-interval refresh-interval
+                                                          :display-name display-name
+                                                          :skip-animation wait-for-boot))
+                         (amidi/add-disconnected-device-handler! (:port-in @controller) disconnection-handler))))
+                   (reset! idle true)))))
+            (cancel-handler []
+              (amidi/remove-new-device-handler! connection-handler)
+              (when-let [device (:port-in @controller)]
+                (amidi/remove-disconnected-device-handler! device disconnection-handler)))]
+
+      ;; See if our Launchpad seems to already be connected, and if so, bind to it right away.
+      (when-let [found (first (amidi/filter-devices device-filter (amidi/open-inputs-if-needed!)))]
+        (connection-handler found false))
+
+      ;; Set up to bind when connected in future.
+      (amidi/add-new-device-handler! connection-handler)
+
+      ;; Return a watcher object which can provide access to the bound controller, and be canceled later.
+      (with-meta
+        {:controller controller
+         :device-filter device-filter
+         :cancel cancel-handler}
+        {:type :launchpad-mini-watcher}))))

  synchronized and a beat is not taking place."
  0x1c)


(defn- valid-identity
  "Checks that the device we are trying to bind to reports the proper

  "The color of buttons that are completely off."
  0x0c)

(def button-dimmed-color
  "The color of buttons that can be pressed but are in conflict or
  otherwise backgrounded."
  0x1d)

(def button-available-color
  "The color of buttons that can be pressed but haven't yet been."
  0x2e)

(def button-active-color
  "The color of an available button that is currently being pressed."
