nickel-silver-server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ == 0.0.1 2008-03-18
2
+
3
+ * 3 major enhancements:
4
+ * Initial release
5
+ * Complete LocoNetOverTCP version 1 implementation
6
+ * Support for the LocoBuffer-USB
data/License.txt ADDED
@@ -0,0 +1,3 @@
1
+ Nickel-Silver is distributed under the same terms as Ruby.
2
+
3
+ Copyright (c) 2008 Tobin Richard
data/Manifest.txt ADDED
@@ -0,0 +1,22 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ config/hoe.rb
7
+ config/requirements.rb
8
+ lib/nickel-silver-server.rb
9
+ lib/nickel-silver-server/version.rb
10
+ lib/LocoBufferUSB.rb
11
+ lib/LocoNetServer.rb
12
+ log/debug.log
13
+ script/destroy
14
+ script/generate
15
+ script/txt2html
16
+ setup.rb
17
+ tasks/deployment.rake
18
+ tasks/environment.rake
19
+ tasks/website.rake
20
+ test/test_helper.rb
21
+ test/test_nickel-silver-server.rb
22
+
data/README.txt ADDED
@@ -0,0 +1,43 @@
1
+ = nickel-silver-server
2
+
3
+ * http://rubyforge.org/projects/nickel-silver/
4
+
5
+ == DESCRIPTION:
6
+
7
+ Ruby, literally, on rails! A Ruby implementation of the LocoNetOverTCP protocol allowing remote clients to connect to Digitrax based model railway layouts. Also included are a number of tools for LocoNet logging and locomotive control.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Complete support for LocoNetOverTCP version 1
12
+
13
+ == SYNOPSIS:
14
+
15
+ require 'rubygems'
16
+ require 'nickel-silver-server'
17
+
18
+ # connect to a LocoBufferUSB on the virtual serial port /dev/tty.serialport
19
+ interface = LocoBufferUSB.new( '/dev/tty.serialport' )
20
+
21
+ # create a server using the default port (i.e. 5626, 'loco' spelt on a phone keypad)
22
+ # using our freshly connected LocoBuffer-USB
23
+ server = LocoNetServer.new( interface )
24
+
25
+ # start the server
26
+ server.start
27
+
28
+ # wait for the server to stop before exiting
29
+ server.join
30
+
31
+ == REQUIREMENTS:
32
+
33
+ * ruby-serialport is needed to connect with LocoBuffer-USB hardware http://rubyforge.org/projects/ruby-serialport/
34
+
35
+ == INSTALL:
36
+
37
+ * sudo gem install nickel-silver-server
38
+
39
+ == LICENSE:
40
+
41
+ Nickel Silver is distributed under the same terms as Ruby.
42
+
43
+ Copyright (c) 2008 Tobin Richard
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
data/config/hoe.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'nickel-silver-server/version'
2
+
3
+ AUTHOR = 'Tobin Richard' # can also be an array of Authors
4
+ EMAIL = "tobin.richard@gmail.com"
5
+ DESCRIPTION = "A Ruby implementation of a LocoNetOverTCP server."
6
+ GEM_NAME = 'nickel-silver-server' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'nickel-silver' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "unknown"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = NickelSilver::Server::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'nickel-silver-server documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.developer(AUTHOR, EMAIL)
52
+ p.description = DESCRIPTION
53
+ p.summary = DESCRIPTION
54
+ p.url = HOMEPATH
55
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
56
+ p.test_globs = ["test/**/test_*.rb"]
57
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
58
+
59
+ # == Optional
60
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
61
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
62
+
63
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
64
+
65
+ end
66
+
67
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
68
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
69
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
70
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
16
+
17
+ require 'nickel-silver-server'
@@ -0,0 +1,63 @@
1
+ # Explicitly call Kernel#require because the serialport library explodes with rubygem's implementation.
2
+ Kernel.require 'serialport'
3
+
4
+ module NickelSilver
5
+ module Server
6
+ module Interface
7
+
8
+ # A simple IO wrapper for the LocoBuffer-USB.
9
+ #
10
+ # See the documentation for LocoNetServer for details on how this should be used.
11
+ #
12
+ # = Stand-alone usage
13
+ # lb = LocoBufferUSB.new( '/dev/ttys0' )
14
+ #
15
+ # lb.run
16
+ #
17
+ # loop do
18
+ # sleep(1)
19
+ #
20
+ # until lb.input_buffer.empty? do
21
+ # lb.io_mutex.synchronize do
22
+ # puts "Got byte #{ format( '%02x', lb.input_buffer.shift ) } from LocoNet"
23
+ # end
24
+ # end
25
+ # end
26
+ #
27
+ class LocoBufferUSB
28
+ attr_accessor :input_buffer, :output_buffer, :io_mutex
29
+
30
+ # Connect to a LocoBuffer-USB using the specified serial port.
31
+ def initialize( serial_port )
32
+ @locobuffer = SerialPort.new( serial_port, 57_600 )
33
+
34
+ # these may be modified at any time by the server
35
+ @input_buffer = []
36
+ @output_buffer = []
37
+
38
+ # only make changes when locked using @io_mutex
39
+ @io_mutex = @iomutex = Mutex.new
40
+ end
41
+
42
+ # Handle packets moving in and out of the LocoBuffer-USB.
43
+ def run
44
+ loop do
45
+ while select( [@locobuffer], nil, nil, 0 ) do
46
+ @io_mutex.synchronize do
47
+ @input_buffer << @locobuffer.getc
48
+ end
49
+ end
50
+
51
+ # puts "outbuf length = #{output_buffer.length}"
52
+ until output_buffer.empty? do
53
+ @io_mutex.synchronize do
54
+ @locobuffer.putc( @output_buffer.shift )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,247 @@
1
+ require 'gserver'
2
+ require 'stringio'
3
+
4
+ module NickelSilver
5
+ module Server
6
+
7
+ # = Summary
8
+ # An implementation of the LoconetOverTcp protocol version 1 for use with the
9
+ # LocoBuffer-USB awailable from RR-CirKits (http://www.rr-cirkits.com).
10
+ #
11
+ # This simple protocol allows clients connected via TCP to access a LocoNet netowrk.
12
+ # Both sending and receiving of packets is supported.
13
+ #
14
+ # Author:: Tobin Richard (mailto:tobin.richard@gmail.com)
15
+ # Copyright:: Copyright (c) 2008
16
+ # License:: Distributes under the same terms as Ruby
17
+ #
18
+ # = Usage
19
+ # The following creates a server listening on the default port of 5626 ('loco' spelt on a phone keypad)
20
+ # using a LocoBuffer-USB connected to the serial port <tt>tty.serialport</tt>.
21
+ #
22
+ # require 'rubygems'
23
+ # require 'nickel-silver-server'
24
+ #
25
+ # # connect to a LocoBufferUSB on the virtual serial port /dev/tty.serialport
26
+ # interface = LocoBufferUSB.new( '/dev/tty.serialport' )
27
+ #
28
+ # # create a server using the default port (i.e. 5626, 'loco' spelt on a phone keypad)
29
+ # # using our freshly connected LocoBuffer-USB
30
+ # server = LocoNetServer.new( interface )
31
+ #
32
+ # # start the server
33
+ # server.start
34
+ #
35
+ # # wait for the server to stop before exiting
36
+ # server.join
37
+ #
38
+ # If you want logging of connections, disconnections and other activity then
39
+ # add <tt>server.audit = true</tt> before <tt>server.start</tt>.
40
+ #
41
+ # = Protocol
42
+ # For full details of the LoconetOverTcp protocol see
43
+ # http://loconetovertcp.sourceforge.net/Protocol/LoconetOverTcp.html
44
+ #
45
+ # Information is exchanged between the server and clients as plain ASCII strings. The server
46
+ # ignores invalid commands and empty lines.
47
+ #
48
+ # Clients may send the following commands to the server, as per the protocol specification:
49
+ #
50
+ # SEND Send a packet out over the LocoNet connection. The packet is not checked for correctness
51
+ # before transmission. E.g. <tt>SEND a0 2f 00 70</tt>
52
+ #
53
+ # The server may send the following information to clients, as per the protocol specification:
54
+ #
55
+ # VERSION: Sent to new clients immediately after they connect. The string which follows
56
+ # describes the LocoNetOverTcp server's name and version.
57
+ # E.g. <tt>VERSION NickelSilver version 0.1</tt>
58
+ #
59
+ # RECEIVE: Sent when a packet is received by the LocoBuffer-USB. E.g. <tt>RECEIVE 83 7c</tt>
60
+ #
61
+ # SENT: Sent to clients after an attempt has been made to process a SEND command. First
62
+ # parameter is always <tt>OK</tt> or <tt>ERROR</tt> and may be followed by a string describing
63
+ # details fo the transmission. E.g. <tt>SENT ERROR Could not communicate with LocoBuffer-USB</tt>
64
+ #
65
+ class LocoNetServer < GServer
66
+
67
+ # Creates a new LocoNetOverTCP server.
68
+ #
69
+ # You must supply an interface object and you may specify a port if the default of 5626
70
+ # does not suit your environment.
71
+ #
72
+ # See the full documentation for this class for an exmaple using the LocoBuffer-USB.
73
+ def initialize( interface, tcp_port=5626, *args )
74
+ # we maintain a list of clients to be notified of LocoNet packets
75
+ @clients = []
76
+
77
+ # we will require access to the interface's buffers
78
+ @interface = interface
79
+
80
+ # start the interface buffering in another thread
81
+ Thread.new { @interface.run }
82
+
83
+ # process incoming packets in another thread
84
+ Thread.new { process_packets }
85
+
86
+ super( tcp_port, *args )
87
+ end
88
+
89
+ private
90
+
91
+ # Serve a client.
92
+ #
93
+ # The client is registered with the server so it may be notified of LocoNet packets.
94
+ #
95
+ # Only this method may read from clients.
96
+ def serve( io )
97
+ # create a mutex to control access to this clients IO
98
+ semaphore = Mutex.new
99
+
100
+ # store the client and mutex for notification of LocoNet packets
101
+ client = { :io => io, :mutex => semaphore }
102
+ @clients << client
103
+
104
+ # ouput VERSION to client
105
+ semaphore.synchronize do
106
+ io.puts( 'VERSION NickelSilver version 0.1' )
107
+ end
108
+
109
+ # read, execute loop
110
+ loop do
111
+ # get any pending input
112
+ # if nil is returned then the client must have disconnected
113
+ line = io.gets
114
+ break if line.nil?
115
+
116
+ command = line.split( ' ' )
117
+
118
+ # LocoNet over TCP Version 1 only supports a single command, SEND
119
+ # ignore all other commands/lines
120
+ if command[0] == 'SEND'
121
+ # convert command into bytes
122
+ packet = command[ 1..command.length ]
123
+ packet.map! { |b| b.to_i(16) }
124
+
125
+ # send packet to loconet
126
+ begin
127
+ send_packet( packet )
128
+ semaphore.synchronize do
129
+ io.puts( 'SENT OK' )
130
+ end
131
+ rescue
132
+ semaphore.synchronize do
133
+ io.puts( 'SENT ERROR' )
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ # client has disconnected, remove it from the notification list
140
+ @clients.delete( client )
141
+ end
142
+
143
+ # Send a packet to the LocoBuffer-USB connection.
144
+ #
145
+ # Does not check the packet format, checksum or anything else.
146
+ #
147
+ # Waits for the the packet sent to be RECEIVE'd back from the LocoBuffer
148
+ def send_packet( packet )
149
+ # create a pipe and mutex and add ourselves as a fake client so we can
150
+ # check the packet is RECEIVE'd back by the LocoBuffer-USB
151
+ reader, writer = IO.pipe
152
+ client = { :io => writer , :mutex => Mutex.new }
153
+ @clients << client
154
+
155
+ # output the bytes
156
+ @interface.io_mutex.synchronize do
157
+ @interface.output_buffer += packet
158
+ end
159
+
160
+ # keep waiting for packets either for 2 seconds passes or we get a match
161
+ start = Time.now
162
+ matched = false
163
+ until Time.now - start > 2.0 || matched
164
+ # break it up and remove the leading RECEIVE
165
+ if select( [reader], nil, nil, 0 )
166
+ in_packet = reader.gets().split( ' ' )
167
+ in_packet.delete_at( 0 )
168
+ in_packet.map! { |b| b.to_i(16) }
169
+
170
+ # check for a match
171
+ matched = in_packet == packet
172
+ end
173
+ end
174
+
175
+ # remove the fake client from the notification list
176
+ @clients.delete( client )
177
+
178
+ # raise an exception if the packet didn't send
179
+ raise "Did not receive echo from LocoBuffer" unless matched
180
+ end
181
+
182
+ # Determine if a packet is complete. That is, determine if it has the correct
183
+ # length as determined by its opcode (and possibly its second byte)
184
+ def packet_complete?( packet )
185
+ # if less than two bytes packet can't be finished
186
+ return false if packet.length < 2
187
+
188
+ # Determine correct length. See LocoNet Personal Use Edition 1.0 for
189
+ # information on packet lengths.
190
+ case 0b0110_0000 & packet[0]
191
+ when 0b0000_0000 # two byte packet
192
+ packet.length == 2
193
+ when 0b0010_0000 # four byte packet
194
+ packet.length == 4
195
+ when 0b0100_0000 # six byte packet
196
+ packet.length == 6
197
+ when 0b0110_0000 # lower seven bits of second byte are message length
198
+ packet.length == packet[1]
199
+ end
200
+ end
201
+
202
+
203
+ # Process incoming packets and notficy clients.
204
+ def process_packets
205
+ packet = []
206
+ loop do
207
+ # wait for data in the buffer
208
+ # sleep long enough to avoid pegging the CPU
209
+ while @interface.input_buffer.empty?
210
+ sleep(0.1)
211
+ end
212
+
213
+ # get the next byte out of the buffer
214
+ byte = 0
215
+ @interface.io_mutex.synchronize do
216
+ byte = @interface.input_buffer.shift
217
+ end
218
+
219
+ # if this is the first byte it must have its msb set
220
+ packet << byte unless packet.empty? && byte < 0b1000_0000
221
+
222
+ # if we somehow got another opcode before completing a packet
223
+ # then dump the current broken packet and start over
224
+ packet = [byte] if byte >= 0b1000_0000
225
+
226
+ # notify clients if the packet is complete
227
+ if packet_complete?( packet )
228
+ notify_clients( packet )
229
+
230
+ # reset cuurent packet
231
+ packet = []
232
+ end
233
+ end
234
+ end
235
+
236
+ # Notify all clients of a received packet.
237
+ def notify_clients( packet )
238
+ @clients.each do |client|
239
+ client[:mutex].synchronize do
240
+ client[:io].puts( 'RECEIVE ' + packet.map{ |b| format( '%02x', b ) }.join( ' ' ) )
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ end
247
+ end
@@ -0,0 +1,11 @@
1
+ module NickelSilver #:nodoc:
2
+ module Server #:nodoc:
3
+ module VERSION #:nodoc:
4
+ MAJOR = 0
5
+ MINOR = 0
6
+ TINY = 1
7
+
8
+ STRING = [MAJOR, MINOR, TINY].join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ module NickelSilver
4
+ module Server
5
+ # nothing here yet
6
+ # all the action is in LocoNetServer.rb
7
+
8
+ module Interface
9
+ # nothing here yet
10
+ # all the action is in LocoBufferUSB.rb
11
+ end
12
+ end
13
+ end
data/log/debug.log ADDED
File without changes
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
data/script/txt2html ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ begin
5
+ require 'newgem'
6
+ rescue LoadError
7
+ puts "\n\nGenerating the website requires the newgem RubyGem"
8
+ puts "Install: gem install newgem\n\n"
9
+ exit(1)
10
+ end
11
+ require 'redcloth'
12
+ require 'syntax/convertors/html'
13
+ require 'erb'
14
+ require File.dirname(__FILE__) + '/../lib/nickel-silver-server/version.rb'
15
+
16
+ version = Nickel-silver-server::VERSION::STRING
17
+ download = 'http://rubyforge.org/projects/nickel-silver-server'
18
+
19
+ class Fixnum
20
+ def ordinal
21
+ # teens
22
+ return 'th' if (10..19).include?(self % 100)
23
+ # others
24
+ case self % 10
25
+ when 1: return 'st'
26
+ when 2: return 'nd'
27
+ when 3: return 'rd'
28
+ else return 'th'
29
+ end
30
+ end
31
+ end
32
+
33
+ class Time
34
+ def pretty
35
+ return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
36
+ end
37
+ end
38
+
39
+ def convert_syntax(syntax, source)
40
+ return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
41
+ end
42
+
43
+ if ARGV.length >= 1
44
+ src, template = ARGV
45
+ template ||= File.join(File.dirname(__FILE__), '/../website/template.rhtml')
46
+
47
+ else
48
+ puts("Usage: #{File.split($0).last} source.txt [template.rhtml] > output.html")
49
+ exit!
50
+ end
51
+
52
+ template = ERB.new(File.open(template).read)
53
+
54
+ title = nil
55
+ body = nil
56
+ File.open(src) do |fsrc|
57
+ title_text = fsrc.readline
58
+ body_text = fsrc.read
59
+ syntax_items = []
60
+ body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
61
+ ident = syntax_items.length
62
+ element, syntax, source = $1, $2, $3
63
+ syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}</#{element}>"
64
+ "syntax-temp-#{ident}"
65
+ }
66
+ title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
67
+ body = RedCloth.new(body_text).to_html
68
+ body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
69
+ end
70
+ stat = File.stat(src)
71
+ created = stat.ctime
72
+ modified = stat.mtime
73
+
74
+ $stdout << template.result(binding)