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