oschii_ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bb0e8b35b66541a40236ea7a4595518d8a9d6727105c2543e74ca5d6fbb8443f
4
+ data.tar.gz: 1d9c121b90502130c86056f9818dca4b6a81fe9485734e257e9f261891bbe6f8
5
+ SHA512:
6
+ metadata.gz: 8b35574e7713cdc5bf13d0e9710894818bc48c72a7a86ade3c6b8688a4449548a2106b01adeb52ac99585f7356f195b4c90999029a0b2aca3c1c6f3d170fb0c0
7
+ data.tar.gz: 57a436a356063c353d9e8f5a5a18014fce3aa2016b689f50b7daef79cf3efcba1a643d6081fb5669a81e0ddcbb8230735575439e3d1db2dba915d8c13dadb106
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ ruby '2.7.7'
4
+
5
+ source 'http://rubygems.org'
6
+
7
+ gemspec
8
+
9
+ group :development do
10
+ gem 'make_menu'
11
+ gem 'byebug'
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,80 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ oschii_ruby (0.0.1)
5
+ activesupport (~> 7.1.2)
6
+ faye-websocket (~> 0.11.3)
7
+ osc-ruby (~> 1.1.4)
8
+ rest-client (~> 2.1.0)
9
+ rubyserial (~> 0.6.0)
10
+ webrick (~> 1.8.1)
11
+
12
+ GEM
13
+ remote: http://rubygems.org/
14
+ specs:
15
+ activesupport (7.1.2)
16
+ base64
17
+ bigdecimal
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ connection_pool (>= 2.2.5)
20
+ drb
21
+ i18n (>= 1.6, < 2)
22
+ minitest (>= 5.1)
23
+ mutex_m
24
+ tzinfo (~> 2.0)
25
+ base64 (0.2.0)
26
+ bigdecimal (3.1.5)
27
+ byebug (11.1.3)
28
+ concurrent-ruby (1.2.2)
29
+ connection_pool (2.4.1)
30
+ domain_name (0.6.20231109)
31
+ drb (2.2.0)
32
+ ruby2_keywords
33
+ eventmachine (1.2.7)
34
+ faye-websocket (0.11.3)
35
+ eventmachine (>= 0.12.0)
36
+ websocket-driver (>= 0.5.1)
37
+ ffi (1.16.3)
38
+ http-accept (1.7.0)
39
+ http-cookie (1.0.5)
40
+ domain_name (~> 0.5)
41
+ i18n (1.14.1)
42
+ concurrent-ruby (~> 1.0)
43
+ make_menu (0.0.3)
44
+ tty-screen (~> 0.8.2)
45
+ mime-types (3.5.1)
46
+ mime-types-data (~> 3.2015)
47
+ mime-types-data (3.2023.1205)
48
+ minitest (5.20.0)
49
+ mutex_m (0.2.0)
50
+ netrc (0.11.0)
51
+ osc-ruby (1.1.4)
52
+ rest-client (2.1.0)
53
+ http-accept (>= 1.7.0, < 2.0)
54
+ http-cookie (>= 1.0.2, < 2.0)
55
+ mime-types (>= 1.16, < 4.0)
56
+ netrc (~> 0.8)
57
+ ruby2_keywords (0.0.5)
58
+ rubyserial (0.6.0)
59
+ ffi (~> 1.9, >= 1.9.3)
60
+ tty-screen (0.8.2)
61
+ tzinfo (2.0.6)
62
+ concurrent-ruby (~> 1.0)
63
+ webrick (1.8.1)
64
+ websocket-driver (0.7.6)
65
+ websocket-extensions (>= 0.1.0)
66
+ websocket-extensions (0.1.5)
67
+
68
+ PLATFORMS
69
+ arm64-darwin-21
70
+
71
+ DEPENDENCIES
72
+ byebug
73
+ make_menu
74
+ oschii_ruby!
75
+
76
+ RUBY VERSION
77
+ ruby 2.7.7p221
78
+
79
+ BUNDLED WITH
80
+ 2.3.26
@@ -0,0 +1,294 @@
1
+ require 'restclient'
2
+ require 'osc-ruby'
3
+ require 'osc-ruby/em_server'
4
+ require 'socket'
5
+ require 'webrick'
6
+ require_relative 'servlets'
7
+
8
+ module Oschii
9
+ class Cloud
10
+ PING_PORT = 3333
11
+ PING_ADDR = '/hello_oschii'
12
+ RESPONSE_PORT = 3333
13
+ RESPONSE_ADDR = '/i_am_oschii'
14
+ HTTP_PORT = 8000
15
+
16
+ def initialize(silent: false)
17
+ @osc_server = OSC::EMServer.new(RESPONSE_PORT)
18
+ @http_server = WEBrick::HTTPServer.new(Port: HTTP_PORT)
19
+
20
+ @devices = {}
21
+ @silent = silent
22
+
23
+ start_listening
24
+ end
25
+
26
+ attr_reader :osc_server, :http_server, :devices, :silent
27
+
28
+ def find_serial(port = nil)
29
+ return ::Oschii::SerialDevice.new(port) if port
30
+
31
+ puts "\n~~> Scanning Serial Ports...."
32
+
33
+ 9.times do |i|
34
+ port = "/dev/ttyUSB#{i}"
35
+ print "\n~~> #{port}: "
36
+ begin
37
+ node = ::Oschii::SerialDevice.new(port)
38
+ name = "serial_#{node.name.downcase}".to_sym
39
+ if (existing_node = devices[name.downcase.to_sym])
40
+ existing_node.refresh
41
+ else
42
+ devices[name] = node
43
+ node.refresh
44
+ end
45
+ puts "'#{node.name}'"
46
+ rescue ::Oschii::DeviceUnavailable => e
47
+ puts e.message
48
+ end
49
+ end
50
+
51
+ print_devices
52
+
53
+ self
54
+ end
55
+
56
+ def start_listening
57
+ osc_server.add_method RESPONSE_ADDR do |message|
58
+ payload = message.to_a.first
59
+ begin
60
+ json = JSON.parse(payload)
61
+ name = json['name']
62
+ ip = json['ip']
63
+ rescue JSON::ParserError
64
+ name = payload.split(':').last.strip
65
+ ip = message.ip_address
66
+ end
67
+
68
+ if (device = devices[name.downcase.to_sym])
69
+ puts "\n==> '#{name}' is back\n" unless silent
70
+ device.refresh
71
+ else
72
+ begin
73
+ device = ::Oschii::HttpDevice.new(ip)
74
+ # add_http_handlers device
75
+ puts "\n==> '#{name}' connected @ #{device.ip_address}\n" unless silent
76
+ devices[name.downcase.to_sym] = device
77
+ rescue Errno::ECONNREFUSED => e
78
+ puts "\n!!! Failed connecting to #{name} on #{ip}"
79
+ end
80
+ end
81
+ save_device device.ip
82
+ end
83
+
84
+ Thread.new do
85
+ puts "~~> Starting OSC Server on port #{RESPONSE_PORT}" unless silent
86
+ osc_server.run
87
+ end
88
+
89
+ trap 'INT' do http_server.shutdown end
90
+
91
+ Thread.new do
92
+ puts "~~> Starting HTTP Server on port #{HTTP_PORT}" unless silent
93
+ http_server.start
94
+ end
95
+
96
+ http_server.mount '/api/devices', GetDevices, self
97
+ http_server.mount '/api/refresh', RefreshCloud, self
98
+ end
99
+
100
+ def wait_for(oschii_name, timeout: 300)
101
+ print "==> Waiting for '#{oschii_name}'.... "
102
+ find_devices
103
+ oschii = nil
104
+ started = Time.now
105
+ while oschii.nil? && Time.now - started <= timeout
106
+ oschii = get oschii_name
107
+ end
108
+ if oschii
109
+ puts 'FOUND :D'
110
+ else
111
+ puts 'NOT FOUND :('
112
+ end
113
+ oschii
114
+ end
115
+
116
+ def restart
117
+ devices.each do |_name, oschii|
118
+ Thread.new { oschii.restart }
119
+ end
120
+ end
121
+
122
+ attr_reader :monitor
123
+
124
+ def monitor_osc(*address_list, max: 100)
125
+ @monitor ||= OscMonitor.new self
126
+
127
+ address_list = [address_list] unless address_list.is_a?(Array)
128
+
129
+ address_list.each do |address|
130
+ monitor.add address, max: max
131
+ end
132
+
133
+ monitor.run
134
+ end
135
+
136
+ def capture_osc(address, simple: true, &block)
137
+ address = "/#{address}" if address[0] != '/'
138
+
139
+ osc_server.add_method address do |message|
140
+ value = simple ? message.to_a.first&.to_i : message.to_a
141
+ if block_given?
142
+ yield value
143
+ else
144
+ puts "--> OSC #{address} [ #{value} ]"
145
+ end
146
+ end
147
+ end
148
+
149
+ BASE_IP_FILE = '.base_ip'.freeze
150
+
151
+ def base_ip=(value)
152
+ File.write BASE_IP_FILE, value unless value.nil?
153
+ @base_ip = value
154
+ end
155
+
156
+ def base_ip
157
+ return @base_ip if @base_ip
158
+
159
+ ip = Socket.ip_address_list.detect{|intf| intf.ipv4_private?}
160
+
161
+ addr = ip.ip_address.to_s
162
+
163
+ puts "Local IP: #{addr}"
164
+
165
+ @base_ip = addr.split('.')[0..-2].join('.')
166
+
167
+ # if File.exists? BASE_IP_FILE
168
+ # @base_ip = File.read(BASE_IP_FILE).strip
169
+ # else
170
+ # @base_ip = '192.168.1'
171
+ # File.write BASE_IP_FILE, @base_ip
172
+ # end
173
+ # @base_ip = @base_ip[0..-2] if @base_ip[-1] == '.'
174
+ # @base_ip
175
+ end
176
+
177
+ def find_devices(echo: false)
178
+ @devices = {}
179
+ saved = saved_devices
180
+ saved.each do |ip|
181
+ begin
182
+ print "\r#{ip}" if echo
183
+
184
+ client = OSC::Client.new(ip, PING_PORT)
185
+ client.send(OSC::Message.new(PING_ADDR, 1))
186
+ rescue => e
187
+ # puts e.message
188
+ end
189
+ end
190
+
191
+ Thread.new do
192
+ (1..254).each do |i|
193
+ begin
194
+ ip = "#{base_ip}.#{i}"
195
+
196
+ next if saved.include? ip
197
+
198
+ print "\r#{ip}" if echo
199
+
200
+ client = OSC::Client.new(ip, PING_PORT)
201
+ client.send(OSC::Message.new(PING_ADDR, 1))
202
+
203
+ sleep 0.08
204
+ rescue => e
205
+ # puts e.message
206
+ end
207
+ end
208
+ end
209
+
210
+ puts if echo
211
+ end
212
+
213
+ def populate(echo: false)
214
+ find_devices(echo: echo)
215
+
216
+ start_waiting = Time.now
217
+ while Time.now - start_waiting < 3
218
+ sleep 0.2
219
+ end
220
+
221
+ print_devices unless silent
222
+ self
223
+ end
224
+
225
+ def print_devices
226
+ puts
227
+ puts self.to_s
228
+ puts
229
+ puts
230
+ end
231
+
232
+ def to_s
233
+ return '' if devices.empty?
234
+
235
+ result = ''
236
+
237
+ conn_width = 0
238
+ name_width = 0
239
+ devices.each do |name, device|
240
+ if device.is_a? ::Oschii::SerialDevice
241
+ conn_width = device.serial_port.size if device.serial_port.size > conn_width
242
+ elsif device.is_a? ::Oschii::HttpDevice
243
+ conn_width = device.ip.size if device.ip.size > conn_width
244
+ end
245
+ name_width = name.size if name.size > name_width
246
+ end
247
+
248
+ sorted = devices.to_a.sort_by { |name, _node| name }.to_h
249
+
250
+ result += "\e[1m#{'Name'.ljust(name_width + 3)}#{'Connection'.ljust(conn_width + 3)}#{'Version'}\e[22m"
251
+ sorted.each do |_name, device|
252
+ connection = if device.is_a? ::Oschii::SerialDevice
253
+ device.serial.ljust(conn_width)
254
+ elsif device.is_a? ::Oschii::HttpDevice
255
+ device.ip.ljust(conn_width)
256
+ else
257
+ ""
258
+ end
259
+ result += "\n#{device.name.ljust(name_width)} #{connection} #{device.version}"
260
+ end
261
+ result += "\n"
262
+ result
263
+ end
264
+
265
+ def get(name)
266
+ devices[name.to_s.downcase.to_sym]
267
+ end
268
+
269
+ def inspect
270
+ "<#{self.class.name} #{local_ip_address} devices: #{devices.size}>"
271
+ end
272
+
273
+ def local_ip_address
274
+ ip = Socket.ip_address_list.detect { |intf| intf.ipv4_private? }
275
+ ip.ip_address
276
+ end
277
+
278
+ def saved_devices
279
+ @saved_devices ||= if File.exists? '.saved_devices'
280
+ File.read('.saved_devices').split("\n")
281
+ else
282
+ []
283
+ end
284
+ end
285
+
286
+ def save_device(ip)
287
+ return if saved_devices.include? ip
288
+
289
+ @saved_devices << ip
290
+
291
+ File.write('.saved_devices', @saved_devices.join("\n"))
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,286 @@
1
+ require 'restclient'
2
+ require 'rubyserial'
3
+ require 'json'
4
+ require 'io/console'
5
+ require 'faye/websocket'
6
+ require 'eventmachine'
7
+ require 'byebug'
8
+
9
+ module Oschii
10
+ DeviceUnavailable = Class.new(StandardError)
11
+ NoConnection = Class.new(StandardError)
12
+
13
+ class Device
14
+ def initialize
15
+ @log_lines = []
16
+ refresh
17
+ end
18
+
19
+ attr_reader :log_lines
20
+
21
+ def restart
22
+ raise NoConnection
23
+ end
24
+
25
+ # Connection
26
+
27
+ def refresh
28
+ @device_details = nil
29
+ @status = nil
30
+ @settings = nil
31
+ @config = nil
32
+ poke!
33
+ end
34
+
35
+ def ip
36
+ raise NoConnection
37
+ end
38
+
39
+ def poke
40
+ raise NoConnection
41
+ end
42
+
43
+ def poke!
44
+ remaining_attempts = 3
45
+ while remaining_attempts > 0
46
+ begin
47
+ if poke
48
+ # device_details
49
+ return true
50
+ end
51
+ return false
52
+
53
+ rescue RubySerial::Error => e
54
+ raise DeviceUnavailable, case e.message
55
+ when 'ENOENT'
56
+ '(no device)'
57
+ when 'EBUSY'
58
+ '(port in use)'
59
+ else
60
+ e.message
61
+ end
62
+ rescue RestClient::Exception => e
63
+ raise DeviceUnavailable, e.message
64
+ end
65
+ remaining_attempts -= 1
66
+ end
67
+ raise DeviceUnavailable, '(no response)'
68
+ end
69
+
70
+ # Device details
71
+
72
+ def raw_device_details
73
+ raise NoConnection
74
+ end
75
+
76
+ def device_details
77
+ @device_details ||= JSON.parse(raw_device_details)
78
+ end
79
+
80
+ def version
81
+ device_details['version']
82
+ end
83
+
84
+ def name
85
+ device_details['name']
86
+ end
87
+
88
+ def name=(new_name)
89
+ self.settings = ({ name: new_name })
90
+ refresh
91
+ end
92
+
93
+ def description
94
+ device_details['description']
95
+ end
96
+
97
+ def config_name
98
+ device_details['configName']
99
+ end
100
+
101
+ def config_description
102
+ device_details['configDescription']
103
+ end
104
+
105
+ # Status
106
+
107
+ def raw_status
108
+ raise NoConnection
109
+ end
110
+
111
+ def status
112
+ @status ||= JSON.parse(raw_status)
113
+ end
114
+
115
+ def uptime
116
+ status['uptime']
117
+ end
118
+
119
+ # Settings
120
+
121
+ def raw_settings
122
+ raise NoConnection
123
+ end
124
+
125
+ def settings=(new_settings)
126
+ raise NoConnection
127
+ end
128
+
129
+ def settings
130
+ @settings ||= JSON.parse(raw_settings)
131
+ end
132
+
133
+ # Configuration
134
+
135
+ def raw_config
136
+ raise NoConnection
137
+ end
138
+
139
+ def raw_config=(json)
140
+ raise NoConnection
141
+ end
142
+
143
+ def config=(hash)
144
+ self.raw_config = hash.to_json
145
+ end
146
+
147
+ def config
148
+ @config ||= Oschii::Config.parse(raw_config)
149
+ end
150
+
151
+ def clear!
152
+ self.config = {}
153
+ end
154
+
155
+ def update!
156
+ self.config = config.compacted
157
+ end
158
+
159
+ def upload_config(filename = nil, silent: false)
160
+ if filename.nil?
161
+ filenames = Dir.glob("configs/#{name}_*.json")
162
+ if filenames.empty?
163
+ puts 'No previous config' unless silent
164
+ return
165
+ end
166
+ filename = filenames.sort.last
167
+
168
+ unless silent
169
+ display_name = filename.split('/')[-1]
170
+ .split('_')[-1]
171
+ .split('+')[0]
172
+ .gsub('T', ' ')
173
+ .gsub('.json', '')
174
+ puts "Latest: #{display_name}"
175
+ begin
176
+ prompt '>> [ENTER] to upload, [ESC] to cancel <<'
177
+ rescue CancelSerialQuery
178
+ return
179
+ end
180
+ end
181
+ end
182
+
183
+ self.config = JSON.parse File.read(filename)
184
+
185
+ self
186
+ end
187
+
188
+ def save_config(filename = nil, silent: false)
189
+ filename ||= "configs/#{name}_#{Time.now.iso8601}.json"
190
+ File.write filename, JSON.pretty_generate(config)
191
+ puts "Saved #{filename.split('/')[-1]}" unless silent
192
+ end
193
+
194
+ # Logging
195
+
196
+ def log
197
+ raise NoConnection
198
+ end
199
+
200
+ def clear_log
201
+ raise NoConnection
202
+ end
203
+
204
+ def tail(filter: nil)
205
+ while true
206
+ while (line = log_lines.shift)
207
+ if filter.nil? || line.match?(filter)
208
+ puts line
209
+ end
210
+ end
211
+ sleep 0.05
212
+ end
213
+ end
214
+
215
+ def logger(params = {})
216
+ current_logger = settings['logger']
217
+
218
+ all = params[:all]
219
+
220
+ sensors = all.nil? ? params[:sensors] : all
221
+ drivers = all.nil? ? params[:drivers] : all
222
+ monitors = all.nil? ? params[:monitors] : all
223
+ listeners = all.nil? ? params[:listeners] : all
224
+ states = all.nil? ? params[:states] : all
225
+ timers = all.nil? ? params[:timers] : all
226
+ subs = all.nil? ? params[:subs] : all
227
+ pixels = all.nil? ? params[:pixels] : all
228
+ network_in = all.nil? ? params[:network_in] : all
229
+ network_out = all.nil? ? params[:network_out] : all
230
+ timestamp = params[:timestamp]
231
+ to_file = params[:to_file]
232
+
233
+ current_logger['sensors'] = sensors unless sensors.nil?
234
+ current_logger['drivers'] = drivers unless drivers.nil?
235
+ current_logger['monitors'] = monitors unless monitors.nil?
236
+ current_logger['listeners'] = listeners unless listeners.nil?
237
+ current_logger['states'] = states unless states.nil?
238
+ current_logger['timers'] = timers unless timers.nil?
239
+ current_logger['subs'] = subs unless subs.nil?
240
+ current_logger['pixels'] = pixels unless pixels.nil?
241
+ current_logger['networkIn'] = network_in unless network_in.nil?
242
+ current_logger['networkOut'] = network_out unless network_out.nil?
243
+ current_logger['timestamp'] = timestamp unless timestamp.nil?
244
+ current_logger['logToFile'] = to_file unless to_file.nil?
245
+ update_settings({ 'logger' => current_logger })
246
+ current_logger = settings['logger']
247
+ puts JSON.pretty_generate current_logger
248
+ end
249
+
250
+ def inspect
251
+ "<#{self.class.name}[#{name}] (v#{version})>"
252
+ end
253
+
254
+ private
255
+
256
+ def prompt(text, obscure: false)
257
+ print "#{text}: "
258
+ input = ''
259
+ char = ''
260
+ until !char.empty? && char.ord == 13
261
+ char = STDIN.getch
262
+ if char.ord == 127
263
+ # BACKSPACE
264
+ input = input[0..-2]
265
+ print "\r#{text}: #{' ' * input.size} "
266
+ print "\r#{text}: #{obscure ? '*' * input.size : input}"
267
+ elsif char.ord == 27
268
+ # ESC
269
+ raise CancelSerialQuery
270
+ elsif char.ord == 13
271
+ # ENTER
272
+ else
273
+ input += char
274
+ if obscure
275
+ print '*'
276
+ else
277
+ print char
278
+ end
279
+ end
280
+ end
281
+ puts
282
+ input
283
+ end
284
+
285
+ end
286
+ end
@@ -0,0 +1,154 @@
1
+ module Oschii
2
+ class HttpDevice < Device
3
+ def initialize(ip)
4
+ @ip_address = ip
5
+ @osc_clients = {}
6
+ super()
7
+ end
8
+
9
+ attr_reader :ip_address, :osc_clients, :log_socket
10
+
11
+ def restart
12
+ RestClient.post("http://#{ip}/restart", {})&.body
13
+ end
14
+
15
+ # Connection
16
+
17
+ def refresh
18
+ stop_web_socket
19
+ super
20
+ end
21
+
22
+ def ip
23
+ ip_address
24
+ end
25
+
26
+ def poke
27
+ device_details
28
+ begin
29
+ start_web_socket
30
+ rescue
31
+ # ignored
32
+ end
33
+ true
34
+ end
35
+
36
+ # Raw getters
37
+
38
+ def raw_device_details
39
+ RestClient.get("http://#{ip}/device")&.body || ''
40
+ end
41
+
42
+ def raw_status
43
+ RestClient.get("http://#{ip}/status")&.body || ''
44
+ end
45
+
46
+ def raw_settings
47
+ RestClient.get("http://#{ip}/settings.json")&.body || ''
48
+ end
49
+
50
+ def raw_config
51
+ RestClient.get("http://#{ip}/config.json")&.body || ''
52
+ end
53
+
54
+ # Updaters
55
+
56
+ def raw_config=(json)
57
+ RestClient.post(
58
+ "http://#{ip}/config",
59
+ json,
60
+ content_type: 'application/json'
61
+ )
62
+ # refresh
63
+ end
64
+
65
+ def settings=(new_settings)
66
+ RestClient.post(
67
+ "http://#{ip}/settings",
68
+ new_settings.to_json,
69
+ content_type: 'application/json'
70
+ )
71
+ refresh
72
+ end
73
+
74
+ # Logging
75
+
76
+ def start_web_socket
77
+ EM.run {
78
+ @log_socket = Faye::WebSocket::Client.new("ws://#{ip}/logger_ws")
79
+
80
+ log_socket.on :open do |event|
81
+ log_socket.send('WebSocket connected')
82
+ end
83
+
84
+ log_socket.on :message do |event|
85
+ event.data.split("\n").each do |line|
86
+ log_lines << line
87
+ end
88
+ end
89
+
90
+ log_socket.on :close do |event|
91
+ log_socket.send("WebSocket DISCONNECTED: #{event.code} #{event.reason}")
92
+ @log_socket = nil
93
+ end
94
+ }
95
+ end
96
+
97
+ def stop_web_socket
98
+ log_socket&.close
99
+ end
100
+
101
+ # Network Commands
102
+
103
+ def fire(address, *values)
104
+ send_osc address, *values
105
+ end
106
+
107
+ def send_osc(address, *values, port: 3333)
108
+ address = "/#{address}" unless address[0] == '/'
109
+ osc_client(port).send(OSC::Message.new(address, *values))
110
+ end
111
+
112
+ def osc_client(port)
113
+ osc_clients[port] ||= OSC::Client.new(ip, port)
114
+ end
115
+
116
+ def get(path, value = 1)
117
+ send_http path, value, method: :get
118
+ end
119
+
120
+ def post(path, value = 1)
121
+ send_http path, value, method: :post
122
+ end
123
+
124
+ def send_http(path, value = 1, method: :post)
125
+ path = "/#{path}" unless %w(/ :).include?(path[0])
126
+ method = method.to_s.downcase.to_sym
127
+ url = "http://#{ip}#{path}"
128
+ if method == :get
129
+ args = [url]
130
+ else
131
+ args = [url, value.to_s]
132
+ end
133
+ puts RestClient.send(method, *args)
134
+ rescue RestClient::BadRequest => e
135
+ puts "Oschii didn't like that: #{e.http_body}"
136
+ end
137
+
138
+ def log
139
+ RestClient.get("http://#{ip}/log")&.body
140
+ end
141
+
142
+ def clear_log
143
+ RestClient.delete("http://#{ip}/log")&.body
144
+ end
145
+
146
+ def to_s
147
+ inspect
148
+ end
149
+
150
+ def inspect
151
+ "<#{self.class.name}[#{name}] #{ip_address} (v#{version})>"
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,57 @@
1
+ module Oschii
2
+ class OscMonitor
3
+ COL_WIDTH = 40
4
+
5
+ def initialize(cloud)
6
+ @cloud = cloud
7
+ @values = {}
8
+ end
9
+
10
+ attr_reader :cloud, :values
11
+
12
+ def add(name, max: 100)
13
+ unless values[name.to_sym]
14
+ values[name.to_sym] = {value: nil, max: max}
15
+ cloud.capture_osc name do |value|
16
+ values[name.to_sym][:value] = value
17
+ end
18
+ end
19
+
20
+ values[name.to_sym][:max] = max
21
+ end
22
+
23
+ def run
24
+ puts
25
+ values.each do |k, v|
26
+ print " #{k}".ljust (COL_WIDTH+4)
27
+ end
28
+ puts
29
+ while true
30
+ render
31
+ sleep 0.01
32
+ end
33
+ end
34
+
35
+ def render
36
+ print "\r"
37
+ values.each do |k, v|
38
+ print sensor_bar(v)
39
+ end
40
+ end
41
+
42
+ def sensor_bar(value)
43
+ result = '['
44
+ norm = [value[:value].to_f / value[:max], 1.0].min * COL_WIDTH
45
+ COL_WIDTH.times do |i|
46
+ if i < norm
47
+ result += '■'
48
+ else
49
+ result += ':'
50
+ end
51
+ end
52
+ result += '] '
53
+ end
54
+
55
+
56
+ end
57
+ end
@@ -0,0 +1,197 @@
1
+ module Oschii
2
+ class SerialDevice < Device
3
+ BAUD_RATE = 115200
4
+
5
+ CancelSerialQuery = Class.new(StandardError)
6
+
7
+ def initialize(serial)
8
+ @serial = serial
9
+ @capturing_serial = false
10
+ super()
11
+ end
12
+
13
+ attr_reader :serial
14
+ attr_accessor :capturing_serial, :serial_line_buffer
15
+
16
+ def poke
17
+ start_serial_capture
18
+ serial_query('poke', timeout: 2) == 'Tickles!'
19
+ end
20
+
21
+ def version
22
+ return @version if @version
23
+
24
+ attempts = 3
25
+ while attempts > 0
26
+ @version = serial_query 'version'
27
+ if @version.empty?
28
+ attempts -= 1
29
+ else
30
+ attempts = 0
31
+ end
32
+ end
33
+ end
34
+
35
+ def ip
36
+ serial_query 'ip'
37
+ end
38
+
39
+ def restart
40
+ serial_query 'restart'
41
+ end
42
+
43
+ def name
44
+ return @name if @name
45
+
46
+ attempts = 3
47
+ while attempts > 0
48
+ @name = serial_query 'name'
49
+ if @name.empty?
50
+ attempts -= 1
51
+ else
52
+ attempts = 0
53
+ end
54
+ end
55
+ end
56
+
57
+ def name=(new_name)
58
+ puts 'Querying serial port....'
59
+ if serial_query('name=') == '>> Enter new name <<'
60
+ puts serial_query new_name
61
+ end
62
+ refresh
63
+ end
64
+
65
+ def config=(new_config)
66
+ puts 'Querying serial port....'
67
+ if serial_query('config=') == '>> Enter new configuration <<'
68
+ puts 'Uploading configuration....'
69
+ puts serial_query new_config.to_json
70
+ end
71
+ refresh
72
+ end
73
+
74
+ def raw_config
75
+ serial_query 'config', timeout: 3
76
+ end
77
+
78
+ def status
79
+ JSON.parse(serial_query('status'))
80
+ end
81
+
82
+ def update_settings(new_settings = nil, file: nil)
83
+ unless file.nil?
84
+ puts 'Reading file....'
85
+ new_settings = JSON.parse(File.read(file))
86
+ end
87
+ puts 'Querying serial port....'
88
+ if serial_query('settings=') == '>> Enter new settings <<'
89
+ puts 'Uploading settings....'
90
+ puts serial_query new_settings.to_json
91
+ refresh
92
+ end
93
+ end
94
+
95
+ def raw_settings
96
+ serial_query 'settings'
97
+ end
98
+
99
+ def serial_query(query, timeout: 3)
100
+ # puts "#> #{query}"
101
+ purge_serial
102
+ serial_port.write (query.empty? ? "\n" : query)
103
+ sleep timeout
104
+ result = log_lines.join
105
+ log_lines.clear
106
+ # puts " #{result}"
107
+ result
108
+ end
109
+
110
+ def purge_serial
111
+ log_lines.clear
112
+ self.serial_line_buffer = ''
113
+ end
114
+
115
+ def start_serial_capture
116
+ return if capturing_serial
117
+
118
+ Thread.new do
119
+ self.capturing_serial = true
120
+ self.serial_line_buffer = ''
121
+ log_lines.clear
122
+ while capturing_serial
123
+ input = serial_port.read 100
124
+ input.each_char do |c|
125
+ if c == "\n"
126
+ log_lines << serial_line_buffer.gsub("\r", '')
127
+ self.serial_line_buffer = ''
128
+ else
129
+ serial_line_buffer << c
130
+ end
131
+ end
132
+ sleep 0.01
133
+ end
134
+ end
135
+ end
136
+
137
+ def stop_serial_capture
138
+ self.capturing_serial = false
139
+ end
140
+
141
+ def start_wifi(ssid = nil, password = nil)
142
+ begin
143
+ if ssid.nil?
144
+ ssid = prompt 'Enter SSID'
145
+ end
146
+
147
+ if password.nil?
148
+ password = prompt 'Enter password', obscure: true
149
+ end
150
+
151
+ puts 'Querying serial port...'
152
+ if serial_query('start wifi') == '>> Enter new Wifi SSID <<'
153
+ puts 'Sending SSID...'
154
+ if serial_query(ssid) == '>> Enter new Wifi Password <<'
155
+ puts 'Sending password...'
156
+ puts serial_query(password)
157
+ end
158
+ end
159
+
160
+ rescue CancelSerialQuery
161
+ puts 'Cancelled'
162
+ end
163
+ end
164
+
165
+ def stop_wifi
166
+ serial_query 'stop wifi'
167
+ end
168
+
169
+ def start_ethernet
170
+ serial_query 'start ethernet'
171
+ end
172
+
173
+ def stop_ethernet
174
+ serial_query 'stop ethernet'
175
+ end
176
+
177
+ def serial_port
178
+ @serial_port ||= Serial.new serial, BAUD_RATE
179
+ end
180
+
181
+ def log
182
+ serial_query 'print log'
183
+ end
184
+
185
+ def clear_log
186
+ serial_query 'clear log'
187
+ end
188
+
189
+ def to_s
190
+ inspect
191
+ end
192
+
193
+ def inspect
194
+ "<#{self.class.name}[#{name}] #{serial} (v#{version})>"
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,44 @@
1
+ module Oschii
2
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
3
+ def initialize(server, cloud)
4
+ super
5
+ @cloud = cloud
6
+ end
7
+
8
+ attr_reader :cloud
9
+
10
+ def add_headers(res)
11
+ res.header['Access-Control-Allow-Origin'] = '*'
12
+ res.header['Access-Control-Allow-Methods'] = 'GET'
13
+ res.header['Vary'] = 'Access-Control-Request-Headers'
14
+ res.header['Access-Control-Allow-Headers'] = 'Content-Type, Accept'
15
+ res.header['Access-Control-Request-Method'] = '*'
16
+ end
17
+ end
18
+
19
+ class GetDevices < Servlet
20
+ def do_GET(req, res)
21
+ res.status = 200
22
+ res.content_type = 'application/json'
23
+ res.body = cloud.devices.sort_by { |k, _v| k }.map do |name, device|
24
+ {
25
+ name: device.name,
26
+ ip: device.ip,
27
+ details: device.device_details
28
+ }
29
+ end.to_json
30
+ add_headers res
31
+ end
32
+ end
33
+
34
+ class RefreshCloud < Servlet
35
+ def do_POST(req, res)
36
+ Thread.new do
37
+ cloud.populate
38
+ end
39
+ res.status = 200
40
+ res.content_type = 'text/plain'
41
+ res.body = 'Refreshing Cloud'
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ require 'oschii'
2
+
3
+ include Oschii
4
+
5
+ if ENV['OSCHII_AUTO_POPULATE']
6
+ populate
7
+ end
@@ -0,0 +1,3 @@
1
+ module Oschii
2
+ VERSION = '0.0.1'
3
+ end
data/lib/oschii.rb ADDED
@@ -0,0 +1,56 @@
1
+ require 'oschii/version'
2
+
3
+ require_relative 'oschii/device'
4
+ require_relative 'oschii/http_device'
5
+ require_relative 'oschii/serial_device'
6
+ require_relative 'oschii/cloud'
7
+ require_relative 'oschii/osc_monitor'
8
+
9
+ module Oschii
10
+ LOGO = "
11
+ ╔═╗┌─┐┌─┐┬ ┬┬┬
12
+ ║ ║└─┐│ ├─┤││
13
+ ╚═╝└─┘└─┘┴ ┴┴┴
14
+ -- R U B Y ---
15
+ #{VERSION.center(14)}
16
+
17
+ ".freeze
18
+
19
+ def self.included(base)
20
+ base.class_eval do
21
+ puts LOGO
22
+ end
23
+ end
24
+
25
+ def cloud(silent: false)
26
+ @cloud ||= Cloud.new(silent: silent)
27
+ end
28
+
29
+ def populate
30
+ cloud.populate
31
+ end
32
+
33
+ def serve_forever
34
+ populate
35
+ puts 'Press ENTER to quit....'
36
+ gets
37
+ end
38
+
39
+ def serial
40
+ @serial ||= cloud.find_serial
41
+ end
42
+
43
+ def method_missing(m, *args, &block)
44
+ return if @cloud.nil?
45
+
46
+ node = cloud.get(m)
47
+
48
+ return node unless node.nil?
49
+
50
+ raise NameError
51
+ end
52
+
53
+ def send_osc(ip, address, *values, port: 3333)
54
+ OSC::Client.new(ip, port).send(OSC::Message.new(address, *values.to_a))
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oschii_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Barri Mason
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-12-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faye-websocket
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.11.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.11.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: rest-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubyserial
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.6.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.6.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: osc-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.1.4
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.1.4
69
+ - !ruby/object:Gem::Dependency
70
+ name: webrick
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.8.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.8.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 7.1.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 7.1.2
97
+ description: Interfaces with a network of Oschiis
98
+ email: loki@amarantha.net
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - Gemfile
104
+ - Gemfile.lock
105
+ - lib/oschii.rb
106
+ - lib/oschii/cloud.rb
107
+ - lib/oschii/device.rb
108
+ - lib/oschii/http_device.rb
109
+ - lib/oschii/osc_monitor.rb
110
+ - lib/oschii/serial_device.rb
111
+ - lib/oschii/servlets.rb
112
+ - lib/oschii/session.rb
113
+ - lib/oschii/version.rb
114
+ homepage: https://rubygems.org/gems/oschii_ruby
115
+ licenses:
116
+ - MIT
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: 2.7.7
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubygems_version: 3.3.26
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: Gem for managing Oschiis
137
+ test_files: []