Skip to content

Commit

Permalink
Build an early release.
Browse files Browse the repository at this point in the history
  • Loading branch information
brunchboy committed Apr 27, 2016
1 parent 4ffc1de commit 99f5949
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 61 deletions.
48 changes: 30 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,39 @@ a little bit about Clojure.
It is already able to watch for DJ Link traffic on all your network
interfaces, and tell you what devices have been noticed, and the local
and broadcast addresses you will want to use when creating a virtual
CDJ device to participate in that network. Here is an example of
trying that out by running Dysentery as an executable jar on my
network at home:
CDJ device to participate in that network.

Here is an example of trying that out by running Dysentery as an
executable jar on my network at home:

```
> java -jar target/dysentery.jar
> java -jar dysentery.jar
Looking for DJ Link devices...
Found:
CDJ-2000nexus /172.16.42.5
DJM-2000nexus /172.16.42.3
CDJ-2000nexus /172.16.42.4
DJM-2000nexus /172.16.42.5
CDJ-2000nexus /172.16.42.6
To communicate create a virtual CDJ with address /172.16.42.2 and MAC address 3c:15:c2:e7:08:6b
and use broadcast address /172.16.42.255
To communicate create a virtual CDJ with address /172.16.42.2,
MAC address 3c:15:c2:e7:08:6c, and use broadcast address /172.16.42.255
Type ^C to exit.
```

There is also preliminary code which uses that information to set up a
virtual CDJ that announces itself on the network and starts receiving
packets from the mixer and other players. That is not yet hooked up to
the command line, and we are just starting to decipher the information
we get back, but we are progressing rapidly. You can uncomment a line
in `core.clj` if you want to turn that on, and watch the packets which
are gathered by device number in the `state` atom within `vcdj.clj`.
It also creates a virtual CDJ to ask those devices to send status
updates, and opens windows tracking the packets it receives from
those devices. When a packet changes the value of one of the bytes
displayed, the background of that byte is drawn in blue, which
gradually fades back to black when the value is not changing. This
helps to identify what parts of the packet change when you do
something on the device being analyzed.

<img src="doc/assets/PacketWindow.png" width="800" alt="Packet Window">


To try this, download the latest `dysentery.jar` from the
[releases](https:/brunchboy/dysentery/releases) page, make
sure you have a recent Java environment installed, and run it as shown
above.

To build it yourself, and play with it interactively, you will need to
clone this repository and install [Leiningen](http://leiningen.org).
Expand All @@ -71,15 +81,15 @@ dysentery.core=>
At that point, you can evalute Clojure expressions:

```clojure
(find-devices)
(view/find-devices)
;; => Looking for DJ Link devices...
;; => Found:
;; => CDJ-2000nexus /172.16.42.5
;; => DJM-2000nexus /172.16.42.3
;; => CDJ-2000nexus /172.16.42.4
;; =>
;; => To communicate create a virtual CDJ with address /172.16.42.2 and MAC address 3c:15:c2:e7:08:6b
;; => and use broadcast address /172.16.42.255
;; => To communicate create a virtual CDJ with address /172.16.42.2,
;; => MAC address 3c:15:c2:e7:08:6b, and use broadcast address /172.16.42.255
nil
```

Expand All @@ -90,6 +100,8 @@ To build the executable jar:
Compiling dysentery.core
Compiling dysentery.finder
Compiling dysentery.util
Compiling dysentery.vcdj
Compiling dysentery.view
Created /Users/james/git/dysentery/target/dysentery-0.1.0-SNAPSHOT.jar
Created /Users/james/git/dysentery/target/dysentery.jar
```
Expand Down
Binary file added doc/assets/PacketWindow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject dysentery "0.1.0-SNAPSHOT"
(defproject dysentery "0.1.0"
:description "Exploring ways to participate in a Pioneer Pro DJ Link network"
:url "http:/brunchboy/dysentery"
:license {:name "Eclipse Public License"
Expand Down
47 changes: 8 additions & 39 deletions src/dysentery/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,16 @@
JAR application."
(:require [dysentery.finder :as finder]
[dysentery.util :as util]
[dysentery.vcdj :as vcdj])
[dysentery.vcdj :as vcdj]
[dysentery.view :as view])
(:gen-class))

(defn describe-devices
"Print a descrption of the DJ Link devices found, and how to
interact with them."
[devices]
(println "Found:")
(doseq [device devices]
(println " " (:name device) (.toString (:address device))))
(println)
(let [[interface address] (finder/find-interface-and-address-for-device (first devices))]
#_(vcdj/start interface address)
(print "To communicate create a virtual CDJ with address" (.toString (.getAddress address)))
(println " and MAC address" (clojure.string/join ":" (map (partial format "%02x")
(map util/unsign (.getHardwareAddress interface)))))
(println "and use broadcast address" (.toString (.getBroadcast address)))))

(defn find-devices
"Run a loop that waits a few seconds to see if any DJ Link devices
can be found on the network. If so, describe them and how to reach
them."
[]
(println "Looking for DJ Link devices...")
(finder/start-if-needed)
(Thread/sleep 2000)
(loop [devices (finder/current-dj-link-devices)
tries 3]
(if (seq devices)
(describe-devices devices)
(if (zero? tries)
(println "No DJ Link devices found; giving up.")
(do
(Thread/sleep 1000)
(recur (finder/current-dj-link-devices) (dec tries))))))
(finder/shut-down))

(defn -main
"The entry point when inovked as a jar from the command line. Will
eventually parse command line options, but for now test the device
and interface identification code."
eventually parse command line options, but for now bring up the
device packet analysis interface."
[& args]
(find-devices)
(System/exit 0))
(view/watch-devices)
(if (seq (finder/current-dj-link-devices))
(println "Type ^C to exit.")
(System/exit 0)))
34 changes: 31 additions & 3 deletions src/dysentery/vcdj.clj
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,41 @@
(timbre/warn e "Problem reading from DJ Link player socket, shutting down.")
(shut-down)))))

(defonce ^{:private true
:doc "Holds a set of functions to call whenever a packet
has been received on the incoming status port. The function will be
called with two arguments, the device number found in the packet,
and the vector of unsigned byte values corresponding to the packet
data."}
packet-listeners (atom #{}))

(defn- process-packet
"React to a packet that was sent to our player port."
[packet data]
;; For now just stash the most recent packet and data into our state.
(swap! state assoc :packet packet :data data)
(let [device-number (get data 33)]
(swap! state assoc-in [:device-data device-number] data)))
(swap! state assoc-in [:device-data device-number] data)
(when (seq @packet-listeners)
(doseq [listener @packet-listeners]
(try
(listener device-number data)
(catch Throwable t
(timbre/warn t "Problem calling device packet listener")))))))

(defn add-packet-listener
"Registers a function to be called whenever a packet is sent to the
incoming status port. The function will be called with two
arguments, the device number found in the packet, and the vector of
unsigned byte values corresponding to the packet data."
[listener]
(swap! packet-listeners conj listener))

(defn remove-packet-listener
"Stops calling a packet listener function that was registered with
[[add-packet-listener]]."
[listener]
(swap! packet-listeners disj listener))

(def keep-alive-interval
"How often, in milliseconds, we should send keep-alive packets to
Expand Down Expand Up @@ -112,12 +140,12 @@
:destination (.getBroadcast address)
:watcher (future (loop []
(let [packet (receive socket)
data (.getData packet)]
data (vec (map util/unsign (take (.getLength packet) (.getData packet))))]
(process-packet packet data))
(recur)))
:keep-alive (future (loop []
(send-keep-alive)
(Thread/sleep keep-alive-interval)
(send-keep-alive)
(recur)))))

(catch Exception e
Expand Down
156 changes: 156 additions & 0 deletions src/dysentery/view.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
(ns dysentery.view
"Provides a way to inspect DJ Link packets, watch for changing
values, and develop an understanding of what they mean."
(:require [dysentery.finder :as finder]
[dysentery.util :as util]
[dysentery.vcdj :as vcdj]
[clojure.math.numeric-tower :as math])
(:import [javax.swing JFrame JPanel JLabel SwingConstants]))

(def byte-font
"The font to use for rendering byte values."
(java.awt.Font. "Monospaced" java.awt.Font/PLAIN 14))

(def fade-steps
"The number of packets over which the background of a value fades
from blue back to black after it changes."
10)

(defonce ^{:private true
:doc "Holds a map of device numbers to the functions that
should be called to update the corresponding informational frame
when a packet is received from that device."}
packet-frames (atom {}))

(defn create-player-frame
"Creates a frame for displaying packets sent to the specified device
number, and returns a function to be called to update the frame when
a new packet is received for that device."
[device-number packet]
(let [frame (JFrame. (str "Player " device-number))
panel (JPanel.)
byte-labels (mapv #(JLabel. (format "%02x" %) SwingConstants/CENTER) packet)
freshness (int-array (count packet))]
(.setLayout panel nil)
(doseq [x (range 16)]
(let [label (JLabel. (format "%x" x) SwingConstants/CENTER)]
(.setFont label byte-font)
(.setForeground label java.awt.Color/yellow)
(.add panel label)
(.setBounds label (* (inc x) 25) 0 20 15)))
(doseq [y (range (inc (quot (dec (count packet)) 16)))]
(let [label (JLabel. (format "%x" y) SwingConstants/RIGHT)]
(.setFont label byte-font)
(.setForeground label java.awt.Color/yellow)
(.add panel label)
(.setBounds label 0 (* (inc y) 15) 20 15)))
(loop [labels byte-labels
index 0]
(when (seq labels)
(let [label (first labels)
x (rem index 16)
y (quot index 16)
left (* x 20)
top (* y 14)]
(.setFont label byte-font)
(.setForeground label java.awt.Color/white)
(.setBackground label java.awt.Color/black)
(.setOpaque label true)
(.add panel label)
(.setBounds label (* (inc x) 25) (* (inc y) 15) 20 15))
(recur (rest labels) (inc index))))

(.setBackground panel java.awt.Color/black)
(.setSize frame 440 250)
(let [location (.getLocation frame)
offset (* 20 (inc (count @packet-frames)))]
(.translate location offset offset)
(.setLocation frame location))
(.add frame panel)
(.setVisible frame true)
(fn [packet]
(if (nil? packet)
(do ; We were told to shut down
(.setVisible frame false)
(.dispose frame))
(dotimes [index (count packet)] ; We have a packet to update our state with
(let [label (get byte-labels index)
value (format "%02x" (get packet index))
level (aget freshness index)]
(when label
(if (.equals value (.getText label))
(when (pos? level) ; Fade out the background, the value has not changed
(.setBackground label (java.awt.Color. (int 0) (int 0)
(int (math/round (* 255 (/ (dec level) fade-steps))))))
(aset freshness index (dec level)))
(do ; The value has changed, update the label and set the background bright blue, setting up a fade
(.setText label value)
(.setBackground label (java.awt.Color/blue))
(aset freshness index fade-steps))))))))))

(defn- handle-device-packet
"Find and update or create the frame used to display packets from
the device."
[device-number packet]
(if-let [frame (get @packet-frames device-number)]
(frame packet)
(swap! packet-frames assoc device-number (create-player-frame device-number packet))))

(defn- start-watching-devices
"Once we have found some DJ-Link devices, set up a virtual CDJ to
receive packets from them, and register a packet listener that will
create or update windows to display those packets."
[devices]
(let [[interface address] (finder/find-interface-and-address-for-device (first devices))]
(vcdj/start interface address)
(vcdj/add-packet-listener handle-device-packet)))

(defn describe-devices
"Print a descrption of the DJ Link devices found, and how to
interact with them."
[devices]
(println "Found:")
(doseq [device devices]
(println " " (:name device) (.toString (:address device))))
(println)
(let [[interface address] (finder/find-interface-and-address-for-device (first devices))]
(println "To communicate create a virtual CDJ with address" (str (.toString (.getAddress address)) ","))
(print "MAC address" (clojure.string/join ":" (map (partial format "%02x")
(map util/unsign (.getHardwareAddress interface)))))
(println ", and use broadcast address" (.toString (.getBroadcast address)))))

(defn find-devices
"Run a loop that waits a few seconds to see if any DJ Link devices
can be found on the network. If so, describe them and how to reach
them."
[]
(println "Looking for DJ Link devices...")
(finder/start-if-needed)
(Thread/sleep 2000)
(loop [devices (finder/current-dj-link-devices)
tries 3]
(if (seq devices)
devices
(if (zero? tries)
(println "No DJ Link devices found; giving up.")
(do
(Thread/sleep 1000)
(recur (finder/current-dj-link-devices) (dec tries)))))))

(defn watch-devices
"Create windows that show packets coming from all DJ-Link devices on
the network, to help analyze them."
[]
(when-let [devices (seq (find-devices))]
(describe-devices devices)
(start-watching-devices devices)))

(defn stop-watching-devices
"Remove the packet listener, close the frames, and shut down the
virtual CDJ."
[]
(vcdj/remove-packet-listener handle-device-packet)
(vcdj/shut-down)
(doseq [frame (vals @packet-frames)]
(frame nil))
(reset! packet-frames {}))

0 comments on commit 99f5949

Please sign in to comment.