arcenciel 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+

|
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:
|