toholio-nickel-silver-server 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,27 @@
1
+ == 0.0.5 2008-08-31
2
+ * 2 enhancements:
3
+ * Added gemspec.
4
+ * Updated documentation to reflect the move to GitHub.
5
+
6
+ == 0.0.4 2008-03-19
7
+
8
+ * 1 bugfix
9
+ * Fixed erroneous example in LocoNetServer.rb
10
+
11
+ == 0.0.3 2008-03-19
12
+
13
+ * 1 minor enhancement:
14
+ * Tidy before first 'public' release
15
+
16
+ == 0.0.2 2008-03-19
17
+
18
+ * 1 bugfix
19
+ * Correctly load interface and server files when gem is 'require'd
20
+
21
+ == 0.0.1 2008-03-18
22
+
23
+ * 3 major enhancements:
24
+ * Initial release
25
+ * Complete LocoNetOverTCP version 1 implementation
26
+ * Support for the LocoBuffer-USB
27
+
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,11 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ lib/nickel-silver-server.rb
7
+ lib/LocoBufferUSB.rb
8
+ lib/LocoNetServer.rb
9
+ test/test_helper.rb
10
+ test/test_nickel-silver-server.rb
11
+
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ = nickel-silver-server
2
+
3
+ * http://github.com/toholio/nickel-silver-server/
4
+
5
+ == DESCRIPTION:
6
+
7
+ A Ruby server implementing the LocoNetOverTCP protocol allowing remote clients to connect to Digitrax based model railway layouts. Currently supports version 1 of the protocol.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Complete support for LocoNetOverTCP version 1
12
+ * Multithreaded server based on GServer
13
+ * Easily extended to use new hardware interfaces
14
+
15
+ == SYNOPSIS:
16
+
17
+ require 'rubygems'
18
+ require 'nickel-silver-server'
19
+
20
+ # connect to a LocoBufferUSB on the virtual serial port /dev/tty.serialport
21
+ interface = LocoBufferUSB.new( '/dev/tty.serialport' )
22
+
23
+ # create a server using the default port (i.e. 5626, 'loco' spelt on a phone keypad)
24
+ # using our freshly connected LocoBuffer-USB
25
+ server = LocoNetServer.new( interface )
26
+
27
+ # start the server
28
+ server.start
29
+
30
+ # wait for the server to stop before exiting
31
+ server.join
32
+
33
+ == REQUIREMENTS:
34
+
35
+ * ruby-serialport is needed to connect with LocoBuffer-USB hardware http://rubyforge.org/projects/ruby-serialport/
36
+
37
+ == INSTALL:
38
+ If you have not added GitHub as a gem source you will need to do so first:
39
+ * gem sources -a http://gems.github.com
40
+
41
+ To install the actual gem:
42
+ * sudo gem install toholio-nickel-silver-server
43
+
44
+ == LICENSE:
45
+
46
+ Nickel Silver is distributed under the same terms as Ruby.
47
+
48
+ Copyright (c) 2008 Tobin Richard
@@ -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 = NickelSilver::Server::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 = NickelSilver::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,62 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'locobufferusb'
4
+ require 'loconetserver'
5
+
6
+ module NickelSilver
7
+ module Server
8
+
9
+ # An interface class should provide the following:
10
+ # * Store incoming packets as FixNums representing bytes in a buffer array
11
+ # * Send outgoing bytes (represented as FixNums in a buffer array) to LocoNet
12
+ # * Use a Mutex to lock access to the buffers when in use (remember Nickel-Silver is multithreaded)
13
+ # * Provide a method that causes your interface to start collecting packets
14
+ #
15
+ # The interface is simple. Only the following public methods are needed:
16
+ # * Accessors for input_buffer, output_buffer and io_mutex
17
+ # * run() which starts buffering
18
+ #
19
+ # How you do this will depend upon your hardware. Take a look at the LocoBufferUSB class to get
20
+ # an idea of how it might be done.
21
+ #
22
+ # A stub driver might look like the following...
23
+ #
24
+ # class SomeLocoNetInterface
25
+ # attr_accessor :input_buffer, :output_buffer, :io_mutex
26
+ #
27
+ # def initialize
28
+ # # these may be modified at any time by the server
29
+ # @input_buffer = []
30
+ # @output_buffer = []
31
+ #
32
+ # # only make changes when locked using @io_mutex
33
+ # @io_mutex = Mutex.new
34
+ # end
35
+ #
36
+ # def run
37
+ # loop do
38
+ # # get incoming bytes
39
+ # if byte_waiting?
40
+ # @io_mutex.synchronize do
41
+ # # byte getting code here
42
+ # @input_buffer << get_byte
43
+ # end
44
+ # end
45
+ #
46
+ # # send outgoing bytes
47
+ # until @output_buffer.empty? do
48
+ # @io_mutex.synchronize do
49
+ # # send a byte
50
+ # send_byte( @output_buffer.shift )
51
+ # end
52
+ # end
53
+ # end
54
+ # end
55
+ #
56
+ # end
57
+ #
58
+ module Interface
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,26 @@
1
+ SPEC = Gem::Specification.new do |s|
2
+ s.name = 'nickel-silver-server'
3
+ s.version = '0.0.5'
4
+ s.summary = 'LocoNet over TCP server.'
5
+ s.description = 'LocoNet over TCP server written in ruby.'
6
+ s.author = 'Tobin Richard'
7
+ s.email = 'tobin.richard@gmail.com'
8
+ s.homepage = 'http://github.com/toholio/nickel-silver-server/'
9
+ s.files = [ 'History.txt',
10
+ 'License.txt',
11
+ 'Manifest.txt',
12
+ 'README.txt',
13
+ 'nickel-silver-server.gemspec',
14
+ 'lib/LocoBufferUSB.rb',
15
+ 'lib/LocoNetServer.rb',
16
+ 'lib/nickel-silver-server.rb']
17
+
18
+ s.test_files = [ 'test/test_helper.rb',
19
+ 'test/test_nickel-silver-server.rb' ]
20
+
21
+ s.require_path = 'lib'
22
+ s.autorequire = 'nickel-silver-server'
23
+
24
+ s.has_rdoc = true
25
+ s.extra_rdoc_files = ['History.txt', 'Manifest.txt', 'README.txt']
26
+ end
@@ -0,0 +1,2 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/nickel-silver-server'
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestNickel-silver-server < Test::Unit::TestCase
4
+
5
+ def setup
6
+ end
7
+
8
+ def test_truth
9
+ assert true
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toholio-nickel-silver-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Tobin Richard
8
+ autorequire: nickel-silver-server
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-13 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: LocoNet over TCP server written in ruby.
17
+ email: tobin.richard@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - History.txt
24
+ - Manifest.txt
25
+ - README.txt
26
+ files:
27
+ - History.txt
28
+ - License.txt
29
+ - Manifest.txt
30
+ - README.txt
31
+ - nickel-silver-server.gemspec
32
+ - lib/LocoBufferUSB.rb
33
+ - lib/LocoNetServer.rb
34
+ - lib/nickel-silver-server.rb
35
+ has_rdoc: true
36
+ homepage: http://github.com/toholio/nickel-silver-server/
37
+ post_install_message:
38
+ rdoc_options: []
39
+
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 1.2.0
58
+ signing_key:
59
+ specification_version: 2
60
+ summary: LocoNet over TCP server.
61
+ test_files:
62
+ - test/test_helper.rb
63
+ - test/test_nickel-silver-server.rb