seriamp 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+ require 'seriamp/backend/serial_port'
5
+ require 'seriamp/yamaha/protocol/constants'
6
+ require 'seriamp/yamaha/protocol/methods'
7
+
8
+ module Seriamp
9
+ module Yamaha
10
+
11
+ RS232_TIMEOUT = 3
12
+
13
+ class Client
14
+ include Protocol::Methods
15
+
16
+ def initialize(device: nil, glob: nil, logger: nil)
17
+ @logger = logger
18
+
19
+ @device = device
20
+ @detect_device = device.nil?
21
+ @glob = glob
22
+
23
+ if block_given?
24
+ begin
25
+ yield self
26
+ ensure
27
+ close
28
+ end
29
+ end
30
+ end
31
+
32
+ attr_reader :device
33
+ attr_reader :glob
34
+ attr_reader :logger
35
+
36
+ def detect_device?
37
+ @detect_device
38
+ end
39
+
40
+ def present?
41
+ last_status
42
+ true
43
+ end
44
+
45
+ def last_status
46
+ unless @status
47
+ with_device do
48
+ end
49
+ end
50
+ @status.dup
51
+ end
52
+
53
+ def last_status_string
54
+ unless @status_string
55
+ with_device do
56
+ end
57
+ end
58
+ @status_string.dup
59
+ end
60
+
61
+ def status
62
+ do_status
63
+ last_status
64
+ end
65
+
66
+ %i(
67
+ model_code firmware_version system_status power main_power zone2_power
68
+ zone3_power input input_name audio_select audio_select_name
69
+ main_volume main_volume_db zone2_volume zone2_volume_db
70
+ zone3_volume zone3_volume_db program program_name sleep night night_name
71
+ format sample_rate
72
+ ).each do |meth|
73
+ define_method(meth) do
74
+ status.fetch(meth)
75
+ end
76
+
77
+ define_method("last_#{meth}") do
78
+ last_status.fetch(meth)
79
+ end
80
+ end
81
+
82
+ %i(
83
+ multi_ch_input effect pure_direct speaker_a speaker_b
84
+ ).each do |meth|
85
+ define_method("#{meth}?") do
86
+ status.fetch(meth)
87
+ end
88
+
89
+ define_method("last_#{meth}?") do
90
+ last_status.fetch(meth)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ include Protocol::Constants
97
+
98
+ def open_device
99
+ if detect_device? && device.nil?
100
+ @device = Seriamp.detect_device(Yamaha, *glob, logger: logger)
101
+ if @device
102
+ logger&.info("Using #{device} as TTY device")
103
+ else
104
+ raise NoDevice, "No device specified and device could not be detected automatically"
105
+ end
106
+ end
107
+
108
+ @io = Backend::SerialPortBackend::Device.new(device, logger: logger)
109
+
110
+ begin
111
+ tries = 0
112
+ begin
113
+ do_status
114
+ rescue CommunicationTimeout
115
+ tries += 1
116
+ if tries < 5
117
+ logger&.warn("Timeout handshaking with the receiver - will retry")
118
+ retry
119
+ else
120
+ raise
121
+ end
122
+ end
123
+
124
+ yield @io
125
+ ensure
126
+ @io.close rescue nil
127
+ @io = nil
128
+ end
129
+ end
130
+
131
+ def with_device(&block)
132
+ if @io
133
+ yield @io
134
+ else
135
+ open_device(&block)
136
+ end
137
+ end
138
+
139
+ # ASCII table: https://www.asciitable.com/
140
+ DC1 = ?\x11
141
+ DC2 = ?\x12
142
+ ETX = ?\x03
143
+ STX = ?\x02
144
+ DEL = ?\x7f
145
+
146
+ STATUS_REQ = "#{DC1}001#{ETX}"
147
+
148
+ ZERO_ORD = '0'.ord
149
+
150
+ def dispatch(cmd)
151
+ with_device do
152
+ @io.syswrite(cmd.encode('ascii'))
153
+ read_response
154
+ end
155
+ end
156
+
157
+ def read_response
158
+ resp = +''
159
+ Timeout.timeout(2, CommunicationTimeout) do
160
+ loop do
161
+ ch = @io.sysread(1)
162
+ if ch
163
+ resp << ch
164
+ break if ch == ETX
165
+ else
166
+ sleep 0.1
167
+ end
168
+ end
169
+ end
170
+ resp
171
+ end
172
+
173
+ def do_status
174
+ resp = nil
175
+ loop do
176
+ resp = dispatch(STATUS_REQ)
177
+ again = false
178
+ while @io && IO.select([@io.io], nil, nil, 0)
179
+ logger&.warn("Serial device readable after completely reading status response - concurrent access?")
180
+ read_response
181
+ again = true
182
+ end
183
+ break unless again
184
+ end
185
+ payload = resp[1...-1]
186
+ @model_code = payload[0..4]
187
+ @version = payload[5]
188
+ length = payload[6..7].to_i(16)
189
+ data = payload[8...-2]
190
+ if data.length != length
191
+ raise BadStatus, "Broken status response: expected #{length} bytes, got #{data.length} bytes; concurrent operation on device?"
192
+ end
193
+ unless data.start_with?('@E01900')
194
+ raise BadStatus, "Broken status response: expected to start with @E01900, actual #{data[0..6]}"
195
+ end
196
+ @status_string = data
197
+ @status = {
198
+ # RX-V1500: model R0177
199
+ model_code: @model_code,
200
+ firmware_version: @version,
201
+ system_status: data[7].ord - ZERO_ORD,
202
+ power: power = data[8].ord - ZERO_ORD,
203
+ main_power: [1, 4, 5, 2].include?(power),
204
+ zone2_power: [1, 4, 3, 6].include?(power),
205
+ zone3_power: [1, 5, 3, 7].include?(power),
206
+ }
207
+ if data.length > 9
208
+ @status.update(
209
+ input: input = data[9],
210
+ input_name: MAIN_INPUTS_GET.fetch(input),
211
+ multi_ch_input: data[10] == '1',
212
+ audio_select: audio_select = data[11],
213
+ audio_select_name: AUDIO_SELECT_GET.fetch(audio_select),
214
+ mute: data[12] == '1',
215
+ # Volume values (0.5 dB increment):
216
+ # mute: 0
217
+ # -80.0 dB (min): 39
218
+ # 0 dB: 199
219
+ # +14.5 dB (max): 228
220
+ # Zone2 volume values (1 dB increment):
221
+ # mute: 0
222
+ # -33 dB (min): 39
223
+ # 0 dB (max): 72
224
+ main_volume: volume = data[15..16].to_i(16),
225
+ main_volume_db: int_to_half_db(volume),
226
+ zone2_volume: zone2_volume = data[17..18].to_i(16),
227
+ zone2_volume_db: int_to_full_db(zone2_volume),
228
+ zone3_volume: zone3_volume = data[129..130].to_i(16),
229
+ zone3_volume_db: int_to_full_db(zone3_volume),
230
+ program: program = data[19..20],
231
+ program_name: PROGRAM_GET.fetch(program),
232
+ # true: straight; false: effect
233
+ effect: data[21] == '1',
234
+ #extended_surround: data[22],
235
+ #short_message: data[23],
236
+ sleep: SLEEP_GET.fetch(data[24]),
237
+ night: night = data[27],
238
+ night_name: NIGHT_GET.fetch(night),
239
+ pure_direct: data[28] == '1',
240
+ speaker_a: data[29] == '1',
241
+ speaker_b: data[30] == '1',
242
+ format: data[31..32],
243
+ sample_rate: data[33..34],
244
+ )
245
+ end
246
+ @status
247
+ end
248
+
249
+ def remote_command(cmd)
250
+ dispatch("#{STX}0#{cmd}#{ETX}")
251
+ end
252
+
253
+ def system_command(cmd)
254
+ dispatch("#{STX}2#{cmd}#{ETX}")
255
+ end
256
+
257
+ def extract_text(resp)
258
+ # TODO: assert resp[0] == DC1, resp[-1] == ETX
259
+ resp[0...-1]
260
+ end
261
+
262
+ def int_to_half_db(value)
263
+ if value == 0
264
+ :mute
265
+ else
266
+ (value - 39) / 2.0 - 80
267
+ end
268
+ end
269
+
270
+ def int_to_full_db(value)
271
+ if value == 0
272
+ :mute
273
+ else
274
+ (value - 39) - 33
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'logger'
5
+ require 'pp'
6
+ require 'seriamp/utils'
7
+ require 'seriamp/detect'
8
+ require 'seriamp/yamaha/client'
9
+
10
+ module Seriamp
11
+ module Yamaha
12
+ class Cmd
13
+ def initialize(args)
14
+ options = {}
15
+ OptionParser.new do |opts|
16
+ opts.banner = "Usage: yamaha [-d device] command arg..."
17
+
18
+ opts.on("-d", "--device DEVICE", "TTY to use (default autodetect)") do |v|
19
+ options[:device] = v
20
+ end
21
+ end.parse!
22
+
23
+ @options = options
24
+
25
+ @logger = Logger.new(STDERR)
26
+ @client = Yamaha::Client.new(device: options[:device], logger: @logger)
27
+
28
+ @args = args
29
+ end
30
+
31
+ attr_reader :args
32
+ attr_reader :logger
33
+
34
+ def run
35
+ if args.any?
36
+ run_command(args)
37
+ else
38
+ STDIN.each_line do |line|
39
+ line.strip!
40
+ line.sub!(/#.*/, '')
41
+ next if line.empty?
42
+
43
+ run_command(line.strip.split(%r,\s+,))
44
+ end
45
+ end
46
+ end
47
+
48
+ def run_command(args)
49
+ cmd = args.shift
50
+ unless cmd
51
+ raise ArgumentError, "No command given"
52
+ end
53
+
54
+ case cmd
55
+ when 'detect'
56
+ device = Seriamp.detect_device(Yamaha, *args, logger: logger)
57
+ if device
58
+ puts device
59
+ exit 0
60
+ else
61
+ STDERR.puts("Yamaha receiver not found")
62
+ exit 3
63
+ end
64
+ when 'power'
65
+ which = ARGV.shift&.downcase
66
+ if %w(main zone2 zone3).include?(which)
67
+ method = "set_#{which}_power"
68
+ state = Utils.parse_on_off(ARGV.shift)
69
+ else
70
+ method = 'set_power'
71
+ state = Utils.parse_on_off(which)
72
+ end
73
+ client.public_send(method, state)
74
+ when 'volume'
75
+ which = ARGV.shift
76
+ if %w(main zone2 zone3).include?(which)
77
+ prefix = "set_#{which}"
78
+ value = ARGV.shift
79
+ else
80
+ prefix = 'set_main'
81
+ value = which
82
+ end
83
+ if %w(. -).include?(value)
84
+ method = "#{prefix}_mute"
85
+ value = true
86
+ else
87
+ method = "#{prefix}_volume_db"
88
+ if value[0] == ','
89
+ value = value[1..]
90
+ end
91
+ value = Float(value)
92
+ end
93
+ client.public_send(method, value)
94
+ p client.get_main_volume_text
95
+ p client.get_zone2_volume_text
96
+ p client.get_zone3_volume_text
97
+ when 'input'
98
+ which = ARGV.shift&.downcase
99
+ if %w(main zone2 zone3).include?(which)
100
+ method = "set_#{which}_input"
101
+ input = ARGV.shift
102
+ else
103
+ method = 'set_main_input'
104
+ input = which
105
+ end
106
+ client.public_send(method, input)
107
+ when 'program'
108
+ value = ARGV.shift.downcase
109
+ client.set_program(value)
110
+ when 'pure-direct'
111
+ state = Utils.parse_on_off(ARGV.shift)
112
+ client.set_pure_direct(state)
113
+ when 'status'
114
+ pp client.last_status
115
+ when 'status_string'
116
+ puts client.last_status_string
117
+ when 'test'
118
+ client.set_power(false)
119
+ [true, false].each do |main_state|
120
+ [true, false].each do |zone2_state|
121
+ [true, false].each do |zone3_state|
122
+ client.set_main_power(main_state)
123
+ client.set_zone2_power(zone2_state)
124
+ client.set_zone3_power(zone3_state)
125
+ puts "#{main_state ?1:0} #{zone2_state ?1:0} #{zone3_state ?1:0} #{client.status[:power]}"
126
+ end
127
+ end
128
+ end
129
+ else
130
+ raise ArgumentError, "Unknown command: #{cmd}"
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ attr_reader :client
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seriamp
4
+ module Yamaha
5
+ module Protocol
6
+ module Constants
7
+
8
+ private
9
+
10
+ PROGRAM_SET = {
11
+ 'munich' => 'E1',
12
+ 'vienna' => 'E5',
13
+ 'amsterdam' => 'E6',
14
+ 'freiburg' => 'E8',
15
+ 'chamber' => 'AF',
16
+ 'village_vanguard' => 'EB',
17
+ 'warehouse_loft' => 'EE',
18
+ 'cellar_club' => 'CD',
19
+ 'the_bottom_line' => 'EC',
20
+ 'the_roxy_theatre' => 'ED',
21
+ 'disco' => 'F0',
22
+ 'game' => 'F2',
23
+ '7ch_stereo' => 'FF',
24
+ '2ch_stereo' => 'C0',
25
+ 'sports' => 'F8',
26
+ 'action_game' => 'F2',
27
+ 'roleplaying_game' => 'CE',
28
+ 'music_video' => 'F3',
29
+ 'recital_opera' => 'F5',
30
+ 'standard' => 'FE',
31
+ 'spectacle' => 'F9',
32
+ 'sci-fi' => 'FA',
33
+ 'adventure' => 'FB',
34
+ 'drama' => 'FC',
35
+ 'mono_movie' => 'F7',
36
+ 'surround_decode' => 'FD',
37
+ 'thx_cinema' => 'C2',
38
+ 'thx_music' => 'C3',
39
+ 'thx_game' => 'C8',
40
+ }.freeze
41
+
42
+ MAIN_INPUTS_SET = {
43
+ 'phono' => '14',
44
+ 'cd' => '15',
45
+ 'tuner' => '16',
46
+ 'cd_r' => '19',
47
+ 'md_tape' => '18',
48
+ 'dvd' => 'C1',
49
+ 'dtv' => '54',
50
+ 'cbl_sat' => 'C0',
51
+ 'vcr1' => '0F',
52
+ 'dvr_vcr2' => '13',
53
+ 'v_aux_dock' => '55',
54
+ 'multi_ch' => '87',
55
+ 'xm' => 'B4',
56
+ }.freeze
57
+
58
+ ZONE2_INPUTS_SET = {
59
+ 'phono' => 'D0',
60
+ 'cd' => 'D1',
61
+ 'tuner' => 'D2',
62
+ 'cd_r' => 'D4',
63
+ 'md_tape' => 'D3',
64
+ 'dvd' => 'CD',
65
+ 'dtv' => 'D9',
66
+ 'cbl_sat' => 'CC',
67
+ 'vcr1' => 'D6',
68
+ 'dvr_vcr2' => 'D7',
69
+ 'v_aux_dock' => 'D8',
70
+ 'xm' => 'B8',
71
+ }.freeze
72
+
73
+ ZONE3_INPUTS_SET = {
74
+ 'phono' => 'F1',
75
+ 'cd' => 'F2',
76
+ 'tuner' => 'F3',
77
+ 'cd_r' => 'F5',
78
+ 'md_tape' => 'F4',
79
+ 'dvd' => 'FC',
80
+ 'dtv' => 'F6',
81
+ 'cbl_sat' => 'F7',
82
+ 'vcr1' => 'F9',
83
+ 'dvr_vcr2' => 'FA',
84
+ 'v_aux_dock' => 'F0',
85
+ 'xm' => 'B9',
86
+ }.freeze
87
+
88
+ MAIN_INPUTS_GET = {
89
+ '0' => 'PHONO',
90
+ '1' => 'CD',
91
+ '2' => 'TUNER',
92
+ '3' => 'CD-R',
93
+ '4' => 'MD/TAPE',
94
+ '5' => 'DVD',
95
+ '6' => 'DTV',
96
+ '7' => 'CBL/SAT',
97
+ '8' => 'SAT',
98
+ '9' => 'VCR1',
99
+ 'A' => 'DVR/VCR2',
100
+ 'B' => 'VCR3/DVR',
101
+ 'C' => 'V-AUX/DOCK',
102
+ 'D' => 'NET/USB',
103
+ 'E' => 'XM',
104
+ }.freeze
105
+
106
+ AUDIO_SELECT_GET = {
107
+ '0' => 'Auto', # Confirmed RX-V1500
108
+ '2' => 'DTS', # Confirmed RX-V1500
109
+ '3' => 'Coax / Opt', # Unconfirmed
110
+ '4' => 'Analog', # Confirmed RX-V1500
111
+ '5' => 'Analog Only', # Unconfirmed
112
+ '8' => 'HDMI', # Unconfirmed
113
+ }.freeze
114
+
115
+ NIGHT_GET = {
116
+ '0' => 'Off',
117
+ '1' => 'Cinema',
118
+ '2' => 'Music',
119
+ }.freeze
120
+
121
+ SLEEP_GET = {
122
+ '0' => 120,
123
+ '1' => 90,
124
+ '2' => 60,
125
+ '3' => 30,
126
+ '4' => nil,
127
+ }.freeze
128
+
129
+ PROGRAM_GET = {
130
+ '00' => 'Munich',
131
+ '01' => 'Hall B',
132
+ '02' => 'Hall C',
133
+ '04' => 'Hall D',
134
+ '05' => 'Vienna',
135
+ '06' => 'Live Concert',
136
+ '07' => 'Hall in Amsterdam',
137
+ '08' => 'Tokyo',
138
+ '09' => 'Freiburg',
139
+ '0A' => 'Royaumont',
140
+ '0B' => 'Chamber',
141
+ '0C' => 'Village Gate',
142
+ '0D' => 'Village Vanguard',
143
+ '0E' => 'The Bottom Line',
144
+ '0F' => 'Cellar Club',
145
+ '10' => 'The Roxy Theater',
146
+ '11' => 'Warehouse Loft',
147
+ '12' => 'Arena',
148
+ '14' => 'Disco',
149
+ '15' => 'Party',
150
+ '17' => '7ch Stereo',
151
+ '18' => 'Music Video',
152
+ '19' => 'DJ',
153
+ '1C' => 'Recital/Opera',
154
+ '1D' => 'Pavilion',
155
+ '1E' => 'Action Gamae',
156
+ '1F' => 'Role Playing Game',
157
+ '20' => 'Mono Movie',
158
+ '21' => 'Sports',
159
+ '24' => 'Spectacle',
160
+ '25' => 'Sci-Fi',
161
+ '28' => 'Adventure',
162
+ '29' => 'Drama',
163
+ '2C' => 'Surround Decode',
164
+ '2D' => 'Standard',
165
+ '30' => 'PLII Movie',
166
+ '31' => 'PLII Music',
167
+ '32' => 'Neo:6 Movie',
168
+ '33' => 'Neo:6 Music',
169
+ '34' => '2ch Stereo',
170
+ '35' => 'Direct Stereo',
171
+ '36' => 'THX Cinema',
172
+ '37' => 'THX Music',
173
+ '3C' => 'THX Game',
174
+ '40' => 'Enhancer 2ch Low',
175
+ '41' => 'Enhancer 2ch High',
176
+ '42' => 'Enhancer 7ch Low',
177
+ '43' => 'Enhancer 7ch High',
178
+ '80' => 'Straight',
179
+ }.freeze
180
+ end
181
+ end
182
+ end
183
+ end