sonamp 0.0.3

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: '08e6106b3b465b9ca95ca7f886e0c3bb9639f7b542f4dfe44b4e7b84c82b803c'
4
+ data.tar.gz: b21d1a4dbd3410c8dd7565242ad3483826b86765454c3ac11c69d35f6b97ce1a
5
+ SHA512:
6
+ metadata.gz: 35a5af1165a07327246bac0c49e737556f1d5c6ca4f821a944fffeb52ee49213e7174e259a2eea3d1bd5c8e7e176095f8b51716b47b94b9ed4e645d7ef6a6739
7
+ data.tar.gz: db8b09fb6f59b8258ace7a7961b61295e6f2cafeaee5c1ceafab426364d0fae5fd7222e090eeee28fc4e53f73a4c7bfd524e280585a5f045318cf6efd98edf52
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/bin/sonamp ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'sonamp'
5
+ rescue LoadError
6
+ $: << File.join(File.dirname(__FILE__), '../lib')
7
+ require 'sonamp'
8
+ end
9
+ require 'optparse'
10
+ require 'logger'
11
+ require 'sonamp/utils'
12
+
13
+ options = {}
14
+ OptionParser.new do |opts|
15
+ opts.banner = "Usage: sonamp [-d device] command arg..."
16
+
17
+ opts.on("-d", "--device DEVICE", "TTY to use (default autodetect)") do |v|
18
+ options[:device] = v
19
+ end
20
+ end.parse!
21
+
22
+ logger = Logger.new(STDERR)
23
+ client = Sonamp::Client.new(options[:device], logger: logger)
24
+
25
+ cmd = ARGV.shift
26
+ unless cmd
27
+ raise ArgumentError, "No command given"
28
+ end
29
+
30
+ include Sonamp::Utils
31
+
32
+ case cmd
33
+ when 'power'
34
+ zone = ARGV.shift.to_i
35
+ state = parse_on_off(ARGV.shift)
36
+ client.power(zone, state)
37
+ when 'zvol'
38
+ zone = ARGV.shift.to_i
39
+ volume = ARGV.shift.to_i
40
+ client.zone_volume(zone, volume)
41
+ when 'cvol'
42
+ channel = ARGV.shift.to_i
43
+ volume = ARGV.shift.to_i
44
+ client.channel_volume(channel, volume)
45
+ when 'status'
46
+ client.status
47
+ else
48
+ raise ArgumentError, "Unknown command: #{cmd}"
49
+ end
data/bin/sonamp-web ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'sonamp/app'
5
+ rescue LoadError
6
+ $: << File.join(File.dirname(__FILE__), '../lib')
7
+ require 'sonamp/app'
8
+ end
9
+ require 'optparse'
10
+ require 'logger'
11
+
12
+ options = {}
13
+ OptionParser.new do |opts|
14
+ opts.banner = "Usage: sonamp-web [-d device] [-- rackup-options...]"
15
+
16
+ opts.on("-d", "--device DEVICE", "TTY to use (default autodetect)") do |v|
17
+ options[:device] = v
18
+ end
19
+
20
+ opts.separator ''
21
+ opts.separator 'To see rackup options: sonamp-web -- -h'
22
+ end.parse!
23
+
24
+ logger = Logger.new(STDERR)
25
+
26
+ #Sonamp::App.set :device, options[:device]
27
+ #Sonamp::App.set :logger, logger
28
+ Sonamp::App.set :client, Sonamp::Client.new(options[:device], logger: logger)
29
+
30
+ options = Rack::Server::Options.new.parse!(ARGV)
31
+ Rack::Server.start(options.merge(app: Sonamp::App))
data/lib/sonamp/app.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'sinatra/base'
2
+ require 'sonamp/utils'
3
+ require 'sonamp/client'
4
+
5
+ module Sonamp
6
+ class App < Sinatra::Base
7
+
8
+ set :device, nil
9
+ set :logger, nil
10
+ set :client, nil
11
+
12
+ get '/power' do
13
+ render_json(client.get_zone_power)
14
+ end
15
+
16
+ get '/zone/:zone/power' do |zone|
17
+ render_json(client.get_zone_power(zone.to_i))
18
+ end
19
+
20
+ put '/zone/:zone/power' do |zone|
21
+ state = Utils.parse_on_off(request.body.read)
22
+ client.set_zone_power(zone.to_i, state)
23
+ end
24
+
25
+ get '/zone/:zone/volume' do |zone|
26
+ render_json(client.get_zone_volume(zone.to_i))
27
+ end
28
+
29
+ put '/zone/:zone/volume' do |zone|
30
+ volume = request.body.read.to_i
31
+ client.set_zone_volume(zone.to_i, volume)
32
+ end
33
+
34
+ get '/channel/:channel/volume' do |channel|
35
+ render_json(client.get_channel_volume(channel.to_i))
36
+ end
37
+
38
+ put '/channel/:channel/volume' do |channel|
39
+ volume = request.body.read.to_i
40
+ client.set_channel_volume(channel.to_i, volume)
41
+ end
42
+
43
+ private
44
+
45
+ def client
46
+ settings.client || begin
47
+ @client ||= Sonamp::Client.new(settings.device, logger: settings.logger)
48
+ end
49
+ end
50
+
51
+ def render_json(data)
52
+ data.to_json
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,185 @@
1
+ module Sonamp
2
+ class Error < StandardError; end
3
+ class InvalidCommand < Error; end
4
+ class NotApplicable < Error; end
5
+ class UnexpectedResponse < Error; end
6
+
7
+ class Client
8
+ def initialize(device = nil, logger: nil)
9
+ @logger = logger
10
+
11
+ if device.nil?
12
+ device = Dir['/dev/ttyUSB*'].sort.first
13
+ if device
14
+ logger&.info("Using #{device} as TTY device")
15
+ end
16
+ end
17
+
18
+ unless device
19
+ raise ArgumentError, "No device specified and device could not be detected automatically"
20
+ end
21
+
22
+ @device = device
23
+ end
24
+
25
+ attr_reader :device
26
+ attr_accessor :logger
27
+
28
+ def get_zone_power(zone = nil)
29
+ get_zone_state('P', zone)
30
+ end
31
+
32
+ def set_zone_power(zone, state)
33
+ if zone < 1 || zone > 4
34
+ raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
35
+ end
36
+ cmd = ":P#{zone}#{state ? 1 : 0}"
37
+ expected = cmd[1...cmd.length]
38
+ dispatch_assert(cmd, expected)
39
+ end
40
+
41
+ def get_zone_volume(zone = nil)
42
+ if zone
43
+ if zone < 1 || zone > 4
44
+ raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
45
+ end
46
+ resp = dispatch(":V#{zone}?")
47
+ resp[2...].to_i
48
+ else
49
+ dispatch(":VG?", 4).map do |resp|
50
+ resp[2...].to_i
51
+ end
52
+ end
53
+ end
54
+
55
+ def set_zone_volume(zone, volume)
56
+ if zone < 1 || zone > 4
57
+ raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
58
+ end
59
+ if volume < 0 || volume > 100
60
+ raise ArgumentError, "Volume must be between 0 and 100: #{volume}"
61
+ end
62
+ cmd = ":V#{zone}#{volume}"
63
+ expected = cmd[1...cmd.length]
64
+ dispatch_assert(cmd, expected)
65
+ end
66
+
67
+ def set_channel_volume(channel, volume)
68
+ if channel < 1 || channel > 8
69
+ raise ArgumentError, "Channel must be between 1 and 4: #{channel}"
70
+ end
71
+ if volume < 0 || volume > 100
72
+ raise ArgumentError, "Volume must be between 0 and 100: #{volume}"
73
+ end
74
+ cmd = ":VC#{channel}#{volume}"
75
+ expected = cmd[1...cmd.length]
76
+ dispatch_assert(cmd, expected)
77
+ end
78
+
79
+ def get_zone_mute(zone = nil)
80
+ get_zone_state('M', zone)
81
+ end
82
+
83
+ def get_bbe(zone = nil)
84
+ get_zone_state('BP', zone)
85
+ end
86
+
87
+ def get_bbe_high_boost(zone = nil)
88
+ get_zone_state('BH', zone)
89
+ end
90
+
91
+ def get_bbe_low_boost(zone = nil)
92
+ get_zone_state('BL', zone)
93
+ end
94
+
95
+ def get_auto_trigger_input(zone = nil)
96
+ get_zone_state('ATI', zone)
97
+ end
98
+
99
+ def get_voltage_trigger_input(zone = nil)
100
+ get_zone_state('VTI', zone)
101
+ end
102
+
103
+ def status
104
+ # Reusing the opened device file makes :VTIG? fail even with a delay
105
+ # in front.
106
+ #open_device do
107
+ p dispatch(':VER?')
108
+ p dispatch(':TP?')
109
+ p get_power
110
+ p dispatch(':FPG?', 4)
111
+ p get_zone_volume
112
+ p dispatch(':VCG?', 8)
113
+ p get_auto_trigger_input
114
+ sleep 0.1
115
+ p get_voltage_trigger_input
116
+ p dispatch(':TVLG?', 8)
117
+ p get_zone_mute
118
+ p dispatch(':MCG?', 8)
119
+ p get_bbe
120
+ p get_bbe_high_boost
121
+ p get_bbe_low_boost
122
+ #end
123
+ end
124
+
125
+ private
126
+
127
+ def open_device
128
+ if @f
129
+ yield
130
+ else
131
+ File.open(device, 'r+') do |f|
132
+ @f = f
133
+ yield.tap do
134
+ @f = nil
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ def dispatch(cmd, resp_lines_count = 1)
141
+ open_device do
142
+ @f << "#{cmd}\x0d"
143
+ resp = 1.upto(resp_lines_count).map do
144
+ read_line(@f, cmd)
145
+ end
146
+ if resp_lines_count == 1
147
+ resp.first
148
+ else
149
+ resp
150
+ end
151
+ end
152
+ end
153
+
154
+ def dispatch_assert(cmd, expected)
155
+ resp = dispatch(cmd)
156
+ if resp != expected
157
+ raise UnexpectedResponse, "Expected #{expected}, got #{resp}"
158
+ end
159
+ end
160
+
161
+ def read_line(f, cmd)
162
+ f.readline.strip.tap do |resp|
163
+ if resp == 'ERR'
164
+ raise InvalidCommand, "Invalid command: #{cmd}"
165
+ elsif resp == 'N/A'
166
+ raise NotApplicable, "Command was recognized but could not be executed - is serial control enabled on the amplifier?"
167
+ end
168
+ end
169
+ end
170
+
171
+ def get_zone_state(cmd_prefix, zone)
172
+ if zone
173
+ if zone < 1 || zone > 4
174
+ raise ArgumentError, "Zone must be between 1 and 4: #{zone}"
175
+ end
176
+ resp = dispatch(":#{cmd_prefix}#{zone}?")
177
+ resp[cmd_prefix.length + 1] == '1' ? true : false
178
+ else
179
+ dispatch(":#{cmd_prefix}G?", 4).map do |resp|
180
+ resp[cmd_prefix.length + 1] == '1' ? true : false
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,15 @@
1
+ module Sonamp
2
+ module Utils
3
+
4
+ module_function def parse_on_off(value)
5
+ case value&.downcase
6
+ when '1', 'on', 'yes', 'true'
7
+ true
8
+ when '0', 'off', 'no', 'value'
9
+ false
10
+ else
11
+ raise ArgumentError, "Invalid on/off value: #{value}"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Sonamp
2
+ VERSION = '0.0.3'.freeze
3
+ end
data/lib/sonamp.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'sonamp/version'
2
+ require 'sonamp/client'
data/sonamp.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ # coding: utf-8
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "sonamp"
5
+ spec.version = '0.0.3'
6
+ spec.authors = ['Oleg Pudeyev']
7
+ spec.email = ['code@olegp.name']
8
+ spec.summary = %q{Sonance Sonamp Amplifier Serial Control Interface}
9
+ spec.description = %q{Library for controlling Sonance Sonamp 875D & 875D MkII amplifiers via the serial port}
10
+ spec.homepage = "https://github.com/p/sonamp-ruby"
11
+ spec.license = "MIT"
12
+
13
+ spec.files = `git ls-files -z`.split("\x0")
14
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ["lib"]
17
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sonamp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Oleg Pudeyev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-10-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Library for controlling Sonance Sonamp 875D & 875D MkII amplifiers via
14
+ the serial port
15
+ email:
16
+ - code@olegp.name
17
+ executables:
18
+ - sonamp
19
+ - sonamp-web
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - ".gitignore"
24
+ - bin/sonamp
25
+ - bin/sonamp-web
26
+ - lib/sonamp.rb
27
+ - lib/sonamp/app.rb
28
+ - lib/sonamp/client.rb
29
+ - lib/sonamp/utils.rb
30
+ - lib/sonamp/version.rb
31
+ - sonamp.gemspec
32
+ homepage: https://github.com/p/sonamp-ruby
33
+ licenses:
34
+ - MIT
35
+ metadata: {}
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.3.15
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Sonance Sonamp Amplifier Serial Control Interface
55
+ test_files: []