sonamp 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/bin/sonamp +49 -0
- data/bin/sonamp-web +31 -0
- data/lib/sonamp/app.rb +55 -0
- data/lib/sonamp/client.rb +185 -0
- data/lib/sonamp/utils.rb +15 -0
- data/lib/sonamp/version.rb +3 -0
- data/lib/sonamp.rb +2 -0
- data/sonamp.gemspec +17 -0
- metadata +55 -0
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
|
data/lib/sonamp/utils.rb
ADDED
@@ -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
|
data/lib/sonamp.rb
ADDED
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: []
|