oschii_ruby 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.
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: []