arcenciel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/DEDICATION +1 -0
- data/Gemfile +2 -0
- data/README.md +116 -0
- data/arcenciel.gemspec +22 -0
- data/bin/arcenciel-demo +12 -0
- data/docs/images/hero.jpg +0 -0
- data/lib/arcenciel.rb +30 -0
- data/lib/arcenciel/manager.rb +187 -0
- data/lib/arcenciel/manager/device.rb +120 -0
- data/lib/arcenciel/manager/hub.rb +33 -0
- data/lib/arcenciel/surfaces.rb +109 -0
- data/lib/arcenciel/surfaces/controller.rb +68 -0
- data/lib/arcenciel/surfaces/knob.rb +211 -0
- data/lib/arcenciel/utility.rb +3 -0
- data/lib/arcenciel/utility/chaser.rb +93 -0
- data/lib/arcenciel/utility/dsl_base.rb +15 -0
- data/lib/arcenciel/utility/logging.rb +58 -0
- data/lib/arcenciel/version.rb +3 -0
- metadata +98 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/DEDICATION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
This project is dedicated to Ben Hughes.
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
## Arcenciel: Physical Knobs for Virtual Machines
|
2
|
+
|
3
|
+
![Hero](docs/images/hero.jpg)
|
4
|
+
|
5
|
+
*"Touch the cloud; feel its thunder beneath your fingertips."*
|
6
|
+
|
7
|
+
### Introduction
|
8
|
+
|
9
|
+
Arcenciel is a declarative microframework for the [Monome](http://monome.org/) Arc OSC controller.
|
10
|
+
|
11
|
+
All sufficiently complex systems have *phases* and *transitions*.
|
12
|
+
Discovering the boundary of each is challenging and often defies intuition.
|
13
|
+
Trajectories can be stable, metastable, or chaotic.
|
14
|
+
Small variations in the parameters of the system can manifest as large, unexpected effects.
|
15
|
+
How can we thoroughly and naturally explore the space of all states?
|
16
|
+
|
17
|
+
Consider this idea: Play your benchmarks like a *musical instrument*.
|
18
|
+
|
19
|
+
Here's an [inspiring example](https://vimeo.com/21596928) of the Arc's potential.
|
20
|
+
|
21
|
+
*Monome has discontinued all models of the Arc controller and the supply is therefore strictly limited.*
|
22
|
+
*Each unit is a stunningly beautiful masterpiece: a rare combination of technology and aesthetic joy.*
|
23
|
+
*If you're among the lucky few who own this extraordinary recherché, love and cherish it forever.*
|
24
|
+
*The Arc's value handily surpasses its weight in gold.*
|
25
|
+
|
26
|
+
### Dependencies
|
27
|
+
|
28
|
+
Arcenciel uses the OSC protocol (UDP).
|
29
|
+
SerialOSC interfaces each Arc with the host machine.
|
30
|
+
|
31
|
+
To get started on Mac OS X, you'll need to:
|
32
|
+
|
33
|
+
* Install the [FTDI virtual COM port (VCP) driver](http://www.ftdichip.com/Drivers/VCP.htm).
|
34
|
+
* Install the [SerialOSC server](https://github.com/monome/serialosc/releases/tag/1.2).
|
35
|
+
* Restart your computer.
|
36
|
+
|
37
|
+
Test your configuration by running the bundled demonstration script:
|
38
|
+
|
39
|
+
```
|
40
|
+
$ gem install arcenciel
|
41
|
+
$ arcenciel-demo
|
42
|
+
[ARC] Added device (m0000171; UDP 19930).
|
43
|
+
[ARC] Assigning control 'Arc' to device...
|
44
|
+
[ARC] Illuminated encoder 'First' (0). Press any key.
|
45
|
+
[ARC] Illuminated encoder 'Second' (1). Press any key.
|
46
|
+
[ARC] Assigned control 'Arc'.
|
47
|
+
```
|
48
|
+
|
49
|
+
### Usage
|
50
|
+
|
51
|
+
The Arc reacts to tactile interactions with its rotary encoders:
|
52
|
+
|
53
|
+
* Angular motion
|
54
|
+
* Threshold pressure
|
55
|
+
|
56
|
+
From events generated by the device, Arcenciel emulates *logical knobs*.
|
57
|
+
Each knob has a distinct configuration: a name, a value type, a range, and a precision (degrees per sweep).
|
58
|
+
When the value of a knob changes, the provided callback is invoked.
|
59
|
+
|
60
|
+
Arcenciel discovers all connected devices and assigns each to a logical controller (defining one or more knobs).
|
61
|
+
When a device is assigned to a controller, for each of its knobs, Arcenciel illuminates the rotary encoder's ring and requests that you to confirm the assignment.
|
62
|
+
|
63
|
+
Knobs are defined using these attributes:
|
64
|
+
|
65
|
+
* Name
|
66
|
+
* Initial value
|
67
|
+
* Minimum and maximum value (individually or as a range)
|
68
|
+
* Sweep (degrees of rotation for the entire range)
|
69
|
+
* Value type (integer or float)
|
70
|
+
|
71
|
+
### Example
|
72
|
+
|
73
|
+
Here's a sample that targets a single Arc (two-encoder version).
|
74
|
+
|
75
|
+
* Emulate two knobs: "Query rate" and "Rows scanned."
|
76
|
+
* Use a distinct range and sweep for each knob, with integer values.
|
77
|
+
* Invoke a distinct callback for each knob.
|
78
|
+
|
79
|
+
Implementation:
|
80
|
+
|
81
|
+
```
|
82
|
+
require 'arcenciel'
|
83
|
+
|
84
|
+
Arcenciel.run! do
|
85
|
+
knob do
|
86
|
+
name "Query rate"
|
87
|
+
|
88
|
+
min 0
|
89
|
+
max 100
|
90
|
+
type :integer
|
91
|
+
sweep 1440
|
92
|
+
|
93
|
+
on_value do |rate|
|
94
|
+
puts "A: #{name}: #{rate}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
knob do
|
99
|
+
name "Rows scanned"
|
100
|
+
|
101
|
+
min 0
|
102
|
+
max 10000
|
103
|
+
type :integer
|
104
|
+
sweep 360
|
105
|
+
|
106
|
+
on_value do |rows|
|
107
|
+
puts "B: #{name}: #{rows}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
### Special Thanks
|
114
|
+
|
115
|
+
* The Monome minimalists, for a solid concept and outstanding craftsmanship.
|
116
|
+
* The generous dudes who sold me their Arcs, without which this project would not be possible.
|
data/arcenciel.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
require 'arcenciel/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'arcenciel'
|
8
|
+
s.version = Arcenciel::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ['Nelson Gauthier']
|
11
|
+
s.email = ['nelson.gauthier@gmail.com']
|
12
|
+
s.homepage = 'https://github.com/nelgau/arcenciel'
|
13
|
+
s.summary = "Declarative Monome Arc microframework"
|
14
|
+
s.description = "Physical knobs for virtual machines"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.executables = ['arcenciel-demo']
|
18
|
+
s.require_path = 'lib'
|
19
|
+
|
20
|
+
s.add_runtime_dependency 'osc-ruby', '~> 1.1.1'
|
21
|
+
s.add_runtime_dependency 'colored', '>= 1.2.0'
|
22
|
+
end
|
data/bin/arcenciel-demo
ADDED
Binary file
|
data/lib/arcenciel.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'osc-ruby'
|
2
|
+
|
3
|
+
require 'arcenciel/utility'
|
4
|
+
require 'arcenciel/manager'
|
5
|
+
require 'arcenciel/surfaces'
|
6
|
+
|
7
|
+
module Arcenciel
|
8
|
+
|
9
|
+
# Lists all controllers.
|
10
|
+
def self.controllers
|
11
|
+
@controllers ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add a new controllers.
|
15
|
+
def self.add(&blk)
|
16
|
+
controllers << Surfaces::Controller.from_dsl(&blk)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Run the main event loop.
|
20
|
+
def self.run!(&blk)
|
21
|
+
add(&blk) if block_given?
|
22
|
+
Manager.run!(controllers)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set the controller lifecycle logger.
|
26
|
+
def self.logger=(logger)
|
27
|
+
Logging.logger = logger
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'arcenciel/manager/device'
|
4
|
+
require 'arcenciel/manager/hub'
|
5
|
+
|
6
|
+
module Arcenciel
|
7
|
+
class Manager
|
8
|
+
include Logging
|
9
|
+
|
10
|
+
attr_reader :controllers
|
11
|
+
attr_reader :devices
|
12
|
+
|
13
|
+
def self.run!(controllers)
|
14
|
+
new(controllers).run!
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(controllers)
|
18
|
+
@controllers = controllers
|
19
|
+
|
20
|
+
@hub = Hub.new
|
21
|
+
@mutex = Mutex.new
|
22
|
+
@shutdown = false
|
23
|
+
|
24
|
+
@id_map = {}
|
25
|
+
@port_map = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def run!
|
29
|
+
trap_signals
|
30
|
+
|
31
|
+
add_listeners
|
32
|
+
list_devices
|
33
|
+
begin_notify
|
34
|
+
|
35
|
+
start_hub
|
36
|
+
run_loop
|
37
|
+
ensure
|
38
|
+
clear_devices
|
39
|
+
end
|
40
|
+
|
41
|
+
def shutdown!
|
42
|
+
@shutdown = true
|
43
|
+
end
|
44
|
+
|
45
|
+
def shutdown?
|
46
|
+
!!@shutdown
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def trap_signals
|
52
|
+
Signal.trap('INT') { shutdown! }
|
53
|
+
Signal.trap('TERM') { shutdown! }
|
54
|
+
end
|
55
|
+
|
56
|
+
def run_loop
|
57
|
+
until shutdown?
|
58
|
+
assign_devices
|
59
|
+
sleep 0.1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def devices
|
64
|
+
@id_map.values
|
65
|
+
end
|
66
|
+
|
67
|
+
def assign_devices
|
68
|
+
devices.each do |device|
|
69
|
+
next if device.attached?
|
70
|
+
begin
|
71
|
+
if controller = controllers.first(&:assigned?)
|
72
|
+
controller.assign!(device)
|
73
|
+
end
|
74
|
+
rescue Device::InvalidDeviceError
|
75
|
+
controller.unassign!
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def clear_devices
|
81
|
+
devices.each do |device|
|
82
|
+
begin
|
83
|
+
device.ring_clear_all
|
84
|
+
rescue
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def start_hub
|
90
|
+
@hub.run!
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_listeners
|
94
|
+
listen('/serialosc/add', :process_add)
|
95
|
+
listen('/serialosc/remove', :process_remove)
|
96
|
+
listen('/serialosc/device', :process_device)
|
97
|
+
listen('/arc/enc/delta', :process_delta)
|
98
|
+
listen('/arc/enc/key', :process_key)
|
99
|
+
end
|
100
|
+
|
101
|
+
def listen(path, name)
|
102
|
+
@hub.listen(path, &method(name))
|
103
|
+
end
|
104
|
+
|
105
|
+
def list_devices
|
106
|
+
@hub.send('/serialosc/list')
|
107
|
+
end
|
108
|
+
|
109
|
+
def begin_notify
|
110
|
+
@hub.send('/serialosc/notify')
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_device(device)
|
114
|
+
return if @id_map.include?(device.id)
|
115
|
+
|
116
|
+
@id_map[device.id] = device
|
117
|
+
@port_map[device.port] = device
|
118
|
+
device.start!(@hub.server_port)
|
119
|
+
end
|
120
|
+
|
121
|
+
def remove_device(id)
|
122
|
+
if device = @id_map.delete(id)
|
123
|
+
@port_map.delete(device.port)
|
124
|
+
device.stop!
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def dispatch_delta(port, index, delta)
|
129
|
+
if device = @port_map[port]
|
130
|
+
device.on_delta(index, delta)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def dispatch_key(port, index, state)
|
135
|
+
if device = @port_map[port]
|
136
|
+
device.on_key(index, state)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def process_add(msg)
|
141
|
+
list_devices
|
142
|
+
begin_notify
|
143
|
+
end
|
144
|
+
|
145
|
+
def process_remove(msg)
|
146
|
+
id = msg.to_a[0]
|
147
|
+
|
148
|
+
@mutex.synchronize do
|
149
|
+
remove_device(id)
|
150
|
+
begin_notify
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def process_device(msg)
|
155
|
+
id, type, port = msg.to_a
|
156
|
+
device = Device.new(id, type, port)
|
157
|
+
|
158
|
+
if device.arc?
|
159
|
+
@mutex.synchronize do
|
160
|
+
add_device(device)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def process_delta(msg)
|
166
|
+
port = msg.ip_port
|
167
|
+
args = msg.to_a
|
168
|
+
index = args[0].to_i
|
169
|
+
delta = args[1].to_i
|
170
|
+
|
171
|
+
@mutex.synchronize do
|
172
|
+
dispatch_delta(port, index, delta)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def process_key(msg)
|
177
|
+
port = msg.ip_port
|
178
|
+
args = msg.to_a
|
179
|
+
index = args[0].to_i
|
180
|
+
state = args[1].to_i
|
181
|
+
|
182
|
+
@mutex.synchronize do
|
183
|
+
dispatch_key(port, index, state)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Arcenciel
|
2
|
+
class Device
|
3
|
+
class InvalidDeviceError < StandardError; end
|
4
|
+
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
attr_reader :id
|
8
|
+
attr_reader :type
|
9
|
+
attr_reader :port
|
10
|
+
attr_reader :size
|
11
|
+
|
12
|
+
attr_reader :controller
|
13
|
+
|
14
|
+
def initialize(id, type, port)
|
15
|
+
@id = id
|
16
|
+
@type = type
|
17
|
+
@port = port
|
18
|
+
@is_arc, @size = parse_type(type)
|
19
|
+
|
20
|
+
@valid = false
|
21
|
+
@controller = nil
|
22
|
+
|
23
|
+
@client = OSC::Client.new('localhost', port)
|
24
|
+
end
|
25
|
+
|
26
|
+
def arc?
|
27
|
+
!!@is_arc
|
28
|
+
end
|
29
|
+
|
30
|
+
def attached?
|
31
|
+
!!@controller
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid?
|
35
|
+
!!@valid
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate!
|
39
|
+
raise InvalidDeviceError unless valid?
|
40
|
+
end
|
41
|
+
|
42
|
+
def start!(server_port)
|
43
|
+
set_destination(server_port)
|
44
|
+
@valid = true
|
45
|
+
ring_clear_all
|
46
|
+
|
47
|
+
log_info "Added device (#{id}; UDP #{port})."
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop!
|
51
|
+
unassign_controller!
|
52
|
+
@valid = false
|
53
|
+
|
54
|
+
log_warn "Removed device (#{id}; UDP #{port})."
|
55
|
+
end
|
56
|
+
|
57
|
+
def attach!(controller)
|
58
|
+
validate!
|
59
|
+
@controller = controller
|
60
|
+
end
|
61
|
+
|
62
|
+
def ring_clear_all
|
63
|
+
validate!
|
64
|
+
(0...size).each do |i|
|
65
|
+
ring_clear(i)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def ring_clear(index)
|
70
|
+
validate!
|
71
|
+
ring_all(index, 0)
|
72
|
+
end
|
73
|
+
|
74
|
+
def ring_set(index, x, level)
|
75
|
+
validate!
|
76
|
+
@client.send(OSC::Message.new('/arc/ring/set', index, x, level))
|
77
|
+
end
|
78
|
+
|
79
|
+
def ring_all(index, level)
|
80
|
+
validate!
|
81
|
+
@client.send(OSC::Message.new('/arc/ring/all', index, level))
|
82
|
+
end
|
83
|
+
|
84
|
+
def ring_map(index, array)
|
85
|
+
validate!
|
86
|
+
@client.send(OSC::Message.new('/arc/ring/map', index, *array))
|
87
|
+
end
|
88
|
+
|
89
|
+
def ring_range(index, x1, x2, level)
|
90
|
+
validate!
|
91
|
+
@client.send(OSC::Message.new('/arc/ring/range', index, x1, x2, level))
|
92
|
+
end
|
93
|
+
|
94
|
+
def on_delta(index, delta)
|
95
|
+
@controller && @controller.on_delta(index, delta)
|
96
|
+
end
|
97
|
+
|
98
|
+
def on_key(index, state)
|
99
|
+
@controller && @controller.on_key(index, state)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def set_destination(port)
|
105
|
+
@client.send(OSC::Message.new('/sys/prefix', 'arc'))
|
106
|
+
@client.send(OSC::Message.new('/sys/host', 'localhost'))
|
107
|
+
@client.send(OSC::Message.new('/sys/port', port))
|
108
|
+
end
|
109
|
+
|
110
|
+
def unassign_controller!
|
111
|
+
@controller && @controller.unassign!
|
112
|
+
@controller = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse_type(type)
|
116
|
+
m = type.match(/monome arc (\d+)/)
|
117
|
+
m ? [true , m[1].to_i] : [false, nil]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Arcenciel
|
4
|
+
class Hub
|
5
|
+
attr_reader :serial_port
|
6
|
+
attr_reader :server_port
|
7
|
+
|
8
|
+
attr_reader :client
|
9
|
+
attr_reader :server
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@serial_port = 12002
|
13
|
+
@server_port = 10210
|
14
|
+
|
15
|
+
@client = OSC::Client.new('localhost', serial_port)
|
16
|
+
@server = OSC::Server.new(server_port)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run!
|
20
|
+
Thread.start do
|
21
|
+
server.run
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def send(command)
|
26
|
+
client.send(OSC::Message.new(command, 'localhost', server_port))
|
27
|
+
end
|
28
|
+
|
29
|
+
def listen(path, &blk)
|
30
|
+
server.add_method(path, &blk)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'arcenciel/surfaces/controller'
|
2
|
+
require 'arcenciel/surfaces/knob'
|
3
|
+
|
4
|
+
module Arcenciel
|
5
|
+
module Surfaces
|
6
|
+
|
7
|
+
class Controller
|
8
|
+
|
9
|
+
class DSL < DSLBase
|
10
|
+
|
11
|
+
# Set the name of this logical controller.
|
12
|
+
def name(name)
|
13
|
+
opts[:name] = name
|
14
|
+
end
|
15
|
+
|
16
|
+
# Add a new logical knob (encoder) to the controller.
|
17
|
+
def knob(&blk)
|
18
|
+
opts[:knobs] ||= []
|
19
|
+
opts[:knobs] << Knob.from_dsl(&blk)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class Knob
|
27
|
+
|
28
|
+
class DSL < DSLBase
|
29
|
+
|
30
|
+
# Set the name of this logical knob (encoder).
|
31
|
+
def name(name)
|
32
|
+
opts[:name] = name
|
33
|
+
end
|
34
|
+
|
35
|
+
# Set the initial value of the knob.
|
36
|
+
# Default - Minimum value
|
37
|
+
def initial(value)
|
38
|
+
opts[:initial] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
# Set the range of values for the knob.
|
42
|
+
def range(range)
|
43
|
+
opts[:min] = range.begin
|
44
|
+
opts[:max] = range.end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the minimum value of the knob.
|
48
|
+
# Default - 0
|
49
|
+
def min(value)
|
50
|
+
opts[:min] = value
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the maximum value of the knob.
|
54
|
+
# Default - 100
|
55
|
+
def max(value)
|
56
|
+
opts[:max] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the type of value (:integer or :float).
|
60
|
+
# Default - :float
|
61
|
+
def type(type)
|
62
|
+
opts[:type] = type
|
63
|
+
end
|
64
|
+
|
65
|
+
# Set the precision of the knob (degrees per sweep).
|
66
|
+
# Default - 360 (one rotation per sweep)
|
67
|
+
def sweep(degrees)
|
68
|
+
opts[:sweep] = degrees
|
69
|
+
end
|
70
|
+
|
71
|
+
# Set the callback invoked when the value changes.
|
72
|
+
def on_value(&blk)
|
73
|
+
opts[:on_value] = blk
|
74
|
+
end
|
75
|
+
|
76
|
+
# Set the callback invoked when the knob is depressed.
|
77
|
+
def on_push(&blk)
|
78
|
+
opts[:on_push] = blk
|
79
|
+
end
|
80
|
+
|
81
|
+
# Set the callback invoked when the knob is released.
|
82
|
+
def on_release(&blk)
|
83
|
+
opts[:on_release] = blk
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
class Context
|
89
|
+
|
90
|
+
def initialize(knob)
|
91
|
+
@knob = knob
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the name of this logical knob (encoder).
|
95
|
+
def name
|
96
|
+
@knob.name
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the value of this knob.
|
100
|
+
def value
|
101
|
+
@knob.typed_value
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Arcenciel
|
2
|
+
module Surfaces
|
3
|
+
class Controller
|
4
|
+
include Logging
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :knobs
|
8
|
+
|
9
|
+
attr_reader :device
|
10
|
+
|
11
|
+
def self.from_dsl(&blk)
|
12
|
+
new(DSL.eval(&blk))
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
@name = options[:name] || default_name
|
17
|
+
@knobs = options[:knobs] || []
|
18
|
+
|
19
|
+
@device = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def assigned?
|
23
|
+
!!@device
|
24
|
+
end
|
25
|
+
|
26
|
+
def assign!(device)
|
27
|
+
log_info "Assigning controller '#{name}' to device..."
|
28
|
+
|
29
|
+
@device = device
|
30
|
+
knobs.each_with_index do |k, i|
|
31
|
+
k.assign!(device, i)
|
32
|
+
k.confirm!
|
33
|
+
end
|
34
|
+
|
35
|
+
device.attach!(self)
|
36
|
+
device.validate!
|
37
|
+
|
38
|
+
knobs.each do |k|
|
39
|
+
k.first_update!
|
40
|
+
end
|
41
|
+
|
42
|
+
log_info "Assigned controller '#{name}'."
|
43
|
+
end
|
44
|
+
|
45
|
+
def unassign!
|
46
|
+
@device = nil
|
47
|
+
knobs.each(&:unassign!)
|
48
|
+
log_warn "Controller '#{name}' is unassigned."
|
49
|
+
end
|
50
|
+
|
51
|
+
def on_delta(index, delta)
|
52
|
+
return unless index < knobs.size
|
53
|
+
knobs[index].on_delta(delta)
|
54
|
+
end
|
55
|
+
|
56
|
+
def on_key(index, state)
|
57
|
+
return unless index < knobs.size
|
58
|
+
knobs[index].on_key(state)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def default_name
|
64
|
+
'Arc'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
|
3
|
+
module Arcenciel
|
4
|
+
module Surfaces
|
5
|
+
class Knob
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {
|
9
|
+
min: 0,
|
10
|
+
max: 100,
|
11
|
+
sweep: 360,
|
12
|
+
type: :float
|
13
|
+
}
|
14
|
+
|
15
|
+
attr_reader :name
|
16
|
+
attr_reader :min
|
17
|
+
attr_reader :max
|
18
|
+
attr_reader :type
|
19
|
+
attr_reader :sweep
|
20
|
+
|
21
|
+
attr_reader :on_value
|
22
|
+
attr_reader :on_push
|
23
|
+
attr_reader :on_release
|
24
|
+
|
25
|
+
attr_reader :value
|
26
|
+
attr_reader :counter
|
27
|
+
attr_reader :precision
|
28
|
+
attr_reader :depressed
|
29
|
+
|
30
|
+
attr_reader :context
|
31
|
+
attr_reader :device
|
32
|
+
attr_reader :index
|
33
|
+
|
34
|
+
def self.from_dsl(&blk)
|
35
|
+
new(DSL.eval(&blk))
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(options)
|
39
|
+
options = DEFAULT_OPTIONS.merge(options)
|
40
|
+
|
41
|
+
@name = options[:name]
|
42
|
+
@min = options[:min]
|
43
|
+
@max = options[:max]
|
44
|
+
@type = options[:type]
|
45
|
+
@sweep = options[:sweep]
|
46
|
+
@value = options[:initial]
|
47
|
+
|
48
|
+
@on_value = options[:on_value]
|
49
|
+
@on_push = options[:on_push]
|
50
|
+
@on_release = options[:on_release]
|
51
|
+
|
52
|
+
@name ||= default_name
|
53
|
+
@value ||= @min
|
54
|
+
|
55
|
+
@precision = precision_for_sweep(sweep)
|
56
|
+
@counter = counter_for_value(@value)
|
57
|
+
@depressed = false
|
58
|
+
|
59
|
+
@context = Context.new(self)
|
60
|
+
@device = nil
|
61
|
+
@index = nil
|
62
|
+
|
63
|
+
clamp_counter
|
64
|
+
update_value
|
65
|
+
trigger_value
|
66
|
+
end
|
67
|
+
|
68
|
+
def typed_value
|
69
|
+
integer_type? ?
|
70
|
+
value.to_i :
|
71
|
+
value.to_f
|
72
|
+
end
|
73
|
+
|
74
|
+
def assigned?
|
75
|
+
!!@device
|
76
|
+
end
|
77
|
+
|
78
|
+
def assign!(device, index)
|
79
|
+
@device = device
|
80
|
+
@index = index
|
81
|
+
end
|
82
|
+
|
83
|
+
def unassign!
|
84
|
+
@device = nil
|
85
|
+
@index = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def confirm!
|
89
|
+
start_chaser
|
90
|
+
|
91
|
+
log_info "Illuminated knob '#{name}' (#{index}). Press any key."
|
92
|
+
wait_for_key
|
93
|
+
|
94
|
+
stop_chaser
|
95
|
+
ring_clear
|
96
|
+
end
|
97
|
+
|
98
|
+
def first_update!
|
99
|
+
update_ring
|
100
|
+
end
|
101
|
+
|
102
|
+
def on_delta(delta)
|
103
|
+
@counter += delta
|
104
|
+
clamp_counter
|
105
|
+
|
106
|
+
update_value
|
107
|
+
update_ring
|
108
|
+
trigger_value
|
109
|
+
end
|
110
|
+
|
111
|
+
def on_key(state)
|
112
|
+
case state
|
113
|
+
when 0
|
114
|
+
@depressed = false
|
115
|
+
trigger_release
|
116
|
+
when 1
|
117
|
+
@depressed = true
|
118
|
+
trigger_push
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def default_name
|
125
|
+
'Unnamed'
|
126
|
+
end
|
127
|
+
|
128
|
+
def integer_type?
|
129
|
+
type == :integer
|
130
|
+
end
|
131
|
+
|
132
|
+
def fraction_for_counter(x)
|
133
|
+
x / precision.to_f
|
134
|
+
end
|
135
|
+
|
136
|
+
def value_for_counter(x)
|
137
|
+
f = fraction_for_counter(x)
|
138
|
+
(max - min) * f + min
|
139
|
+
end
|
140
|
+
|
141
|
+
def precision_for_sweep(x)
|
142
|
+
(256 / 360.0 * x).to_i
|
143
|
+
end
|
144
|
+
|
145
|
+
def counter_for_value(x)
|
146
|
+
((x - min) / (max - min).to_f * precision.to_f).to_i
|
147
|
+
end
|
148
|
+
|
149
|
+
def clamp_counter
|
150
|
+
@counter = 0 if @counter < 0
|
151
|
+
@counter = precision if @counter > precision
|
152
|
+
end
|
153
|
+
|
154
|
+
def update_value
|
155
|
+
@value = value_for_counter(counter)
|
156
|
+
end
|
157
|
+
|
158
|
+
def update_ring
|
159
|
+
f = fraction_for_counter(counter)
|
160
|
+
ring_fraction(f)
|
161
|
+
end
|
162
|
+
|
163
|
+
def trigger_value
|
164
|
+
on_value &&
|
165
|
+
context.instance_exec(typed_value, &on_value)
|
166
|
+
end
|
167
|
+
|
168
|
+
def trigger_push
|
169
|
+
on_push &&
|
170
|
+
context.instance_exec(&on_push)
|
171
|
+
end
|
172
|
+
|
173
|
+
def trigger_release
|
174
|
+
on_release &&
|
175
|
+
context.instance_exec(&on_release)
|
176
|
+
end
|
177
|
+
|
178
|
+
def ring_clear
|
179
|
+
device.ring_clear(index)
|
180
|
+
end
|
181
|
+
|
182
|
+
def ring_fraction(x)
|
183
|
+
units = 64 * x
|
184
|
+
count = units.to_i
|
185
|
+
subpixel = units - units.to_i
|
186
|
+
final_level = (15 * subpixel).to_i
|
187
|
+
|
188
|
+
levels = []
|
189
|
+
(0...count).each { levels << 15 }
|
190
|
+
levels << final_level if levels.size < 64
|
191
|
+
(levels.size...64).each { levels << 0 }
|
192
|
+
|
193
|
+
device.ring_map(index, levels)
|
194
|
+
end
|
195
|
+
|
196
|
+
def start_chaser
|
197
|
+
@chaser = Chaser.new(device, index)
|
198
|
+
@chaser.start!
|
199
|
+
end
|
200
|
+
|
201
|
+
def stop_chaser
|
202
|
+
@chaser.stop!
|
203
|
+
@chaser = nil
|
204
|
+
end
|
205
|
+
|
206
|
+
def wait_for_key
|
207
|
+
$stdin.noecho(&:gets)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Arcenciel
|
4
|
+
class Chaser
|
5
|
+
MAX_LEVEL = 13
|
6
|
+
|
7
|
+
SEQUENCE = (1..5).to_a.reverse.map do |i|
|
8
|
+
(MAX_LEVEL * 1.0 / (i * i)).to_i
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :device
|
12
|
+
attr_reader :index
|
13
|
+
|
14
|
+
def self.position
|
15
|
+
@position ||= 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.advance
|
19
|
+
@position ||= 0
|
20
|
+
@position += 1
|
21
|
+
@position %= 64
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(device, index)
|
25
|
+
@device = device
|
26
|
+
@index = index
|
27
|
+
|
28
|
+
@running = false
|
29
|
+
@thread = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def running?
|
33
|
+
!!@running
|
34
|
+
end
|
35
|
+
|
36
|
+
def start!
|
37
|
+
return if running?
|
38
|
+
@running = true
|
39
|
+
ring_clear
|
40
|
+
run_async
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop!
|
44
|
+
return if !running?
|
45
|
+
@running = false
|
46
|
+
join_async
|
47
|
+
ring_clear
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def run_async
|
53
|
+
@thread = Thread.start do
|
54
|
+
run_loop
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def join_async
|
59
|
+
@thread.join
|
60
|
+
@thread = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def run_loop
|
64
|
+
while running?
|
65
|
+
advance
|
66
|
+
update
|
67
|
+
sleep 0.02
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def advance
|
72
|
+
self.class.advance
|
73
|
+
end
|
74
|
+
|
75
|
+
def update
|
76
|
+
pos = self.class.position
|
77
|
+
(0...4).each do |e|
|
78
|
+
SEQUENCE.each_with_index do |level, i|
|
79
|
+
x = (pos + 16 * e + i) % 64
|
80
|
+
ring_set(x, level)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def ring_clear
|
86
|
+
device.ring_clear(index)
|
87
|
+
end
|
88
|
+
|
89
|
+
def ring_set(x, level)
|
90
|
+
device.ring_set(index, x, level)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'colored'
|
3
|
+
|
4
|
+
module Arcenciel
|
5
|
+
module Logging
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_writer :logger
|
9
|
+
|
10
|
+
def logger
|
11
|
+
@logger ||= DEFAULT_LOGGER
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def logger
|
16
|
+
Logging.logger
|
17
|
+
end
|
18
|
+
|
19
|
+
def log_info(msg)
|
20
|
+
logger.info(msg)
|
21
|
+
end
|
22
|
+
|
23
|
+
def log_warn(msg)
|
24
|
+
logger.warn(msg)
|
25
|
+
end
|
26
|
+
|
27
|
+
def log_error(msg)
|
28
|
+
logger.error(msg)
|
29
|
+
end
|
30
|
+
|
31
|
+
class DefaultFormater
|
32
|
+
SEVERITY_CONFIG = {
|
33
|
+
'INFO' => ['ARC', :blue],
|
34
|
+
'WARN' => ['WARN', :red],
|
35
|
+
'ERROR' => ['ERROR', :red],
|
36
|
+
'FATAL' => ['FATAL', :red],
|
37
|
+
'DEBUG' => ['DEBUG', :blue]
|
38
|
+
}
|
39
|
+
|
40
|
+
def call(severity, time, progname, msg)
|
41
|
+
tag, color = SEVERITY_CONFIG[severity]
|
42
|
+
colored_tag = Colored.colorize(tag, foreground: color)
|
43
|
+
|
44
|
+
string = ''
|
45
|
+
string += "[#{progname}] " if progname
|
46
|
+
string += "[#{colored_tag}] "
|
47
|
+
string += msg
|
48
|
+
string += "\n"
|
49
|
+
|
50
|
+
string
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
DEFAULT_LOGGER = Logger.new($stdout).tap do |l|
|
55
|
+
l.formatter = DefaultFormater.new
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: arcenciel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nelson Gauthier
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-03-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: osc-ruby
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.1
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.1.1
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: colored
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.2.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.2.0
|
46
|
+
description: Physical knobs for virtual machines
|
47
|
+
email:
|
48
|
+
- nelson.gauthier@gmail.com
|
49
|
+
executables:
|
50
|
+
- arcenciel-demo
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- DEDICATION
|
56
|
+
- Gemfile
|
57
|
+
- README.md
|
58
|
+
- arcenciel.gemspec
|
59
|
+
- bin/arcenciel-demo
|
60
|
+
- docs/images/hero.jpg
|
61
|
+
- lib/arcenciel.rb
|
62
|
+
- lib/arcenciel/manager.rb
|
63
|
+
- lib/arcenciel/manager/device.rb
|
64
|
+
- lib/arcenciel/manager/hub.rb
|
65
|
+
- lib/arcenciel/surfaces.rb
|
66
|
+
- lib/arcenciel/surfaces/controller.rb
|
67
|
+
- lib/arcenciel/surfaces/knob.rb
|
68
|
+
- lib/arcenciel/utility.rb
|
69
|
+
- lib/arcenciel/utility/chaser.rb
|
70
|
+
- lib/arcenciel/utility/dsl_base.rb
|
71
|
+
- lib/arcenciel/utility/logging.rb
|
72
|
+
- lib/arcenciel/version.rb
|
73
|
+
homepage: https://github.com/nelgau/arcenciel
|
74
|
+
licenses: []
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 1.8.23
|
94
|
+
signing_key:
|
95
|
+
specification_version: 3
|
96
|
+
summary: Declarative Monome Arc microframework
|
97
|
+
test_files: []
|
98
|
+
has_rdoc:
|