sonamp 0.0.3

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: '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: []