Thursday 25 November 2010

Printing Labels

I recently had a need to print labels from our database. The factory produces samples; the idea is that they enter the samples into the database, which spits out a label to stick on it. Analysts then update the record as the results come in.

After some rather lengthy discussions with my IT supplier we went for a Zebra GK420t label printer. The accompanying CD has a manual for programming in a language called ZPL, allowing me to set up my labels with complete flexibility.

Configuring a Printer Port

I am in a Windows environment, and some of this is specific to Windows.

The general strategy is to use Ruby to create a file, and then copy the file to the serial port. Most PCs nowadays do not have a serial port, but you can pretend the printer is on a serial port using the "net use" command at the DOS prompt. It is first necessary to share your printer on the network. Then, at the server where your project is run from, type something like this (comp01 is the computer's network name, zebra is the share name for the printer):
net use lpt4 \\comp01\zebra /persistent:yes

You can check if the printer is still assigned to that port using "net use".

If only it were that simple. Windows runs services in their own environment, so setting the printer at the prompt is great for when you are developing the project, as you run your web server from the command prompt. Then you go live, and now the project is run as a service on Tomcat or whatever, and has no clue about what you have done at the command prompt.

The way I got around this was to put code into a file in config/initial, like this:
printer = File.read('config/printer.cfg').strip
command = "net use lpt4 \"#{printer}\""
`net use lpt4 /delete`
result = `#{command}`

Everytime Rails starts up, this reads the printer name from a file called config/printer.cfg, deletes the old setting and applies the new.

Printing

Using the "copy" command, you can now send a file to the printer. I think it is useful to try that before getting into Ruby to ensure that works, so here is an example ZPL file (for labels 100x50 mm).
^XA
~SD15
^LL200
^PW750
^FO50,60^ADN,36,40^FDMy first line^FS
^FO50,110^ADN,36,20^FDA second line^FS
^FO50,160^ADN,36,20^FDThe third line^FS
^FO50,220^ADN,18,10^FDMy company^FS
^FO50,245^ADN,18,10^FDCreated at 1847 on 13/Sep/10^FS
^FO35,50^GB680,160,2^FS
^XZ
END

Here is some Ruby code that will generate a new file on the fly, and send it to the printer. Note that ZPL does not require that data is sent in the order in which it appears on the label.
require 'date'

class Printer
PRINTER = 'lpt4'
COMPANY = 'My Company Ltd'

def self.print_label lines
# Accept either a string with line breaks embedded,
# or an array of strings
# (assume each line is not too long)
lines = lines.split("\n") if lines.is_a? String

# A "here document" defines the basic label,
# With a box, date and company name
text = <<-END
^XA
~SD15
^LL200
^PW750
^FO50,#{70 + 50 * lines.length}^ADN,18,10^FD#{COMPANY}^FS
^FO50,#{95 + 50 * lines.length}^ADN,18,10^FDCreated at #{DateTime.now.strftime('%H%M on %d/%b/%y')}^FS
^FO35,50^GB680,#{10 + 50 * lines.length},2^FS
^FO50,#{60}^ADN,36,40^FD#{lines[0]}^FS
END

# Each line sent is now added
(1..lines.length).each do |i|
text << "^FO50,#{60 + 50 * i}^ADN,36,20^FD#{lines[i]}^FS\n"
end

# The file terminator is tagged on the end
text << "^XZ"

# The file is created
File.open("print.txt", "w") { |file| file << text }

# In Rails, stop at this point if in a unit test
return 'not printed' if RAILS_ENV == "test"

# Create a DOS command
command = "copy #{RAILS_ROOT}/print.txt #{PRINTER}"

# Usually Ruby will convert forward slashes to backslashes
# in Windows, but not here
command.gsub! '/', '\\'

# Use backticks for the system command
# so we can capture the output
result = `#{command}`

# Printed okay
return 'label printed' if result =~ /1 file\(s\) copied/

# Problem, so return the error message and command
"Label failed with error \"#{result}\", command was \"#{command}\""
end
end


Printer.print_label ["My first line", "A second line", "The third line"]

You may want to consider whether two people might try to print at the same time. If the second writes a new file before the first has printed it, both users will get the second label. My understanding is that Rails handles one request at a time, so I do not think this is a problem, and for me, uses will only be printing from a single computer anyway.

Struggling with Ruby: Contents Page

Saturday 6 November 2010

Monkeybars, a UI framework

Finding a decent UI for use with Ruby is something of an on-going quest. I blogged a couple of years ago about Ruby Shoes, which is a very neat idea, but is limited, and not really suited to big projects. I am not too sure how well it is being supported nowadays, though there is some activity at GitHub.

So recently I was looking at Monkeybars. Monkeybars is not a UI as such, but a framework for using Swing inside an IDE. It attempts to hide all the underlying Java, so you just design your interface through your IDE, and the rest is Ruby (specially, JRuby of course, for Swing).

There is a problem with Monkeybars; it does not work with recent versions of JRuby. I found an older version in an example file, and that works old (I have reported this as a bug here).

Installation

Installing is simple (this will also install the "rawr" gem):
gem install monkeybars


Create a project

To create a new project:
monkeybars my_project
cd my_project
rawr install

You than need to set it up in your IDE. Monkeybars was designed with NetBeans in mind, and that is the IDE I use too (it obviously needs both Ruby and Java included). It needs to be a Java project, as you are actually running a Java application that uses Ruby, so your new NetBeans project will be a "Java Project with Existing Sources". You need to point NetBeans to the source packages in the "my_project/src" folder. Once the project is created, right click on "Libraries" in the project browser, select "Add JAR/Folder" and select the .jar files in "my_project/lib/java".

When you first run your project, NetBeans will ask for the main file; select "org.rubyforge.rawr.Main"

Create your model-view-controller

Just like Rails, you can use a rake task to generate these (in ths example, called "main"):

rake generate ALL=src/main


This will generate the files:
* src/main/main_controller.rb
* src/main/main_view.rb
* src/main/main_model.rb

Create the UI

You actually need a fourth file, which is the UI itself. In the IDE, right click on the folder (in this case "main") in the project browser, and select New - JFrame Form, and give it a suitable name (I chose MainJFrame). You can now add components graphically, using your IDE. The important point to remember is that any component you want your Ruby code to interact with should have a Ruby-friendly name. A "Quit" option on the file menu might be "quit_menu_item", but the menu itself you can leave to the default.

To keep it simple, drag a label on to the dialog box, and set the variable name (found under the code tab) to "message".

Edit the view

The main_view.rb file is the glue between the Java UI and your Ruby code. The first thing it needs is the name of your Java UI. Then it needs to know how the components on the UI map (or more accurately, properties of the components) to the model. In this example, then we need only two lines.
class MainView < ApplicationView
set_java_class 'main.MainJFrame'
map :model => :message, :view => "message.text"
end

You can have as many map statements as you need, one for each component that displays data (not required for buttons, etc. that generate events only). The :view part means that one end of the link is to the text property of the JLabel that we called "message". The other end of the link is the message property in the model.

You can set a mapping to be one way, by adding a :using value. In our example, the label cannot be edited directly by the user, so we only want the data to go from the model to the UI component, not the other way around (you can flip "nil" and ":default" to have the data go the other way only, but your model needs to provide read and write access to the property even so, because of the way Monkeybars creates a new model with the view data, then transfers data from that to the real model).
map :model => :message, :view => "message.text", :using => [:default, nil]

You can also use the ":using" value to specify a method to convert the data; give the method name instead of :default.

Edit the controller

The controller needs to be told the name of the model, and the name of the view. You can also set an action for when the dialog close is clicked. All of this is done for you, and is all we need for this simple application.
class MainController < ApplicationController
set_model 'MainModel'
set_view 'MainView'
set_close_action :exit
end

However, if you have any way for the user to interact with your dialog box, that gets captured here. Let us suppose you have a "Quit" option on yor file menu, and you have set the variable name to "quit_menu_item". This method will respond to that menu item being selected, and ask for confirmation.
def quit_menu_item_action_performed
r = javax.swing.JOptionPane.showConfirmDialog(nil,
"Do you really want to quit?",
"Confirmation",
javax.swing.JOptionPane::YES_NO_OPTION,
javax.swing.JOptionPane::QUESTION_MESSAGE)
exit if r == javax.swing.JOptionPane::YES_OPTION
end

Here is another example for a button; the user presses the button and the text in two JTextAreas is used to update the model (the first JTextArea is called "text_area_1", but mapped to "text1" in the model, using map :model => :text1, :view => "text_area_1.text" in the view). The update_model method is used to transfer data to the model. Then the update method in the model is called (this is a method I have written, to do what I need in my model). Finally, update_view is called so the UI is updated to reflect the new state of the model.
def update_button_action_performed
model.text1 = view_state.model.text1
model.text2 = view_state.model.text2
model.update
update_view
end

What happens is that calling view_state creates a new instance of the model, and this is populated with the values from the UI. You can then copy across the values you want into the real model. A convenience method, update_model, can be used instead.
def update_button_action_performed
update_model(view_state.model, :text1, text2)
model.update
update_view
end

This is how the controller will handle most events, first transfer the data from the UI to the model, then call a method in the model, then update the view, so you could have one method to create a whole set.
%w(up down edit cut paste insert).each do |action|
%w(button menu_item).each do |type|
class_eval <<-"END"
def #{action}_#{type}_action_performed
model.text = view_state.model.text
model.#{action}
update_view
end
END
end
end

You can set the value for set_close_action to :nothing, :exit, :close, :dispose or :hide. If you want other functionality, override one of those methods in the controller (I found I could override close, but not exit).

Edit the model

The model needs to include accessor methods for all the properties you mapped to in the view, and methods for all the actions. In our simple example, like this:
attr_accessor :message

You can set properties through properly defined methods, but that is something of a minefield, I have found, and better avoided.

There is no more to say about the model; Monkeybars makes no assumptions about it, and it has no parent class to inherit from (other than Object). This is where you do the real work.


Struggling with Ruby: Contents Page