A Clojure library for communicating with the Open Lighting Architecture
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
dev_resources/ola_clojure
dev_src/ola_clojure
doc
resources/proto
src/ola_clojure
.gitignore
LICENSE
README.md
project.clj

README.md

ola-clojure

Join the chat at https://gitter.im/brunchboy/afterglow

A Clojure library for communicating with the Open Lighting Architecture.

Protocol Buffers are used to efficiently communicate with the olad daemon via its RPC Service.

This project was extracted from Afterglow, so that other Clojure projects could communicate with OLA without having to pull in all of Afterglow.

License

Overview

ola-clojure uses the OLA RPC system to communicate with the Open Lighting Architecture. Its build process scans the Protobuf specification file that defines the OLA methods exported by olad, and uses a code generator to create Clojure functions which marshal maps you pass them into properly formatted Protocol Buffers, open the olad connection if necessary, wrap an RPC message telling olad what you want to do, and send it. When a response is received (or if the send attempt fails), a callback function that you supply will be invoked with either the response or a failure indication.

This means that in the most common configuration, of talking to olad on the default port 9010 on the local machine, you need to write almost no code. But you can also configure ola-clojure to talk to other ports and other machines, as described below.

Usage

  1. If you haven't already, Install OLA. (On the Mac I recommend using Homebrew which lets you simply brew install ola). Once you launch the olad server you can interact with its embedded web server, which is very helpful in seeing whether anything is working; you can even watch live DMX values changing.

  2. Set up a Clojure project using Leiningen or Boot.

  3. Add this project as a dependency: Clojars Project

  4. In the namespace from which you want to communinicate with olad, add this to the :require section of the ns form:

    [ola-clojure.ola-service :as ola]

And you are ready to invoke methods in olad. The methods which are exported by ola-service are parsed from the OlaServerService section of Ola.proto. The messages which the methods use as parameters are defined earlier in the file. Consider for example the GetUniverseInfo method:

// RPCs handled by the OLA Server
service OlaServerService {
  // ...
  rpc GetUniverseInfo (OptionalUniverseRequest) returns (UniverseInfoReply);
  // ...
}

The request message, OptionalUniverseRequest is specified earlier in the file:

// request info about a universe
message OptionalUniverseRequest {
  optional int32 universe = 1;
}

Armed with this information, and having properly required and aliased ola-service (if you are just experimenting interactively at the REPL, (require '[ola-clojure.ola-service :as ola]) at this point), we can obtain a list of universes from olad like so:

(ola/GetUniverseInfo #(clojure.pprint/pprint %))

The only required argument to the wrapper functions is a callback function that will be invoked with the results. In this case, we are passing an anonymous function which simply uses the standard Clojure pretty-printer to format the map we get back. You may have to hit Enter again to see the output, because connecting to olad and sending the RPC happens asynchronously, and the call to GetUniverseInfo returns immediately, as that process is just beginning. But once you do, you will see something like this:

{:response
 {:universe
  [{:universe 0,
    :name "Dummy Universe",
    :merge_mode :LTP,
    :input_port_count 0,
    :output_port_count 1,
    :rdm_devices 6,
    :output_ports
    [{:port_id 0,
      :priority_capability 0,
      :description "Dummy Port",
      :universe 0,
      :active true,
      :supports_rdm true}]}
   {:universe 1,
    :name "DMX USB Out 1",
    :merge_mode :LTP,
    :input_port_count 0,
    :output_port_count 1,
    :rdm_devices 0,
    :output_ports
    [{:port_id 0,
      :priority_capability 0,
      :description "Serial #: 02615032, firmware 4.8",
      :universe 1,
      :active true,
      :supports_rdm true}]}
   {:universe 2,
    :name "DMX USB Out 2",
    :merge_mode :LTP,
    :input_port_count 0,
    :output_port_count 1,
    :rdm_devices 0,
    :output_ports
    [{:port_id 1,
      :priority_capability 0,
      :description "Serial #: 02615032, firmware 4.8",
      :universe 2,
      :active true,
      :supports_rdm true}]}
   {:universe 3,
    :name "DMX USB In 1",
    :merge_mode :LTP,
    :input_port_count 1,
    :output_port_count 0,
    :rdm_devices 0,
    :input_ports
    [{:port_id 0,
      :priority_capability 1,
      :description "Serial #: 02615032, firmware 4.8",
      :universe 3,
      :active true,
      :priority_mode 1,
      :priority 100,
      :supports_rdm false}]}]}}

If you are sending a "fire and forget" message and you don't care about the result, you can pass nil for your callback function.

This is exactly what we would expect from looking at the specifications of the reply message:

message UniverseInfo {
  required int32 universe = 1;
  required string name = 2;
  required MergeMode merge_mode = 3;
  required int32 input_port_count = 4;
  required int32 output_port_count = 5;
  required int32 rdm_devices = 6;
  repeated PortInfo input_ports = 7;
  repeated PortInfo output_ports = 8;
}
 
message UniverseInfoReply {
  repeated UniverseInfo universe = 1;
}

In the response, the Protocol Buffer structures have all been expanded into ordinary Clojure data structures for convenient access.

As we saw in its specification above, the GetUniverseInfo takes an optional parameter identifying a specific universe of interest. To pass parameters using ola-clojure, you simply supply a normal Clojure map with keys and values corresponding to the message definitions within the Protobuf specification. In this case, the parameter is named universe and takes an integer, so the following variant gets information about a single universe:

(ola/GetUniverseInfo {:universe 1} #(clojure.pprint/pprint %))

As expected:

{:response
 {:universe
  [{:universe 1,
    :name "DMX USB Out 1",
    :merge_mode :LTP,
    :input_port_count 0,
    :output_port_count 1,
    :rdm_devices 0,
    :output_ports
    [{:port_id 0,
      :priority_capability 0,
      :description "Serial #: 02615032, firmware 4.8",
      :universe 1,
      :active true,
      :supports_rdm true}]}]}}

What happens if we ask for a universe that doesn't exist?

(ola/GetUniverseInfo {:universe 42} #(clojure.pprint/pprint %))

This call initially returns true just like the others, but yields different output:

{:failed "OLA RpcMessage failed: Universe doesn't exist"}

Notice that instead of a :response key, there is a :failed key containing a description of the problem. This is how you can tell that something went wrong. If there is an exception that provides more context to the problem, it will be present under the key :thrown.

In addition to the result map printed by our callback function, you will see a different line of output:

15-Aug-12 10:34:02 Alacrity.local WARN [ola-clojure.ola-client] -
                   OLA RpcMessage failed: Universe doesn't exist

This comes from the logging mechanism used by ola-clojure. See below for information about how that can be configured.

Other methods work in the same way, like GetPlugins:

(ola/GetPlugins #(clojure.pprint/pprint %))
{:response
 {:plugin
  [{:plugin_id 1, :name "Dummy", :active true, :enabled true}
   {:plugin_id 2, :name "ArtNet", :active true, :enabled true}
   {:plugin_id 3, :name "ShowNet", :active true, :enabled true}
   {:plugin_id 4, :name "ESP Net", :active true, :enabled true}
   {:plugin_id 5, :name "Serial USB", :active true, :enabled true}
   {:plugin_id 6, :name "Enttec Open DMX", :active true, :enabled true}
   {:plugin_id 7, :name "SandNet", :active false, :enabled false}
   {:plugin_id 8, :name "StageProfi", :active false, :enabled false}
   {:plugin_id 9, :name "Pathport", :active true, :enabled true}
   {:plugin_id 11, :name "E1.31 (sACN)", :active true, :enabled true}
   {:plugin_id 12, :name "USB", :active true, :enabled true}
   {:plugin_id 14, :name "OSC", :active true, :enabled true}
   {:plugin_id 16, :name "KiNET", :active true, :enabled true}
   {:plugin_id 17, :name "KarateLight", :active false, :enabled false}
   {:plugin_id 18,
    :name "Milford Instruments",
    :active false,
    :enabled false}
   {:plugin_id 19, :name "Renard", :active true, :enabled true}
   {:plugin_id 21,
    :name "Open Pixel Control",
    :active true,
    :enabled true}
   {:plugin_id 22, :name "GPIO", :active true, :enabled true}]}}

Or, extracting and just printing the most important part of the response to GetPluginDescription:

(ola/GetPluginDescription {:plugin_id 5}
                          #(print (get-in % [:response :description])))
Serial USB Plugin
----------------------------
 
This plugin supports DMX USB devices that emulate a serial port. This 
includes:
 - Arduino RGB Mixer
 - DMX-TRI & RDM-TRI
 - DMXking USB DMX512-A, Ultra DMX, Ultra DMX Pro
 - DMXter4 & mini DMXter
 - Enttec DMX USB Pro
 - Robe Universe Interface
 
See http://opendmx.net/index.php/USB_Protocol_Extensions for more info.
 
--- Config file : ola-usbserial.conf ---
 
device_dir = /dev
The directory to look for devices in.
 
device_prefix = ttyUSB
The prefix of filenames to consider as devices. Multiple keys are allowed.
 
ignore_device = /dev/ttyUSB
Ignore the device matching this string. Multiple keys are allowed.
 
pro_fps_limit = 190
The max frames per second to send to a Usb Pro or DMXKing device.
 
tri_use_raw_rdm = [true|false]
Bypass RDM handling in the {DMX,RDM}-TRI widgets.
 
ultra_fps_limit = 40
The max frames per second to send to a Ultra DMX Pro device.
 
uucp_lock_path = /var/lock
Path to check for UUCP Lock files.

Finally, as an example of a more interesting RPC you will likely want to call, here is the section of Afterglow's show namespace which sends an updated set of DMX control values to one of the show's universes:

(let [levels (get buffers universe)]
  (ola/UpdateDmxData {:universe universe :data (ByteString/copyFrom levels)}
                     response-handler))

In this example, universe contains the ID of a universe that the show is controlling, and levels is a Java byte array containing the desired DMX channel values for the universe. This invocation causes olad to send those values to whatever plugin and interface is controlling that universe.

These examples, combined with the Ola.proto specification which creates the wrapper functions, will hopefully enable you to figure out how to send whatever messages you need to olad.

Connection Configuration

If you need to talk to olad on a different port or address, you can do so by configuring the ola-client namespace which manages the connection on behalf of the RPC wrapper functions, before the connection is established:

(require '[ola-clojure.ola-client :as ola-client])
(reset! ola-client/olad-host "172.30.246.32")
(reset! ola-client/olad-port 9200)

If you do need to talk to an OLA server on a different machine, by changing the olad-host value, this means that the communication will be much slower than talking to a local process. Because of that, you will almost certainly want to tell ola-client to change from using the unbuffered channel that it normally uses to gather messages you want to send to the OLA server. This default channel will block if you ever try to send a second message while the first one is still being written to the network. To use a channel with a buffer, you call use-buffered-channel:

(ola-client/use-buffered-channel)

Again, this needs to be done before communication with the OLA server has begun. The default buffer size holds 32 messages, and if it fills up because the network is unable to keep up with the rate at which you are trying to send messages, older messages will be discarded. This is probably plenty large a buffer, and will not cause message loss under normal circumstances, but you can specify a different buffer size by giving an argument to use-buffered-channel:

(ola-client/use-buffered-channel 48)

The ola-client namespace also provides a shutdown function which you can call if you ever want to explicitly close the olad connection:

(ola-client/shutdown)

You can use this if you have changed your connection parameters after a connection was already established, and you want your new values to take effect.

There is also a start function to open the connection again, but there is no real need to call this, as the RPC wrapper functions will all call it if necessary.

Logging Configuration

Like Afterglow, ola-clojure uses the excellent Timbre logging framework. If you do nothing, log messages above the debug level will be written to the standard output. But you can configure it however you would like, as described in its documentation.

Status

This has been used without any changes since the beginning of the Afterglow project, although it was only recently separated into its own project, and cleaned up slightly in the process. More recently, support for buffered channels was added, to help people who want to talk to OLA from Windows, where it can't run as a local process.

Bugs

Although there are none known as of the time of this release, please log issues as you encounter them!

References

License

Deep Symmetry

Copyright © 2015 Deep Symmetry, LLC

Distributed under the Eclipse Public License 1.0, the same as Clojure. By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software. A copy of the license can be found in doc/epl-v10.html within this project.

The OLA RPC Protobuf specification files are distributed under the GNU Lesser General Public License, version 2.1.