Greetings. I have been studying Ruby, have the pickaxe and Ruby Way
(Fulton). Studying for a long time as I am a diehard C/assembly
programmer and it is very hard to get out of the data-driven mindset.
I believe to have finally found my killer application for Ruby (not
rails, or the web.) I am however unfamiliar with OOP-isms and am
trying to come to grips with them. Any assistance or direction anyone
could help me with, I appreciate!
PS: The first time I coded this, it worked without a hitch. A real
testament to Ruby.
The application: Picture a grid of NTSC monitors controlled by Apple
II computers. Each Apple II has a serial card in it, hooked up to a
portmaster, connected to a LAN and showing up on the local system (the
Console) as virtual ttys a la /dev/ttyr1, etc. The Apples are running
custom assembly routines, triggered by a write to the serial port from
the Console to the tty port. (I figured Ruby wouldn’t be able to
handle this kind of IO. Dead wrong, me!) The routines make the Apple
monitor do different things - I can Fill the screen with a letter,
Draw a horizontal or vertical line, Display a compressed image stored
on the machine, etc. For synchronization, when the Apple is done, it
sends a ACK bit to the serial port.
Here is what I have now, that works:
#name #port #y value #x value
MATRIX1 =[
[“iip1”, “/dev/ttyr2”, 0, 0],
[“iip2”, “/dev/ttyr3”, 0, 1],
[“iic”, “/dev/ttyr1”, 0, 2] ]
MATRIX2 = [ [“iie”, “/dev/ttyr5”] ]
MATRIXES = [ MATRIX1, MATRIX2]
class Matrixes is a simple array (@store = []) and holds each
DisplayMatrix. Each DisplayMatrix has its own group of computers and
may have different properties - for example the first three machines
are black and white, and in a row, so they make up a x,y display (40 *
3) by (24 * 1). The second matrix has one machine in it and is
color. A visualization can receive any number of these matrixes.
class Matrixes
def initialize
@store = []
end
def <<(data)
@store << data
end
def each
@store.each{ |mx| yield mx }
end
def flush_and_wait
threads = []
@store.each{ |mx| threads << Thread.new{ mx.flush_and_wait } }
threads.each{ |t| t.join }
end
end
class DisplayMatrix holds an array of machines and has routines to
flush an entire matrix, and wait for all machines to be finished.
class DisplayMatrix
def initialize(machines)
@machines = machines
end
def flush_and_wait
threads = []
@machines.each{ |m| threads << Thread.new{ m.flush_and_wait } }
threads.each{ |t| t.join }
end
class Machine is one machine, and does all the communication with the
serial port and keeps the object in a current state. It receives
command requests and stores them on a queue.
class Machine
def initialize(name, port)
@name = name
@dev = open_and_init(port) # calls open, also uses Termios library
@q = []
@q.extend(MonitorMixin)
end
def enqueue(data)
@q.synchronize{ @q << data }
end
def flush_and_wait
@q.synchronize do
@q.each{ |q| @dev.syswrite(q) }
@q.clear
readyfd = select([@dev], nil, nil)
raise "select on our fd didn't yield it?" if not
readyfd.flatten.include? @dev
@dev.sysread(100) #read & toss acknowlegement, right now simply a
SPACE
end
end
Here is how I have a visualization currently. You will see I can make
a visualization with custom parameters - in this case how many times
to loop it. Other variables I can think of are how long to flash it,
which character to flash it with, etc. So I must “create” a new
FlashVis object based on custom parameters and how I want to run it.
Queueing the letter F, and then an ascii SPACE, makes that Apple II
have a white on black screen.
Queueing the letter F, and then hex A0, blanks the Apple II screen (it
is an apple II normal space).
When I do a flush on the entire visualization matrix I wait for all
Apples to be done. This creates O(n) threads as you can see, but
seems to have negligable performance impact.
BlinkVis simply blinks the Apple II screens
class BlinkVis
def initialize(count)
@count = count
end
def run(mxs)
return Thread.new do
@count.times do
mxs.each{ |mx| mx.queue_to_all("F ") }
mxs.flush_and_wait
mxs.each{ |mx| mx.queue_to_all("F\xa0") }
mxs.flush_and_wait
end
end
end
end
And then the main program where you can see all the objects created
cleanly:
begin
mxs = Matrixes.new
MATRIXES.each do |matrix_def|
ms = []
matrix_def.each{ |name, port| ms << Machine.new(name, port) }
mxs << DisplayMatrix.new(ms)
end
vis = BlinkVis.new(3)
while true do
thr = vis.run(mxs)
thr.join
end
end
****************************** (end of program code)
**************************8
A few things are apparent about this and I must say - FIrst of all
this code does exactly what my C code does with about 10x less the
amount of lines due to ruby’s excellent hierarchical error passing and
excellent array support, as you can imagine eliminating each “if fcntl
< 0 then error… if tcgetattr < 0 then error… if select < 0 then
error… if read < 0 then error…” took a crap load of code away.
Secondly the code is extremely easy to parse.
Now that it does what my server coded in C does, I would like to
know: What is the best way to abstract away the “intention” of the
command from the “actual” command I’m sending to the Apple? (eg,
machine.enqueue( FLASH, A2_SPACE | A2_INVERSE ) instead of
machine.enqueue(“F\xa0”)
machine.enqueue( FLASH, A2_SPACE | A2_NORMAL ) instead of
machine.enqueue("F ")
EG: The 6502 code also prints vertical lines “V” and horizontal lines
“H” with three parameters: x from 0 to 39, y from 0 to 23, and
length. It prints a compressed text screen block “E” with parameters
which char to use for the light bits, which char for the dark bits,
and the block. It will make a kaleidoscope in lo-res graphics by
passing “K”. I can picture many more modes I will program in 6502 -
for example, printing text in varying sizes and fonts on the hi-res
screen,
etc.
machine.enqueue( KALEIDOSCOPE, optional starting_color )
machine.enqueue( HLINE, starty, startx, length, optional color )
machine.enqueue( TEXT, “The quick brown fox”, FONT_BLA, 16 )
So with all these different commands, and parameters, what is the best
way to “enqueue” one of these commands to the machine for sending/
updating? And I would like to be able to handle different kinds of
Machine in the future - it could be an Apple II, or it could be a dumb
terminal, or possibly a NES system with custom cartridge & serial
connection. In each case the output to the machine would be
different. The way I have done it so far seems to imply I can do what
I am asking here. (And a big pain in the butt in C, which is why I
stopped and did this!!) Ruby example code would be helpful!
PS: If any of you are II fans and would like to use this I will be
puting a webpage together with the assembly and all necessary
materials. II Infinitum!
-m