Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TCP Client #23

Closed
PouleR opened this issue Mar 11, 2017 · 8 comments
Closed

TCP Client #23

PouleR opened this issue Mar 11, 2017 · 8 comments

Comments

@PouleR
Copy link

PouleR commented Mar 11, 2017

Hi,

Is it possible to create a TCP client in the Global Setup Expression which connects to an IP address:port on start up and send TCP packets from (for e.g.) a Beat Expression ?

I've read the user guide, which only uses the UDP protocol, since I'm not completely familiar with Clojure I'm searching for some examples on how to create a TCP client and use some kind of 'write command' from a Beat Expression.

I used an example from this website: https://nakkaya.com/2010/02/10/a-simple-clojure-irc-client/, but this throws an exception with the error 'Unable to resolve classname: Socket'.

Really like this project and I'm planning to integrate this with the Quick DMX software: http://www.highlite.nl/nl/Shop/Products/Entertainment-Lighting/Lightcontrollers/Software-Controllers/Quick-DMX-D512

@brunchboy
Copy link
Member

brunchboy commented Mar 11, 2017

Hello! As I mentioned on twitter, this should be very possible. I suspect I’m roughly eight hours behind you in time zones, and I have some chores that I must finish while businesses are still open near home, so it might be rather late tonight before you see anything detailed from me, but I am very happy to help with this. It will be good to have a TCP example in the documentation as well.

If you want to try something in the mean time, a likely problem you ran into is that you need to use a slightly different approach to import Java class declarations inside a beat-link-trigger Expression than you would use in an ordinary Clojure source file. You might want to try something like this at the start of your Global Setup Expression (instead of the ns form version) if you want to follow that example:

(import '(java.net Socket)
        '(java.io PrintWriter InputStreamReader BufferedReader))

@brunchboy
Copy link
Member

And I am delighted you have found this and are excited about it! 😁

@brunchboy
Copy link
Member

I tried looking through the Quick DMX documentation and downloads, and did not quickly find anything about supporting TCP connections. If you could point me at the details of the TCP protocol that you want to communicate with, that would help me put together some sample code. There is an example of using TCP connections from Clojure in the beat-link-trigger source itself, in the carabiner namespace—this communicates with a simple C++ daemon I wrote to integrate with Ableton Link—but it contains many distracting details which will not be relevant to your specific goals, so I’d be happy to help by writing something more focused for you.

Alternately, if you are willing to share the Clojure code you have attempted so far, I could help adjust it to get past the problems you are finding. Feel free to contact me via email for that, if you would prefer.

@brunchboy
Copy link
Member

Looking more closely at the “simple Clojure ICR client” example you found, I see that it has more than a few flaws and distracting irrelevant details as well. I look forward to learning more about exactly what you need to implement, and helping you do that in a clean and efficient way.

@brunchboy
Copy link
Member

brunchboy commented Mar 12, 2017

All right, based on the documentation you kindly sent me, I have a first pass at this working. Here is the code in the Global Setup Expression:

(defn live-response-handler
  "A loop that reads messages from ShowXPress Live and responds
  appropriately."
  []
  (try
    (loop [socket (get-in @globals [:live-connection :socket])]
      (when (and socket (not (.isClosed socket)))
        (let [buffer (byte-array 1024)
              input  (.getInputStream socket)
              n      (.read input buffer)]
          (when (pos? n)  ; We got data, so the socket has not yet been closed.
            (let [message (String. buffer 0 n "UTF-8")]
              (timbre/info "Received from ShowXPress Live:" message)
              (cond
                (= message "HELLO\r\n")
                (timbre/info "ShowXPress Live login successful.")

                (= message "BEAT_ON\r\n")
                (do (swap! globals assoc-in [:live-connection :beats-requested] true)
                    (timbre/info "Beat message request from ShowXPress Live recorded."))

                (= message "BEAT_OFF\r\n")
                (do (swap! globals assoc-in [:live-connection :beats-requested] false)
                    (timbre/info "Beat message request from ShowXPress Live removed."))

                (.startsWith message "ERROR")
                (timbre/warn "Error message from ShowXPress Live:" message)

                :else
                (timbre/info "Ignoring unrecognized ShowXPress message type.")))
            (recur (get-in @globals [:live-connection :socket]))))))
    (catch Throwable t
      (timbre/error t "Problem reading from ShowXPress Live, loop aborted."))))

(defn send-live-command
  "Sends a command message to ShowXPress Live."
  [message]
  (let [socket (get-in @globals [:live-connection :socket])]
    (if (and socket (not (.isClosed socket)))
      (.write (.getOutputStream socket) (.getBytes (str message "\r\n") "UTF-8"))
      (timbre/warn "Cannot write to ShowXPress Live, no open socket, discarding:" message))))

(defn set-live-tempo
  "Tells ShowXPress Live the current tempo if it is different than the
  value we last reported. Rounds to the nearest beat per minute
  because the protocol does not seem to accept any fractional values.
  The expected way to use this is to include the following in a
  trigger's Tracked Update Expression:

  `(when trigger-active? (set-live-tempo effective-tempo))`"
  [bpm]
  (let [bpm (Math/round bpm)]
    (when-not (= bpm (get-in @globals [:live-connection :bpm]))
      (send-live-command (str "BPM|"bpm))
      (swap! globals assoc-in [:live-connection :bpm] bpm)
      (timbre/info "ShowXPress Live tempo set to" bpm))))

(defn send-live-beat
  "Sends a beat command to ShowXPress Live if we have received a
  request to do so. The expected way to use this is to include the
  following in a trigger's Beat Expresssion:

  `(when trigger-active? (send-live-beat))`"
  []
  (when (get-in @globals [:live-connection :beats-requested])
    (send-live-command "BEAT")))

;; Attempt to connect to the Live external application port.
;; Edit the variable definitions below to reflect your setup.
(try
  (let [live-address    "127.0.0.1"
        live-port       7348
        live-password   "pw"
        connect-timeout 5000
        socket-address  (InetSocketAddress. live-address live-port)
        socket          (java.net.Socket.)]
    (.connect socket socket-address connect-timeout)
    (swap! globals assoc :live-connection {:socket socket})
    (future (live-response-handler))
    (send-live-command (str "HELLO|beat-link-trigger|" live-password)))
  (catch Exception e
    (timbre/error e "Unable to connect to ShowXPress Live")))

You will want to edit the values assigned to live-address, live-port, and live-password to match your setup. This code assumes that ShowXPress Live already running and configured to listen on the specified port before you launch beat-link-trigger. If nothing seems to be working, check the log file for error messages, and see if the login process was successful. Unfortunately, there is no friendly user interface to tell it to try again if it was not, but you can do so by editing the Global Setup Expression and saving it—even without making any changes, that will run both the shutdown and setup code again for you.

Then, here is the corresponding Global Shutdown Expression:

;; Disconnect from the Live external application port.
(when-let [socket (get-in @globals [:live-connection :socket])]
  (.close socket)
  (swap! globals dissoc :live-connection))

With these in place you can set up triggers which are enabled as appropriate by tracks loaded and beat numbers reached, and if you want these triggers to control the Live BPM when they are active, set their Tracked Update Expression to:

(when trigger-active? (set-live-tempo effective-tempo))

You may also want to set their Beat Expression to:

(when trigger-active? (send-live-beat))

That way, if Live has requested that we send BEAT messages on each beat, the triggers will do so when they are active. (But if it has not requested that, they will not.)

It is not entirely clear to me what the purpose of the BEAT messages is, so sending them might be redundant given that we are already sending BPM messages whenever the BPM value changes, rounded to the nearest integer, which is the most precision that the protocol seems to support.

Of course you will also want to be able to trigger cues when triggers activate, which is as simple as setting the trigger’s Activation Expression to something like:

(send-live-command "BUTTON_PRESS|Chill 3")

Where the name of the button is the value which follows the | character, (in this example, Chill 3). To have the button released when the trigger deactivates, as you might expect, you set the trigger’s Deactivation Expression to something like:

(send-live-command "BUTTON_RELEASE|Chill 3")

So please give this approach a try, and let me know how it works for you, and what might be missing. I would like it to undergo some real-world testing before I add it to the user guide.

@PouleR
Copy link
Author

PouleR commented Mar 14, 2017

The provided code for the Global Setup Expression is working great together with LightningController / Quick DMX / Sweetlight / ShowXPress Live. (I don't know why it has so many product names!)

I haven't tested it with real hardware yet, only with the simulation 3D view which is available in the application.

For example, an example for the Enabled Filter Expression:
(or (and (>= beat-number 35) (<= beat-number 36)) (and (>= beat-number 43) (<= beat-number 44)) (and (>= beat-number 51) (<= beat-number 66)) )

Activation Expression:
(send-live-command "BUTTON_PRESS|strobo_flash")

Deactivation Expression:
(send-live-command "BUTTON_RELEASE|strobo_flash")

And another example for a Beat Expression:
(when trigger-active? (send-live-command "BUTTON_PRESS|led_pars") (future (Thread/sleep (long (/ 30000 effective-tempo))) (send-live-command "BUTTON_RELEASE| led_pars")))

Some links to the application:
http://sweetlight-controller.com/download/
http://forum.thelightingcontroller.com

@brunchboy
Copy link
Member

Thanks, I am working on adding this to the user guide now. One small simplification that you can make for your Enabled Filter Expression: In Clojure, inequalities like the >= function can take more than two values, and as long as the values are all in the specified order, it evaluates to true. So your expression could become:

(or
  (>= 35 beat-number 36)
  (>= 43 beat-number 44)
  (>= 51 beat-number 66))

@brunchboy
Copy link
Member

All right, this has been incorporated in a new section, Chauvet ShowXpress Live (SweetLight, QuickDMX). Please let me know if I left out anything, or was inaccurate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants