patch 0.4.13

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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +13 -0
  3. data/README.md +176 -0
  4. data/bin/patchrb +40 -0
  5. data/lib/patch/config.rb +124 -0
  6. data/lib/patch/em_patch.rb +47 -0
  7. data/lib/patch/hub.rb +68 -0
  8. data/lib/patch/io/midi/action.rb +42 -0
  9. data/lib/patch/io/midi/input.rb +110 -0
  10. data/lib/patch/io/midi/message.rb +112 -0
  11. data/lib/patch/io/midi/output.rb +58 -0
  12. data/lib/patch/io/midi.rb +45 -0
  13. data/lib/patch/io/module.rb +35 -0
  14. data/lib/patch/io/osc/action.rb +43 -0
  15. data/lib/patch/io/osc/client.rb +60 -0
  16. data/lib/patch/io/osc/message.rb +109 -0
  17. data/lib/patch/io/osc/server.rb +159 -0
  18. data/lib/patch/io/osc.rb +43 -0
  19. data/lib/patch/io/websocket/node.rb +103 -0
  20. data/lib/patch/io/websocket/socket.rb +103 -0
  21. data/lib/patch/io/websocket.rb +27 -0
  22. data/lib/patch/io.rb +15 -0
  23. data/lib/patch/log.rb +97 -0
  24. data/lib/patch/message.rb +67 -0
  25. data/lib/patch/node/container.rb +69 -0
  26. data/lib/patch/node/map.rb +71 -0
  27. data/lib/patch/node.rb +10 -0
  28. data/lib/patch/patch.rb +59 -0
  29. data/lib/patch/report.rb +132 -0
  30. data/lib/patch/thread.rb +19 -0
  31. data/lib/patch.rb +42 -0
  32. data/test/config/nodes.yml +16 -0
  33. data/test/config/patches.yml +41 -0
  34. data/test/config_test.rb +216 -0
  35. data/test/helper.rb +20 -0
  36. data/test/hub_test.rb +49 -0
  37. data/test/io/midi/action_test.rb +82 -0
  38. data/test/io/midi/input_test.rb +130 -0
  39. data/test/io/midi/message_test.rb +54 -0
  40. data/test/io/midi/output_test.rb +44 -0
  41. data/test/io/midi_test.rb +94 -0
  42. data/test/io/module_test.rb +21 -0
  43. data/test/io/osc/action_test.rb +76 -0
  44. data/test/io/osc/client_test.rb +49 -0
  45. data/test/io/osc/message_test.rb +53 -0
  46. data/test/io/osc/server_test.rb +116 -0
  47. data/test/io/osc_test.rb +111 -0
  48. data/test/io/websocket/node_test.rb +96 -0
  49. data/test/io/websocket_test.rb +37 -0
  50. data/test/js/logger.js +67 -0
  51. data/test/js/message.js +62 -0
  52. data/test/js/qunit-1.18.0.js +3828 -0
  53. data/test/js/qunit.css +291 -0
  54. data/test/js/test.html +15 -0
  55. data/test/js/websocket.js +12 -0
  56. data/test/log_test.rb +96 -0
  57. data/test/message_test.rb +109 -0
  58. data/test/node/container_test.rb +104 -0
  59. data/test/node/map_test.rb +50 -0
  60. data/test/node_test.rb +14 -0
  61. data/test/patch_test.rb +57 -0
  62. data/test/report_test.rb +37 -0
  63. metadata +320 -0
@@ -0,0 +1,103 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ module Websocket
6
+
7
+ class Node
8
+
9
+ attr_reader :id
10
+
11
+ # @param [Fixnum] id
12
+ # @param [String] host
13
+ # @param [Fixnum] port
14
+ # @param [Hash]
15
+ # @option properties [Log] :log
16
+ def initialize(id, host, port, options = {})
17
+ @config = {
18
+ :host => host,
19
+ :port => port
20
+ }
21
+ @id = id
22
+ @log = options[:log]
23
+ end
24
+
25
+ # Send a message over the socket
26
+ # @param [Patch::Patch] patch Context
27
+ # @param [Array<::Patch::Message>] messages A message or messages to send
28
+ # @return [String, nil] If a message was sent, its JSON string; otherwise nil
29
+ def puts(patch, messages)
30
+ if running?
31
+ unless (messages = [messages].flatten.compact).empty?
32
+ json = messages.to_json
33
+ @log.puts("Sending messages: #{json}") if @log
34
+ begin
35
+ @socket.puts(json)
36
+ rescue Exception => exception # failsafe
37
+ @log.exception(exception) if @log
38
+ ::Thread.main.raise(exception)
39
+ end
40
+ json
41
+ end
42
+ else
43
+ @log.puts("Warning: No connection") if @log
44
+ nil
45
+ end
46
+ end
47
+
48
+ # Disable the message listener
49
+ # @return [Boolean]
50
+ def disable(patch)
51
+ @socket.disable
52
+ end
53
+
54
+ # Listen for messages with the given patch context
55
+ # @param [Patch] patch
56
+ # @param [Proc] callback callback to fire when events are received
57
+ # @return [Boolean]
58
+ def listen(patch, &callback)
59
+ ensure_socket.on_message do |data|
60
+ handle_input(patch, data, &callback)
61
+ end
62
+ true
63
+ end
64
+
65
+ # Start the websocket
66
+ # @return [Boolean]
67
+ def socket
68
+ ensure_socket
69
+ end
70
+ alias_method :start, :socket
71
+
72
+ # Is the server active?
73
+ # @return [Boolean]
74
+ def active?
75
+ !@socket.nil? && @socket.active?
76
+ end
77
+ alias_method :running?, :active?
78
+
79
+ private
80
+
81
+ def ensure_socket
82
+ @socket ||= ::Patch::IO::Websocket::Socket.start(@config)
83
+ end
84
+
85
+ # Handle a received message
86
+ # @param [String] json_message A raw inputted JSON message
87
+ # @param [Proc] callback A callback to fire with the received message
88
+ # @return [Message]
89
+ def handle_input(patch, json_message, &callback)
90
+ message_hash = JSON.parse(json_message, :symbolize_names => true)
91
+ message = Message.new(message_hash)
92
+ @log.puts("Recieved message: #{message_hash.to_json}") if @log
93
+ yield(message) if block_given?
94
+ message
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,103 @@
1
+ module Patch
2
+
3
+ module IO
4
+
5
+ module Websocket
6
+
7
+ class Socket
8
+
9
+ def self.start(config)
10
+ socket = new
11
+ socket.start(config)
12
+ socket
13
+ end
14
+
15
+ def initialize
16
+ @onmessage = []
17
+ end
18
+
19
+ def puts(data)
20
+ @socket.send(data)
21
+ end
22
+
23
+ # @return [Boolean]
24
+ def disable
25
+ @socket.onmessage = nil
26
+ @onmessage.clear
27
+ true
28
+ end
29
+
30
+ # @param [Proc] callback callback to fire when events are received
31
+ # @return [Boolean]
32
+ def on_message(&callback)
33
+ if @socket.nil?
34
+ @onmessage << callback
35
+ else
36
+ @socket.onmessage { |data| yield(data) }
37
+ end
38
+ true
39
+ end
40
+
41
+ # Start the websocket
42
+ # @param [Hash] config
43
+ # @return [Boolean]
44
+ def start(config, &block)
45
+ EM::WebSocket.run(config) do |websocket|
46
+ ::Thread.current.abort_on_exception = true
47
+ begin
48
+ enable(websocket)
49
+ rescue Exception => exception
50
+ ::Thread.main.raise(exception)
51
+ end
52
+ end
53
+ true
54
+ end
55
+
56
+ # Is the socket active?
57
+ # @return [Boolean]
58
+ def active?
59
+ !@socket.nil?
60
+ end
61
+
62
+ private
63
+
64
+ # If callbacks were added before the socket was active, assign them to the socket event handler
65
+ def configure_message_callbacks
66
+ @onmessage.each do |callback|
67
+ on_message(&callback)
68
+ end
69
+ @onmessage.clear
70
+ end
71
+
72
+ # Enable this node after initializing an EM::Websocket
73
+ # @param [EM::Websocket] websocket
74
+ # @return [Boolean]
75
+ def enable(websocket)
76
+ @socket = websocket
77
+ configure
78
+ true
79
+ end
80
+
81
+ # Configure the server actions
82
+ # @return [Boolean]
83
+ def configure
84
+ @socket.onopen do |handshake|
85
+ puts "Connection open"
86
+ end
87
+
88
+ @socket.onclose do
89
+ puts "Connection closed"
90
+ end
91
+
92
+ configure_message_callbacks unless @onmessage.empty?
93
+
94
+ true
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,27 @@
1
+ require "patch/io/websocket/node"
2
+ require "patch/io/websocket/socket"
3
+
4
+ module Patch
5
+
6
+ module IO
7
+
8
+ # Websocket IO
9
+ module Websocket
10
+
11
+ # Key that will be used by Patch to identify the module
12
+ KEY = :websocket
13
+ extend self
14
+ ::Patch::IO::Module.add(self)
15
+
16
+ # Construct a websocket from a node config
17
+ # @param [Hash] config
18
+ # @param [Hash] options
19
+ # @param [Hash]
20
+ # @option properties [Log] :log
21
+ def new_from_config(config, options = {})
22
+ ::Patch::IO::Websocket::Node.new(config[:id], config[:host], config[:port], options)
23
+ end
24
+
25
+ end
26
+ end
27
+ end
data/lib/patch/io.rb ADDED
@@ -0,0 +1,15 @@
1
+ # Classes
2
+ require "patch/io/module"
3
+
4
+ # Implementations
5
+ require "patch/io/midi"
6
+ require "patch/io/osc"
7
+ require "patch/io/websocket"
8
+
9
+ module Patch
10
+
11
+ # Namespace for IO nodes
12
+ module IO
13
+ end
14
+
15
+ end
data/lib/patch/log.rb ADDED
@@ -0,0 +1,97 @@
1
+ module Patch
2
+
3
+ # Logging
4
+ class Log
5
+
6
+ # @param [IO] out
7
+ # @param [Hash] options
8
+ # @option options [Array<Symbol>] :show
9
+ def initialize(out, options = {})
10
+ @out = out
11
+ @start = Time.now
12
+ populate_level(options)
13
+ end
14
+
15
+ def path
16
+ @out.path
17
+ end
18
+
19
+ # The current time since startup
20
+ # @return [Time]
21
+ def time
22
+ Time.now - @start
23
+ end
24
+
25
+ # Output an info message
26
+ # @param [String] message
27
+ # @return [String]
28
+ def puts(message)
29
+ message = format(message, :type => :info)
30
+ @out.puts(message) if @info
31
+ message
32
+ end
33
+ alias_method :info, :puts
34
+
35
+ # Output an exception
36
+ # @param [String] exception
37
+ # @return [String]
38
+ def exception(exception)
39
+ if @exception
40
+ message = format(exception.message, :type => :exception)
41
+ @out.puts(message)
42
+ end
43
+ exception
44
+ end
45
+ alias_method :error, :exception
46
+
47
+ private
48
+
49
+ # Populate the level setting
50
+ # @param [Hash] options
51
+ # @return [Debug]
52
+ def populate_level(options = {})
53
+ if !options[:show].nil?
54
+ show = [options[:show]].flatten.compact
55
+ @exception = !(show & [:exception, :error]).empty?
56
+ @info = !(show & [:info, :message]).empty?
57
+ end
58
+ @exception = true if @exception.nil?
59
+ @info = true if @info.nil?
60
+ self
61
+ end
62
+
63
+ # Format a message for output
64
+ # @param [String] message
65
+ # @param [Hash] options
66
+ # @option options [Symbol] type
67
+ # @return [String]
68
+ def format(message, options = {})
69
+ {
70
+ :timestamp => time.seconds.round(2),
71
+ :caller => caller_method,
72
+ :message => message,
73
+ :type => options[:type]
74
+ }.to_json
75
+ end
76
+
77
+ # Get the caller method where a message originated
78
+ # @param [Fixnum] depth
79
+ # @return [String]
80
+ def caller_method(depth=1)
81
+ method = caller(depth+1).first
82
+ parse_caller(method)
83
+ end
84
+
85
+ # Parse the caller name
86
+ # @param [String] at
87
+ # @return [String]
88
+ def parse_caller(at)
89
+ if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
90
+ file = Regexp.last_match[1]
91
+ file.scan(/.+\/(\w+)\.rb/)[0][0]
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,67 @@
1
+ module Patch
2
+
3
+ # A generic controller message
4
+ class Message
5
+
6
+ attr_accessor :index, :patch_name, :value
7
+ attr_reader :time
8
+
9
+ # @param [Hash] properties
10
+ def initialize(properties = nil)
11
+ populate_from_properties(properties) unless properties.nil?
12
+ @time ||= Time.now
13
+ end
14
+
15
+ # Convert the message to a hash
16
+ # @return [Hash]
17
+ def to_h
18
+ properties = {
19
+ :index => @index,
20
+ :patch_name => @patch_name,
21
+ :timestamp => timestamp, #js format
22
+ :value => @value
23
+ }
24
+ properties.merge!(@other_properties) unless @other_properties.nil?
25
+ properties
26
+ end
27
+
28
+ # Convert the message to a JSON string
29
+ # @return [String]
30
+ def to_json(*args)
31
+ to_h.to_json(*args)
32
+ end
33
+
34
+ # Get the message time as a JS timestamp
35
+ # @return [Fixnum]
36
+ def timestamp
37
+ js_time = @time.to_f * 1000
38
+ js_time.to_i
39
+ end
40
+
41
+ private
42
+
43
+ # Populate this message from a hash of properties
44
+ # @param [Hash] properties
45
+ # @return [Hash]
46
+ def populate_from_properties(properties)
47
+ properties = properties.dup
48
+ @index = properties.delete(:index)
49
+ @patch_name = properties.delete(:patch_name)
50
+ @value = properties.delete(:value)
51
+ if !(timestamp = properties.delete(:timestamp)).nil?
52
+ @time = timestamp_to_time(timestamp)
53
+ end
54
+ @other_properties = properties
55
+ end
56
+
57
+ # Convert a JS timestamp to a Ruby time
58
+ # @param [String, Numeric] timestamp
59
+ # @return [Time]
60
+ def timestamp_to_time(timestamp)
61
+ js_time = timestamp.to_f / 1000
62
+ Time.at(js_time.to_i)
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,69 @@
1
+ module Patch
2
+
3
+ module Node
4
+
5
+ # A container for Patch::Node
6
+ class Container
7
+
8
+ include Enumerable
9
+ extend Forwardable
10
+
11
+ attr_reader :nodes
12
+ def_delegators :@nodes, :all?, :any?, :count, :empty?
13
+
14
+ # @param [Array<Object>] nodes
15
+ def initialize(nodes)
16
+ @threads = []
17
+ @nodes = nodes
18
+ end
19
+
20
+ def |(other)
21
+ @nodes | other.nodes
22
+ end
23
+
24
+ def each(&block)
25
+ @nodes.each(&block)
26
+ end
27
+
28
+ # Enable the nodes in this container
29
+ # @return [Boolean]
30
+ def enable
31
+ result = @nodes.map { |node| enable_node(node) }
32
+ result.any?
33
+ end
34
+
35
+ # Get the nodes of the given type
36
+ # @param [Symbol] :type The type of node (eg :midi)
37
+ # @return [Array<IO::MIDI, IO::OSC, IO::Websocket>]
38
+ def find_all_by_type(type)
39
+ if (mod = IO::Module.find_by_key(type)).nil?
40
+ []
41
+ else
42
+ @nodes.select { |node| node.class.name.match(/\A#{mod.name}/) }
43
+ end
44
+ end
45
+
46
+ # Find the node with the given id
47
+ # @param [Fixnum] id
48
+ # @return [IO::MIDI, IO::OSC, IO::Websocket]
49
+ def find_by_id(id)
50
+ @nodes.find { |node| node.id == id }
51
+ end
52
+
53
+ private
54
+
55
+ # Enable the given node
56
+ # @param [Patch::Node] node
57
+ # @return [Boolean]
58
+ def enable_node(node)
59
+ if node.respond_to?(:start) && !node.active?
60
+ @threads << ::Patch::Thread.new { node.start }
61
+ end
62
+ true
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
69
+ end
@@ -0,0 +1,71 @@
1
+ module Patch
2
+
3
+ module Node
4
+
5
+ # A map of connections between nodes for a given patch
6
+ class Map
7
+
8
+ attr_reader :from, :to
9
+
10
+ # @param [Array<Object>, NodeContainer, Object] from
11
+ # @param [Array<Object>, NodeContainer, Object] to
12
+ def initialize(from, to)
13
+ @from = to_node_container(from)
14
+ @to = to_node_container(to)
15
+ end
16
+
17
+ # Disable the map for the given patch context
18
+ # @return [Boolean]
19
+ def disable(patch)
20
+ result = @to.map do |to_node|
21
+ disabled = @from.map do |from_node|
22
+ from_node.disable(patch)
23
+ true
24
+ end
25
+ disabled.any?
26
+ end
27
+ result.any?
28
+ end
29
+
30
+ # Enable this map for the given nodes
31
+ # @param [::Patch::Patch] patch The patch context to enable the map in
32
+ # @return [Boolean] Whether nodes were enabled
33
+ def enable(patch)
34
+ result = @to.map do |to_node|
35
+ enabled = @from.map do |from_node|
36
+ from_node.listen(patch) do |messages|
37
+ to_node.puts(patch, messages)
38
+ end
39
+ true
40
+ end
41
+ enabled.any?
42
+ end
43
+ result.flatten.any?
44
+ end
45
+
46
+ # The nodes for this map, collected
47
+ # @return [NodeContainer]
48
+ def nodes
49
+ @from | @to
50
+ end
51
+
52
+ private
53
+
54
+ # Convert the given arg to a node container
55
+ # @param [Object] object
56
+ # @return [NodeContainer]
57
+ def to_node_container(object)
58
+ if !object.kind_of?(Array) || !object.kind_of?(Node::Container)
59
+ object = [object].flatten.compact
60
+ end
61
+ if object.kind_of?(Array)
62
+ object = Node::Container.new(object)
63
+ end
64
+ object
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
71
+ end
data/lib/patch/node.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "patch/node/container"
2
+ require "patch/node/map"
3
+
4
+ module Patch
5
+
6
+ # A network or hardware connection
7
+ module Node
8
+ end
9
+
10
+ end
@@ -0,0 +1,59 @@
1
+ module Patch
2
+
3
+ # A single patch consisting of a node mapping and actions
4
+ class Patch
5
+
6
+ attr_reader :actions, :maps, :name
7
+
8
+ # @param [Symbol, String] name
9
+ # @param [Array<Node::Map>, Node::Map] maps A node map or maps
10
+ # @param [Array<Hash>, Hash] actions An action or actions
11
+ def initialize(name, maps, actions)
12
+ @name = name
13
+ populate(maps, actions)
14
+ end
15
+
16
+ # Enable the given nodes to implement this patch
17
+ # @param [Node::Container] nodes
18
+ # @return [Boolean]
19
+ def enable
20
+ result = @maps.map { |map| map.enable(self) }
21
+ result.any?
22
+ end
23
+
24
+ private
25
+
26
+ # Populate the patch
27
+ # @param [Array<Hash, Node::Map>, Hash, Node::Map] maps
28
+ # @param [Array<Hash>, Hash] actions
29
+ # @return [Patch]
30
+ def populate(maps, actions)
31
+ populate_maps(maps)
32
+ populate_actions(actions)
33
+ self
34
+ end
35
+
36
+ # Populate the patch actions from various arg formats
37
+ # @param [Array<Hash>, Hash] actions
38
+ # @return [Array<Hash>]
39
+ def populate_actions(actions)
40
+ @actions = actions.kind_of?(Hash) ? [actions] : actions
41
+ end
42
+
43
+ # Populate the node maps from various arg formats
44
+ # @param [Array<Hash, Node::Map>, Hash, Node::Map] maps
45
+ # @return [Array<Node::Map>]
46
+ def populate_maps(maps)
47
+ maps = [maps] unless maps.kind_of?(Array)
48
+ maps = maps.map do |map|
49
+ if map.kind_of?(Hash)
50
+ Node::Map.new(map.keys.first, map.values.first)
51
+ else
52
+ map
53
+ end
54
+ end
55
+ @maps = maps.flatten.compact
56
+ end
57
+
58
+ end
59
+ end