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 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