nickel-silver-server 0.0.1

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