radio 0.0.1 → 0.0.2

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.
@@ -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 FirSetup
20
+ # It's ok that not all filter patterns use all instance variables.
21
+ def setup data
22
+ @mix_phase = 0.0
23
+ @mix_phase_inc = @options[:mix]
24
+ @dec_pos = @dec_size = @options[:decimate]
25
+ coef = @options[:fir]
26
+ @fir_pos = 0
27
+ @fir_size = coef.size
28
+ @fir_coef = NArray.to_na coef.reverse*2
29
+ @fir_buf = NArray.complex @fir_size
30
+ super
31
+ end
32
+ end
33
+
34
+ module FloatEachMixDecimateFir
35
+ include FirSetup
36
+ def call data
37
+ data.each do |energy|
38
+ @fir_pos = @fir_size if @fir_pos == 0
39
+ @fir_pos -= 1
40
+ @fir_buf[@fir_pos] = Complex(Math.cos(@mix_phase)*energy, -Math.sin(@mix_phase)*energy)
41
+ @mix_phase += @mix_phase_inc
42
+ @mix_phase -= PI2 if @mix_phase >= PI2
43
+ @dec_pos -= 1
44
+ if @dec_pos == 0
45
+ @dec_pos = @dec_size
46
+ iq = @fir_buf.mul_accum @fir_coef[@fir_size-@fir_pos..-1-@fir_pos],0
47
+ yield iq[0]
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ module ComplexFir
54
+ include FirSetup
55
+ def call data
56
+ @fir_pos = @fir_size if @fir_pos == 0
57
+ @fir_pos -= 1
58
+ @fir_buf[@fir_pos] = data
59
+ iq = @fir_buf.mul_accum @fir_coef[@fir_size-@fir_pos..-1-@fir_pos],0
60
+ yield iq[0]
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,84 @@
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
+ # 2D Array of numbers will be mapped to 1..127 colors.
17
+ # This 7-bit data is encoded uncompressed and left for
18
+ # http to compress. Background color reserved for future use.
19
+
20
+ class Radio
21
+ class Gif
22
+
23
+ # This is an optimized GIF generator for waterfalls.
24
+ # It requires 128 RGB colors, the first is unused and
25
+ # reserved for transparency if we ever need it.
26
+ def self.waterfall colors, data
27
+
28
+ gif = [
29
+ 'GIF87a', # Start Header
30
+ data[0].size, # width
31
+ data.size, # height
32
+ 0xF6, # 128 24-bit colors
33
+ 0x00, # background color index
34
+ 0x00 # aspect ratio
35
+ ].pack 'a6vvCCC'
36
+
37
+ gif += colors.flatten.pack 'C*'
38
+
39
+ gif += [
40
+ 0x2C, # Start Image Block
41
+ 0x0000, # Left position
42
+ 0x0000, # Top position
43
+ data[0].size, # width
44
+ data.size, # height
45
+ 0x00, # No color table, not interlaced
46
+ 0x07 # LZW code size
47
+ ].pack('CvvvvCC')
48
+
49
+ data.each_with_index do |vals, row|
50
+ col = 0
51
+ min = vals.min
52
+ range = [1e-99, vals.max - min].max
53
+ while col < vals.size
54
+ # Uncompressed GIF trickery avoids bit packing too
55
+ # 126 byte chunks with reset keeps LZW in 8 bit codes
56
+ col_end = [col+126,vals.size].min
57
+ slice = vals.slice(col...col_end).to_a
58
+ # This 126 because palette is 1..127
59
+ slice = slice.collect { |x| (x - min) / range * 126 + 1 }
60
+ slice = slice.pack 'C*'
61
+ newstuff = [
62
+ slice.size+1,
63
+ slice,
64
+ 0x80 # LZW reset
65
+ ].pack('Ca*C')
66
+ gif += newstuff
67
+ col += 126
68
+ end
69
+
70
+ end
71
+
72
+ gif += [
73
+ 0x01, # end image blocks
74
+ 0x81, # final image block: LZW end
75
+ 0x00, # end image blocks
76
+ 0x3B # end gif
77
+ ].pack('C*')
78
+
79
+ return gif
80
+
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,88 @@
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 HTTP
18
+ class FileResponse
19
+
20
+ def initialize(env, filename, content_type = nil)
21
+ @env = env
22
+ @filename = filename
23
+ @status = 200
24
+ @headers = {}
25
+ @body = []
26
+
27
+ begin
28
+ raise Errno::EPERM unless File.file?(filename) and File.readable?(filename)
29
+ rescue SystemCallError
30
+ @body = ["404 Not Found\n"]
31
+ @headers["Content-Length"] = @body.first.size.to_s
32
+ @headers["Content-Type"] = 'text/plain'
33
+ @headers["X-Cascade"] = 'pass'
34
+ @status = 404
35
+ return
36
+ end
37
+
38
+ # Caching strategy
39
+ mod_since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']) rescue nil
40
+ last_modified = File.mtime(filename)
41
+ @status = 304 and return if last_modified == mod_since
42
+ @headers["Last-Modified"] = last_modified.httpdate
43
+ if env['QUERY_STRING'] =~ /^[0-9]{9,10}$/ and last_modified == Time.at(env['QUERY_STRING'].to_i)
44
+ @headers["Cache-Control"] = 'max-age=86400, public' # one day
45
+ else
46
+ @headers["Cache-Control"] = 'max-age=0, private, must-revalidate'
47
+ end
48
+
49
+ # Sending the file or reading an unknown length stream to send
50
+ @body = self
51
+ unless size = File.size?(filename)
52
+ @body = [File.read(filename)]
53
+ size = @body.first.respond_to?(:bytesize) ? @body.first.bytesize : @body.first.size
54
+ end
55
+ @headers["Content-Length"] = size.to_s
56
+ @headers["Content-Type"] = content_type || Rack::Mime.mime_type(File.extname(filename), 'text/plain')
57
+ end
58
+
59
+ # Support using self as a response body.
60
+ # @yield [String] 8k blocks
61
+ def each
62
+ File.open(@filename, "rb") do |file|
63
+ while part = file.read(8192)
64
+ yield part
65
+ end
66
+ end
67
+ end
68
+
69
+ # Filename attribute.
70
+ # Alias is used by some rack servers to detach from Ruby early.
71
+ # @return [String]
72
+ attr_reader :filename
73
+ alias :to_path :filename
74
+
75
+ # Was the file in the system and ready to be served?
76
+ def found?
77
+ @status == 200 or @status == 304
78
+ end
79
+
80
+ # Present the final response for rack.
81
+ # @return (Array)[status, headers, body]
82
+ def finish
83
+ [@status, @headers, @body]
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,133 @@
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
+ require 'erb'
17
+
18
+ class Radio
19
+
20
+ class HTTP
21
+
22
+ # A Script instance is the context in which scripts are rendered.
23
+ # It inherits everything from Rack::Request and supplies a Response instance
24
+ # you can use for redirects, cookies, and other controller actions.
25
+ class Script < Rack::Request
26
+
27
+ include ERB::Util
28
+
29
+ class NotFound < StandardError
30
+ end
31
+
32
+ class RenderStackOverflow < StandardError
33
+ end
34
+
35
+ def initialize(env, filename)
36
+ super(env)
37
+ @render_stack = []
38
+ @response = original_response = Rack::Response.new
39
+ rendering = render(filename)
40
+ if @response == original_response and @response.empty?
41
+ @response.write rendering
42
+ end
43
+ rescue RenderStackOverflow, NotFound => e
44
+ if @render_stack.size > 0
45
+ # Make errors appear from the render instead of the engine.call
46
+ e.set_backtrace e.backtrace[1..-1]
47
+ raise e
48
+ end
49
+ @response.status = 404
50
+ @response.write "404 Not Found\n"
51
+ @response.header["X-Cascade"] = "pass"
52
+ @response.header["Content-Type"] = "text/plain"
53
+ end
54
+
55
+ # After rendering, #finish will be sent to the client.
56
+ # If you replace the response or add to the response#body,
57
+ # the script engine rendering will not be added.
58
+ # @return [Rack::Response]
59
+ attr_accessor :response
60
+
61
+ # An array of filenames representing the current render stack.
62
+ # @example
63
+ # <%= if render_stack.size == 1
64
+ # render 'html_version'
65
+ # else
66
+ # render 'included_version'
67
+ # end
68
+ # %>
69
+ # @return [<Array>]
70
+ attr_reader :render_stack
71
+
72
+ # Render another Script.
73
+ # @example view_test.erb
74
+ # <%= render 'util/logger_popup' %>
75
+ # @param (String) filename Relative to current Script.
76
+ # @param (Hash) locals Local variables for the Script.
77
+ def render(filename, locals = {})
78
+ if render_stack.size > 100
79
+ # Since nobody sane should recurse through here, this mainly
80
+ # finds a render self that you might get after a copy and paste
81
+ raise RenderStackOverflow
82
+ elsif render_stack.size > 0
83
+ # Hooray for relative paths and easily movable files
84
+ filename = File.expand_path(filename, File.dirname(render_stack.last))
85
+ else
86
+ # Underbar scripts are partials by convention; keep them from rendering at root
87
+ filename = File.expand_path(filename)
88
+ raise NotFound if File.basename(filename) =~ /^_/
89
+ end
90
+ ext = File.extname(filename)
91
+ files1 = [filename]
92
+ files1 << filename + '.html' if ext == ''
93
+ files1 << filename.sub(/.html$/,'') if ext == '.html'
94
+ files1.each do |filename1|
95
+ files2 = [filename1+'.erb']
96
+ files2 << filename1.gsub(/.html$/, '.erb') if File.extname(filename1) == '.html'
97
+ unless filename1 =~ /^_/ or render_stack.empty?
98
+ files2 = files2 + files2.collect {|f| "#{File.dirname(f)}/_#{File.basename(f)}"}
99
+ end
100
+ files2.each do |filename2|
101
+ if File.file?(filename2) and File.readable?(filename2)
102
+ if render_stack.empty?
103
+ response.header["Content-Type"] = Rack::Mime.mime_type(File.extname(filename1), 'text/html')
104
+ end
105
+ render_stack.push filename2
106
+ erb = ::ERB.new(File.read(filename2), nil, '-')
107
+ erb.filename = filename2
108
+ set_locals = locals.keys.map { |k| "#{k}=locals[#{k.inspect}];" }.join
109
+ instance_binding = instance_eval{binding}
110
+ eval set_locals, instance_binding
111
+ result = erb.result instance_binding
112
+ render_stack.pop
113
+ return result
114
+ end
115
+ end
116
+ end
117
+ raise NotFound
118
+ end
119
+
120
+ # Helper for finding files relative to Scripts.
121
+ # @param [String] filename
122
+ # @return [String] absolute filesystem path
123
+ def expand_path(filename, dir=nil)
124
+ dir ||= File.dirname render_stack.last
125
+ File.expand_path filename, dir
126
+ end
127
+
128
+ end
129
+
130
+
131
+ end
132
+
133
+ end
@@ -0,0 +1,94 @@
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
+ require 'rack'
17
+ require 'erb'
18
+
19
+ class Radio
20
+ class HTTP
21
+
22
+ MOUNTS = [
23
+ ['', File.expand_path(File.join(File.dirname(__FILE__), '../../../www'))]
24
+ ]
25
+
26
+ class Server
27
+
28
+ ENV_THREAD_BYPASS = 'radio.thread_bypass'
29
+
30
+ def initialize
31
+ @working_dir = Dir.getwd
32
+ end
33
+
34
+ # Thin can run some processes in threads if we provide the logic.
35
+ # Static files are served without deferring to a thread.
36
+ # Everything else is tried in the EventMachine thread pool.
37
+ def deferred? env
38
+ path_info = Rack::Utils.unescape(env['PATH_INFO'])
39
+ return false if path_info =~ /\.(\.|erb$)/ # unsafe '..' and '.erb'
40
+ MOUNTS.each do |path, dir|
41
+ if path_info =~ %r{^#{Regexp.escape(path)}(/.*|)$}
42
+ filename = File.join(dir, $1)
43
+ Dir.chdir @working_dir
44
+ response = FileResponse.new(env, filename)
45
+ if !response.found? and File.extname(path_info) == ''
46
+ response = FileResponse.new(env, filename + '.html')
47
+ end
48
+ if response.found?
49
+ env[ENV_THREAD_BYPASS] = response
50
+ return false
51
+ end
52
+ env[ENV_THREAD_BYPASS] = filename
53
+ end
54
+ end
55
+ return true
56
+ end
57
+
58
+ # Rack interface.
59
+ # @param (Hash) env Rack environment.
60
+ # @return (Array)[status, headers, body]
61
+ def call(env)
62
+ # The preprocessing left us with nothing, a response,
63
+ # or a filename that we should try to run.
64
+ case deferred_result = env.delete(ENV_THREAD_BYPASS)
65
+ when String
66
+ filename = deferred_result
67
+ response = Script.new(env, filename).response
68
+ if response.header["X-Cascade"] == 'pass'
69
+ index_response = Script.new(env, filename + '/index').response
70
+ response = index_response unless index_response.header["X-Cascade"] == 'pass'
71
+ end
72
+ response.finish
73
+ when NilClass
74
+ not_found
75
+ else
76
+ deferred_result.finish
77
+ end
78
+ end
79
+
80
+ # Status 404 with X-Cascade => pass.
81
+ # @return (Array)[status, headers, body]
82
+ def not_found
83
+ return @not_found if @not_found
84
+ body = "404 Not Found\n"
85
+ @not_found = [404, {'Content-Type' => 'text/plain',
86
+ 'Content-Length' => body.size.to_s,
87
+ 'X-Cascade' => 'pass'},
88
+ [body]]
89
+ @not_found
90
+ end
91
+
92
+ end
93
+ end
94
+ end