radio 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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 accessible technologies such as HTML and Javascript.
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: current WIP
12
- * PSK31: LPCM only, next WIP
13
- * SSB: Not Started
14
- * CW: Not Started
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 does not have dependencies; begin with Ruby install.
38
+ Windows needs fftw3 installed but the install process is not yet known.
38
39
 
39
40
  ### Ruby
40
41
 
@@ -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
+
@@ -12,7 +12,6 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require 'libusb'
16
15
 
17
16
  class Radio
18
17
  module Controls
@@ -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 name to load.
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
- mod_name = 'Each'
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
- # implement in modules, if you desire.
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