radio 0.0.2 → 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.
- data/README.md +7 -6
- data/lib/radio.rb +5 -4
- data/lib/radio/controls/civ.rb +284 -0
- data/lib/radio/controls/si570avr.rb +0 -1
- data/lib/radio/filter.rb +33 -34
- data/lib/radio/filters/agc.rb +65 -0
- data/lib/radio/filters/fir.rb +264 -33
- data/lib/radio/filters/iq.rb +142 -0
- data/lib/radio/gif.rb +38 -40
- data/lib/radio/psk31/rx.rb +17 -7
- data/lib/radio/rig.rb +25 -3
- data/lib/radio/rig/rx.rb +11 -6
- data/lib/radio/rig/spectrum.rb +54 -43
- data/lib/radio/rig/ssb.rb +190 -0
- data/lib/radio/rig/ssb.rb_ +150 -0
- data/lib/radio/{input.rb → signal.rb} +16 -12
- data/lib/radio/{inputs → signals}/alsa.rb +20 -23
- data/lib/radio/signals/coreaudio.rb +136 -0
- data/lib/radio/{inputs → signals}/file.rb +8 -8
- data/lib/radio/{inputs → signals}/wav.rb +41 -28
- data/lib/radio/utils/firpm.rb +395 -0
- data/lib/radio/utils/misc.rb +39 -0
- data/lib/radio/utils/window.rb +37 -0
- data/lib/radio/version.rb +1 -1
- data/www/index.erb +81 -12
- data/www/lo.erb +2 -2
- data/www/setup/af.erb +72 -0
- data/www/setup/lo.erb +31 -0
- data/www/setup/{input.erb → rx.erb} +11 -10
- data/www/ssb.erb +20 -0
- data/www/tune.erb +5 -0
- data/www/waterfall.erb +1 -1
- metadata +38 -26
- data/lib/radio/inputs/coreaudio.rb +0 -102
- data/lib/radio/psk31/fir_coef.rb +0 -292
- data/test/test.rb +0 -76
- data/test/wav/bpsk8k.wav +0 -0
- data/test/wav/qpsk8k.wav +0 -0
- data/test/wav/ssb.wav +0 -0
data/README.md
CHANGED
@@ -2,16 +2,17 @@
|
|
2
2
|
|
3
3
|
An http server for interactive digital signal processing.
|
4
4
|
The mission is to find a solution that enables development of radios
|
5
|
-
using open and
|
5
|
+
using open and approachable technologies such as HTML and Javascript.
|
6
6
|
|
7
7
|
## Status
|
8
8
|
|
9
9
|
* HTTP Server: Working
|
10
10
|
* Spectrum Analyzer: Working
|
11
|
-
* Rig Control:
|
12
|
-
*
|
13
|
-
*
|
14
|
-
* CW:
|
11
|
+
* Rig Control: Working
|
12
|
+
* SSB: Working
|
13
|
+
* PSK31: Working, no UI
|
14
|
+
* CW: AF mode started
|
15
|
+
* Transmit: not yet
|
15
16
|
* User Interface: experimental, please contribute
|
16
17
|
* More drivers: always a WIP, please contribute
|
17
18
|
|
@@ -34,7 +35,7 @@ Apple OSX using homebrew:
|
|
34
35
|
|
35
36
|
brew install fftw libusb
|
36
37
|
|
37
|
-
Windows
|
38
|
+
Windows needs fftw3 installed but the install process is not yet known.
|
38
39
|
|
39
40
|
### Ruby
|
40
41
|
|
data/lib/radio.rb
CHANGED
@@ -15,9 +15,13 @@
|
|
15
15
|
|
16
16
|
require 'fftw3'
|
17
17
|
require 'thin'
|
18
|
+
require 'libusb'
|
19
|
+
require 'serialport'
|
20
|
+
require 'yaml'
|
18
21
|
|
19
22
|
# Suffer load order here
|
20
23
|
%w{
|
24
|
+
radio/utils/*.rb
|
21
25
|
radio/rig/*.rb
|
22
26
|
radio/*.rb
|
23
27
|
radio/**/*.rb
|
@@ -28,14 +32,11 @@ require 'thin'
|
|
28
32
|
end
|
29
33
|
|
30
34
|
class Radio
|
31
|
-
|
32
|
-
PI = Math::PI.freeze
|
33
|
-
PI2 = (8.0 * Math.atan(1.0)).freeze
|
34
35
|
|
35
36
|
def self.start
|
36
37
|
|
37
38
|
app = Rack::Builder.new do
|
38
|
-
use Rack::CommonLogger
|
39
|
+
# use Rack::CommonLogger
|
39
40
|
use Rack::ShowExceptions
|
40
41
|
use Rack::Lint
|
41
42
|
run @radio = Radio::HTTP::Server.new
|
@@ -0,0 +1,284 @@
|
|
1
|
+
# Copyright 2012 The ham21/radio Authors
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
|
16
|
+
class Radio
|
17
|
+
module Controls
|
18
|
+
|
19
|
+
# Support for Icom CI-V
|
20
|
+
# http://www.plicht.de/ekki/civ/civ-p0a.html
|
21
|
+
|
22
|
+
# The serialport gem is not flexible with device names, so:
|
23
|
+
# sudo ln -s /dev/cu.usbserial-A600BVS2 /dev/cuaa0
|
24
|
+
|
25
|
+
# You can use this somewhat asynchronously but if you queue up
|
26
|
+
# multiple commands of the same type or multiple commands that
|
27
|
+
# return OK/NG then it can't be perfect. This is a limitation
|
28
|
+
# of the CI-V protocol, not the implementation. While this makes
|
29
|
+
# my inner engineer cringe, it does work perfectly fine for
|
30
|
+
# user interfaces and typical applications.
|
31
|
+
class CIV
|
32
|
+
|
33
|
+
# An exception is raised when commands do not get a response.
|
34
|
+
TIMEOUT = 0.5
|
35
|
+
|
36
|
+
# Commands are retried automatically when we're not seeing messages.
|
37
|
+
# This happens when you have a collision. Also consider that older
|
38
|
+
# radios will need longer to respond.
|
39
|
+
WATCHDOG = 0.2
|
40
|
+
|
41
|
+
# Cache responses briefly
|
42
|
+
DWELL = 0.1
|
43
|
+
|
44
|
+
def initialize options={}
|
45
|
+
@semaphore = Mutex.new
|
46
|
+
@queue = Queue.new
|
47
|
+
@host = options[:host]|| 0xE0 # my address
|
48
|
+
@device = options[:device]|| 0x50 # radio address
|
49
|
+
port = options[:port]|| 0
|
50
|
+
@baud = options[:baud]|| 1200
|
51
|
+
bits = options[:bits]|| 8
|
52
|
+
stop = options[:stop]|| 1
|
53
|
+
parity = options[:parity]|| SerialPort::NONE
|
54
|
+
@io = SerialPort.new port, @baud, bits, stop, parity
|
55
|
+
setup options[:compat]
|
56
|
+
@state = :WaitPreamble1
|
57
|
+
@carrier_sense = 0
|
58
|
+
@last_message = Time.now
|
59
|
+
@machine = Thread.new &method(:machine)
|
60
|
+
@watchdog = Thread.new &method(:watchdog)
|
61
|
+
@lo = nil
|
62
|
+
raise "Icom CI-V device not found" unless lo
|
63
|
+
end
|
64
|
+
|
65
|
+
def stop
|
66
|
+
@machine.kill
|
67
|
+
@watchdog.kill
|
68
|
+
@machine.join
|
69
|
+
@watchdog.join
|
70
|
+
@io.close
|
71
|
+
end
|
72
|
+
|
73
|
+
def lo
|
74
|
+
@semaphore.synchronize do
|
75
|
+
return @lo if @lo and Time.now < @lo_expires
|
76
|
+
end
|
77
|
+
begin
|
78
|
+
lo = command 3
|
79
|
+
@semaphore.synchronize do
|
80
|
+
@lo_expires = Time.now + DWELL
|
81
|
+
@lo = lo
|
82
|
+
end
|
83
|
+
rescue RuntimeError => e
|
84
|
+
# defeat timeout when spinning the dial
|
85
|
+
# we can pick up the value from the 0x00 updates
|
86
|
+
@lo
|
87
|
+
# will all commands timeout while spinning?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def lo= freq
|
92
|
+
unless command 5, num_to_bcd(freq * 1000000, 5)
|
93
|
+
raise 'Unsupported frequency'
|
94
|
+
end
|
95
|
+
@semaphore.synchronize do
|
96
|
+
@lo = nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def command type, data_or_array = nil
|
103
|
+
cmd = "\xFE\xFE#{@device.chr}\xE0#{type.chr}".force_encoding("binary")
|
104
|
+
if Array === data_or_array
|
105
|
+
cmd += data_or_array.pack('C*').force_encoding("binary")
|
106
|
+
else
|
107
|
+
cmd += "#{data_or_array}".force_encoding("binary")
|
108
|
+
end
|
109
|
+
cmd += "\xfd".force_encoding("binary")
|
110
|
+
queue = Queue.new
|
111
|
+
@semaphore.synchronize do
|
112
|
+
send_when_clear cmd
|
113
|
+
@queue << [queue, type, cmd, Time.now] unless type < 2
|
114
|
+
end
|
115
|
+
if type < 2
|
116
|
+
true
|
117
|
+
else
|
118
|
+
result = queue.pop
|
119
|
+
raise result if RuntimeError === result
|
120
|
+
result
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def requeue cmd_queue, cmd_type, cmd_msg, cmd_time
|
125
|
+
if Time.now - cmd_time > TIMEOUT
|
126
|
+
cmd_queue << RuntimeError.new("Command #{cmd_type} timeout.")
|
127
|
+
end
|
128
|
+
send_when_clear cmd_msg
|
129
|
+
@queue << [cmd_queue, cmd_type, cmd_msg, cmd_time]
|
130
|
+
end
|
131
|
+
|
132
|
+
def bcd_to_num s
|
133
|
+
mult = 1
|
134
|
+
o = 0
|
135
|
+
s.each_byte do |b|
|
136
|
+
o += mult * (((b & 0xf0) >> 4)*10 + (b & 0xf))
|
137
|
+
mult *= 100
|
138
|
+
end
|
139
|
+
o
|
140
|
+
end
|
141
|
+
|
142
|
+
def num_to_bcd n, count
|
143
|
+
n = n.to_i
|
144
|
+
a = []
|
145
|
+
count.times do
|
146
|
+
a << ((n % 10) | ((n/10) % 10) << 4)
|
147
|
+
n /= 100
|
148
|
+
end
|
149
|
+
a
|
150
|
+
end
|
151
|
+
|
152
|
+
def watchdog
|
153
|
+
loop do
|
154
|
+
elapsed = nil
|
155
|
+
@semaphore.synchronize do
|
156
|
+
@last_message = Time.now if @queue.empty?
|
157
|
+
elapsed = Time.now - @last_message
|
158
|
+
if elapsed > WATCHDOG
|
159
|
+
# A sent message must have got lost in collision
|
160
|
+
# We only need to resend one to get things rolling again
|
161
|
+
requeue *@queue.pop
|
162
|
+
elapsed = 0
|
163
|
+
end
|
164
|
+
end
|
165
|
+
sleep WATCHDOG - elapsed
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Wait to make sure there isn't any data moving
|
170
|
+
# on the RS-422 2-wire bus before we begin sending.
|
171
|
+
# This needs to be tuned to account for most
|
172
|
+
# users not having a CT-17 for collision detect.
|
173
|
+
def send_when_clear cmd_msg
|
174
|
+
snooze = 0.05 + rand / 100 # 50-60ms
|
175
|
+
unless @carrier_sense == 0 and @last_message < Time.now - snooze
|
176
|
+
loop do
|
177
|
+
@carrier_sense = 0
|
178
|
+
sleep snooze
|
179
|
+
break if @carrier_sense == 0
|
180
|
+
end
|
181
|
+
end
|
182
|
+
@io.write cmd_msg
|
183
|
+
end
|
184
|
+
|
185
|
+
def machine
|
186
|
+
loop do
|
187
|
+
c = @io.getbyte
|
188
|
+
@carrier_sense = 1
|
189
|
+
@state = :WaitPreamble1 if c == 0xFC
|
190
|
+
case @state
|
191
|
+
when :WaitPreamble1
|
192
|
+
@state = :WaitPreamble2 if c == 0xFE
|
193
|
+
when :WaitPreamble2
|
194
|
+
if c == @host or c == 0x00
|
195
|
+
@state = :WaitFmAdress
|
196
|
+
else
|
197
|
+
@state = :WaitPreamble2
|
198
|
+
end
|
199
|
+
when :WaitFmAdress
|
200
|
+
if c == @device
|
201
|
+
@incoming = ''.force_encoding('binary')
|
202
|
+
@state = :WaitCommand
|
203
|
+
else
|
204
|
+
@state = :WaitPreamble1
|
205
|
+
end
|
206
|
+
when :WaitCommand
|
207
|
+
if c < 0x1F or c == 0xFA or c == 0xFB
|
208
|
+
@command = c
|
209
|
+
@state = :WaitFinal
|
210
|
+
else
|
211
|
+
@state = :WaitPreamble1
|
212
|
+
end
|
213
|
+
when :WaitFinal
|
214
|
+
if c == 0xFD
|
215
|
+
process @command, @incoming
|
216
|
+
@last_message = Time.now
|
217
|
+
@carrier_sense = 0
|
218
|
+
@state = :WaitPreamble1
|
219
|
+
elsif c > 0xFD
|
220
|
+
@state = :WaitPreamble1
|
221
|
+
else
|
222
|
+
@incoming += c.chr
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def process type, data
|
229
|
+
@semaphore.synchronize do
|
230
|
+
queue = nil
|
231
|
+
redos = []
|
232
|
+
if type > 1
|
233
|
+
while !@queue.empty?
|
234
|
+
queue, cmd_type, cmd_msg, cmd_time = @queue.pop
|
235
|
+
break if [0xFA,0xFB].include?(type) and @okng.include?(cmd_type)
|
236
|
+
break if cmd_type == type and @sizes[cmd_type] == data.size
|
237
|
+
redos << [queue, cmd_type, cmd_msg, cmd_time]
|
238
|
+
queue = nil
|
239
|
+
# Only skip one when handling a mangled response
|
240
|
+
break if cmd_type == type
|
241
|
+
end
|
242
|
+
end
|
243
|
+
redos.each do |cmd_queue, cmd_type, cmd_msg, cmd_time|
|
244
|
+
requeue cmd_queue, cmd_type, cmd_msg, cmd_time
|
245
|
+
end
|
246
|
+
return unless queue or type < 2
|
247
|
+
case type
|
248
|
+
when 0x00
|
249
|
+
if @sizes[3] == data.size
|
250
|
+
@lo = bcd_to_num(data).to_f/1000000
|
251
|
+
end
|
252
|
+
when 0x03
|
253
|
+
queue.push bcd_to_num(data).to_f/1000000
|
254
|
+
when 0xFB # OK
|
255
|
+
queue.push true
|
256
|
+
when 0xFA # NG no good
|
257
|
+
queue.push false
|
258
|
+
else
|
259
|
+
#TODO logger
|
260
|
+
p "Unsupported message: #{type} #{data.dump}"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Only the IC-735/IC-731 ever used 4byte frequencies.
|
266
|
+
# Other rigs may have a compatibility option.
|
267
|
+
# We depend on this heavily because the only data
|
268
|
+
# integrity check is the serial parity bit.
|
269
|
+
def setup compat
|
270
|
+
freq = compat ? 4 : 5
|
271
|
+
#TODO define all the commands
|
272
|
+
@sizes = {
|
273
|
+
2 => freq*2+1,
|
274
|
+
3 => freq,
|
275
|
+
4 => 2
|
276
|
+
}
|
277
|
+
#TODO compute OK/NG from a complete @sizes
|
278
|
+
@okng = [5, 6]
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
data/lib/radio/filter.rb
CHANGED
@@ -17,53 +17,52 @@ class Radio
|
|
17
17
|
|
18
18
|
# This generic Filter class will optimize by replacing its #call
|
19
19
|
# with an optimized version from a module. The type of data
|
20
|
-
# and initializer options determine the module
|
20
|
+
# and initializer options determine the module to load.
|
21
21
|
class Filter
|
22
|
+
|
23
|
+
TYPES = %w{
|
24
|
+
iq mix interpolate decimate fir agc
|
25
|
+
}.collect(&:to_sym).freeze
|
22
26
|
|
23
|
-
# Filters are built with mixing first and fir last.
|
24
|
-
# Chain multiple filters to get other effects.
|
25
|
-
# Make sure unused options don't test true.
|
26
|
-
# Filter.new(:mix => phase_inc, :decimate => int, :fir => array)
|
27
27
|
def initialize options
|
28
28
|
@options = options
|
29
|
+
@mod_name = ''
|
30
|
+
TYPES.each do |type|
|
31
|
+
@mod_name += type.to_s.capitalize if @options[type]
|
32
|
+
end
|
33
|
+
extend eval @mod_name
|
34
|
+
setup
|
29
35
|
end
|
30
36
|
|
31
|
-
# The first call with data is when we decide which module is optimal.
|
32
37
|
def call data, &block
|
33
|
-
|
34
|
-
if Enumerable === data
|
35
|
-
first = data.first
|
36
|
-
elsif NArray === data
|
37
|
-
first = data[0]
|
38
|
-
else
|
39
|
-
first = data
|
40
|
-
mod_name = ''
|
41
|
-
end
|
42
|
-
if Complex === first
|
43
|
-
mod_name = 'Complex' + mod_name
|
44
|
-
elsif Float === first
|
45
|
-
mod_name = 'Float' + mod_name
|
46
|
-
else
|
47
|
-
raise "Unknown data type: #{first.class}"
|
48
|
-
end
|
49
|
-
mod_name += 'Mix' if @options[:mix]
|
50
|
-
mod_name += 'Decimate' if @options[:decimate]
|
51
|
-
mod_name += 'Fir' if @options[:fir]
|
52
|
-
this_call = method :call
|
53
|
-
extend eval mod_name
|
54
|
-
if this_call == method(:call)
|
55
|
-
raise "#{mod_name} must override #call(data)"
|
56
|
-
end
|
57
|
-
setup data
|
38
|
+
extend_by_data_type data
|
58
39
|
call data, &block
|
59
40
|
end
|
41
|
+
|
42
|
+
def call! data, &block
|
43
|
+
extend_by_data_type data
|
44
|
+
call! data, &block
|
45
|
+
end
|
60
46
|
|
61
|
-
|
62
|
-
# have everything call super, this will catch.
|
63
|
-
def setup data
|
47
|
+
def setup
|
64
48
|
# noop
|
65
49
|
end
|
66
50
|
|
51
|
+
private
|
52
|
+
|
53
|
+
# If the base module didn't include a #call then it has
|
54
|
+
# defined specialized calls for each data type. We don't
|
55
|
+
# know the actual data type until the first data arrives.
|
56
|
+
def extend_by_data_type data
|
57
|
+
mod_type = @mod_name + '::' + data[0].class.to_s
|
58
|
+
if @fully_extended
|
59
|
+
raise "#{mod_type} not providing #call or #call! method."
|
60
|
+
end
|
61
|
+
this_call = method :call
|
62
|
+
extend eval mod_type
|
63
|
+
@fully_extended = true
|
64
|
+
end
|
65
|
+
|
67
66
|
end
|
68
67
|
end
|
69
68
|
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Copyright 2012 The ham21/radio Authors
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
|
16
|
+
class Radio
|
17
|
+
class Filter
|
18
|
+
|
19
|
+
module Agc
|
20
|
+
|
21
|
+
def setup
|
22
|
+
@gain = @options[:agc]
|
23
|
+
if Numeric === @gain
|
24
|
+
@gain = @gain.to_f
|
25
|
+
else
|
26
|
+
@gain = 20.0
|
27
|
+
end
|
28
|
+
@min = 0.0001
|
29
|
+
@max = 0.0
|
30
|
+
@attack = 0.05
|
31
|
+
@decay = 0.01
|
32
|
+
@reference = 0.05
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
module Float
|
37
|
+
def call data
|
38
|
+
yield(data.collect do |v|
|
39
|
+
out = @gain * v
|
40
|
+
abs_delta = out.abs - @reference
|
41
|
+
if abs_delta.abs > @gain
|
42
|
+
rate = @attack
|
43
|
+
out = -1.0 if out < -1.0
|
44
|
+
out = 1.0 if out > 1.0
|
45
|
+
else
|
46
|
+
rate = @decay
|
47
|
+
if out < -1.0
|
48
|
+
out = -1.0
|
49
|
+
rate *= 10
|
50
|
+
end
|
51
|
+
if out > 1.0
|
52
|
+
out = 1.0
|
53
|
+
rate *= 10
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@gain -= abs_delta * rate
|
57
|
+
out
|
58
|
+
end)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|