Introduction
MobileLPR is an LPR client library built for the .NET Compact Framework 2.0 that gives a Windows Mobile application the ability to print to an LPR (a.k.a. LPD) server, an LPRng server, or a Socket API printer. One thing all these protocols have in common is the use of a TCP socket to transport data. With the length of time the LPR protocol has been available, the prevalence of print server appliances and printers with internal network interfaces, and the dearth of printing solutions for mobile devices, it surprised me somewhat how little code was actually available on the web to do printing from mobile devices.
Why did I build it? After about a week of web searches yielding meager results, and as my mental digestion of the relatively small LPR document improved, I decided this situation presented me with a good opportunity both to create the functionality I needed and to give a useful bit of code back to the programming community, of which I had been a beneficiary for so long.
The goal was to assemble a small, well-performing library to run in a mobile environment that contained the most useful LPR functions without being over-engineered. Some sections certainly could have been written more efficiently but that do their intended tasks. Coming from a C programming background, I know how to write very compact and cryptic code that gets the job done, but in six months, even I wouldn't be able to decipher it. I aim to write maintainable code.
Background
My career for the past couple decades largely revolved around making mobile devices work productively for small and large businesses. Every year or two brought a new herd of devices, some with new capabilities, faster hardware, or simply repackaged in novel and useless ways. I watched the industry take a big step forward every five years, bringing not just bigger memory and faster processors, but actually putting more capabilities into those palm-size computing wonders. In the late 1990s, Microsoft introduced a new Operating System, Windows CE, that commoditized the role of personal hand-held devices and opened up the formerly-proprietary world of portable computing to the general programming population. About a decade later, echoing the growth of the Internet, these devices evolved to include network connectivity and many of the functions of less portable platforms. Today, nearly everyone uses a wireless device of some type that can be programmed nearly identically to its industrial siblings.
Displacing multitudes of special-purpose devices used for fixed-station data collection, scanning, and printing, families of isolated and single-function components merged into the general-purpose platforms in use today. Many common features have greatly improved in quality and ease-of-use, but printing is something that time nearly has left behind when it comes to mobile computing platforms. This deficiency is what MobileLPR was written to address.
In my pursuit of rewriting a system for a long-time loyal customer, I was faced with the requirement of replacing a wireless printing station that had been cobbled together from an industrial label printer and a small "brick" outfitted with a wireless radio and an embedded program. This printing node took its directions from a custom written PC server to service special inventory labeling requests for a small team of users outfitted with mobile computers.
Affordable printers are now available that either contain their own internal print servers or can be equipped with external print servers that can communicate with several standard printing protocols. They can be dropped onto a network, wired or wireless, and require nearly no additional infrastructure or ongoing administration to operate. This is the kind of device that was selected to become the label station for the new system, and created my need: an interface that allowed me print to a networked printer via a standard protocol.
Before jumping into this project, I found only a few Windows Mobile printing client software packages that sounded like they might work for me. After reading through their information, familiarizing myself with the LPR protocol, and staring at an empty wallet, I couldn't justify paying several hundred dollars for a commercial package to solve such a relatively simple task. Thus was born the idea of writing the MobileLPR library for fun and (not for) profit.
I found several indispensable documents on the web regarding LPR. The primary documentation was RFC 1179 - Line printer daemon protocol, a guide to the existing LPR protocol, mainly as it applied to BSD Unix servers. It was not a specification and left much to the imagination, but provided a fundamental understanding of the command set and data flow to be followed.
The greatest asset in my effort was the excellent LPRng SourceForge Project, "an enhanced, extended, and portable implementation of the Berkely LPR protocol" authored by Patrick Powell. Though not documented exhaustively, there was ample practical information about how LPR worked, decisions about how LPRng was implemented, and how it dealt with critical instructions that were missing from RFC 1179. The availability of Mr. Powell's source code and his treatise on printing made an otherwise difficult job fairly simple. Thank you, Mr. Powell!
The end result summarizes the protocols supported by MobileLPR:
- LPR
- Server port: default 515. May be any.
- Local port: reserved ports 721-732.
- Print queue: default "printer". Must name a valid queue on the server.
- LPRng
- Server port: default 515. May be any.
- Local port: reserved ports 512-1023.
- Print queue: default "printer". Must name a valid queue on the server.
- Direct (Socket API)
- Server port: default 9100. May be any.
- Local port: no reserved port required.
- Print queue: not applicable.
How to use MobileLPR
The MobileLPR library was written to be simply a DLL attached to an application program. Referencing the DLL from your Visual Studio SmartDevice project makes all the power of its classes available to your program. Though anticlimactic, there really is only one class you need to be concerned with: LprJob
. It is this class that is responsible for accepting the printing parameters and data files, organizing them to create a print job that is prescribed by the protocol in use, and sending the data to the print server.
The first place to start is with the print server, which nowadays is usually built into the printer, to identify what capabilities it supports. For simplicity, the remainder of this article will not distinguish between internal and external print servers, and will refer to them collectively as "print server". Some offer Socket API, some LPD/LPR, and some offer both. Even if your printer does not have network capability, there are commercially available print server appliances that are inexpensive and provide either a direct socket connection or LPR; even some wireless routers and NAS devices have them. No matter what physical device houses the print server, MobileLPR can talk to it.
Once you have identified the print protocol to use, you need the address of the server. If you are on a network that provides dynamic DHCP services, you can configure the print server with a name and allow it to be assigned any address. If your print server is obtaining an address without dynamic DNS being available, either tie the IP address to the MAC address, or assign your print server a static address. Whether accessed by name or by numeric IP address depends on what your mobile device is capable of. A numeric IP address should always work.
The printer queue name is the next piece. In practice, there are no common naming conventions. Some print servers, especially the external ones, are capable of servicing multiple printers from one unit. In these devices, selection of the output printer is made either by individual port number or by separate print queues for each printer or interface. Queue names like "P0", "P1", "S0", or "S1" are not uncommon for communicating with printers connected to parallel and serial interfaces, respectively. Some print servers allow you to configure whatever name you want and attach it to the required interface. How to configure a print server is beyond the scope of this article, so I will be using the nondescript queue name of "printer" in my examples.
The LprJob
class separates job setup into three logical areas: destination, printable data, and monitoring. Each instance of an LprJob
object contains all the information necessary to connect to a print server, transfer the data to be printed, and know when that transfer is complete. It also provides the barest queue control operations as defined by the LPR protocol. Printable data is supplied as a list of one or more local data files. The LprJob
class provides several events that can be used to monitor the progress of the print job.
Identify the Print Job Destination
The destination information identifies the print server and port number to connect to, the name of the printer queue to use, if any, and the method of transferring the data.
The most straightforward means of specifying a print job's destination is through the SetPrinterURI
method, identifying all the components with a single string. The general syntax of the string follows the generic Uniform Resource Identifier specification that can be found as RFC 3986 and Uniform Resource Identifier (URI): Generic Syntax. For MobileLPR, its syntax is: protocol://[userinfo@]servername[:serverport][/printername]. Note that the SetPrinterURI
method may generate a UriFormatException
if the string is formatted incorrectly.
MobileLPR uses the scheme, authority, and path components of the URI string. The scheme component identifies the job transfer protocol. Easily readable protocol names (and aliases) are used to select the job transfer method to employ:
Scheme
|
Protocol
|
direct
|
Use a direct Socket API (TCP) connection to the specified print server. Standard port is 9100.
|
lpd
|
Same as lpr.
|
lpr
|
Use standard LPR protocol. Standard port is 515. This is the default protocol used if none (or an invalid scheme) is specified.
|
lprng
|
Use LPRng parameters in the control file, use expanded reserved local port numbers, but perform a basic LPR transfer. Standard port is 515.
|
raw
|
Same as direct.
|
The authority component contains the name of the print server, a non-default port number to use to establish a connection, and user information in case the print server requires authorization to execute the print job. For example, some LPR servers on Linux platforms may restrict printing to certain users, and hold or discard jobs for all others; supplying the user name gives the print server the information it needs to set appropriate permissions for printing. The default user name used is "MobileLPR".
If your program is running on a device that has DNS resolution ability, the server name can be specified as an alphanumeric domain name. Otherwise, it may be safest to use a numeric IP address here. Your network administrator should be able to tell you what services are available.
Port number may be optional. If the print services use the standard ports, selecting the protocol through the URI also sets the standard port number. If your print services are using non-standard ports, or you assign the connection parameters individually, the correct port number must be supplied for a successful connection.
The path component sets the name of the print queue that will service the print job. Depending on your print server, this name may or may not be case-sensitive. The print server documentation should contain the information you need. If in doubt, use the printer queue name exactly as it is configured in your print server.
Alternatively, the Protocol
, ServerName
, ServerPort
, UserName
, and PrinterName
properties can be set individually. If you anticipate sending multiple jobs to the same destination, the DefaultProtocol
, DefaultServerName
, DefaultServerPort
, and DefaultPrinterName
properties can be set up first, so any new LprJob
class instance will be created with those values already assigned.
Example: to send the print job using LPR protocol to a print server named "lprserver", serviced by print queue "P1" on port 715, allowing printing by user "printuser", you can use:
LprJob job = new LprJob();
job.SetPrinterURI("lpr://printuser@lprserver:715/P1");
A simpler example is printing over a direct socket connection to a printer with an internal print server using all default information. The print server address is 1.2.3.4:
LprJob job = new LprJob();
job.SetPrinterURI("direct://1.2.3.4");
Attach the Data Files
Now that the print server has been specified, it is time to attach the data files to the job. The AddDataFile
method taking a single argument attaches a named data file to the job as a literal (raw, untranslated) file. That means the data that will be sent to the printer must be exactly as the printer expects it. An overload of the AddDataFile
method allows specifying the format used for printing the file, selectable from the standard data formats for which LPR was designed. In most LPR installations, the format controls the filters that data will be passed through to get the desired printer output.
Briefly, the formats may be broken down into two groups: literal (binary, raw, or untranslated) data sent with the Literal
and Raster
formats; and text data sent with all other formats. MobileLPR passes literal data unchanged. Text formats assume the data is of a textual nature, and data is translated from the native text format of the mobile device into ASCII text that LPR servers expect to receive. The data content represented by the text is not changed in any way. With either literal or text data, the application is responsible for generating the appropriate content.
Format
|
Content
|
Literal
|
Literal (untranslated, binary) file.*
|
CIF
|
Caltech Interchange Format plot file.
|
Ditroff
|
ditroff output file.
|
DVI
|
DVI (TeX output) file.
|
Formatted
|
Plain text file to be formatted.
|
Fortran
|
Plain text with FORTRAN carriage control.
|
Paginate
|
Text to process through the Unix 'pr' (paginate) command.
|
Plot
|
Berkeley Unix plot file.
|
Postscript
|
Standard Postscript file.
|
Raster
|
Sun raster image file.*
|
Troff
|
Graphic Systems C/A/T phototypesetter file.
|
*Data is sent without any translation.
|
The LPRng protocol limits the number of data files to 52, which corresponds to the number of index letters available for remote file names. The LPR protocol does not specify a limit, but many servers cannot handle more than one data file per job. No such restriction is enforced for the Direct protocol.
One difference where MobileLPR may diverge from the standard protocol is allowing multiple copies of the files to be printed, because there is no clear documentation concerning it. Copies
specifies the number of copies to print for all data files in the job, and is not for individual files. Copies
processing is done by MobileLPR. In the case of the LPR protocol, the entire job is sent iteratively for as many copies as are specified. For LPRng, copies are handled by including each data file multiple times within the job. For Direct, each data file is sent multiple times over the same socket connection. In practice, I don't think this design choice poses any problems with standard print servers, but that is an assumption on my part.
The TestMobileLPR sample program included as part of this article provides a typical example that shows the correct steps necessary in using the library.
Print Job Monitoring
A design consideration when putting together this library was maintaining a responsive user interface. Keeping this in mind, the SubmitJob
method that is responsible for organizing and sending the print job to the print server executes the whole transfer process on a background thread. Several mechanisms are provided for determining when the job transfer is complete.
The simplest mechanism is the IsComplete
property. This is a flag that is set to false while an operation is in progress, and set to true when the operation is complete. This property can be used periodically for polling to see when the job is done.
Another mechanism provided is the WaitForCompletion
method. An internal synchronization object reflects the state of the IsComplete
property. This method waits until the synchronization object is signaled, so no polling has to be done. Calling this from your UI code will suspend user input/output until the print job completes.
The final monitoring mechanism is a related group of events named LprJobStarted
, LprJobProgressChanged
, and LprJobCompleted
. Two parameters are passed to any specified event handler. The first parameter is the LprJob
object that raised the event; the second is an instance of LprJobProgressEventArgs
, populated with the total number of bytes to be sent for the jobs (counting bytes in the local data) and the cumulative number of bytes sent at the time the event was raised. The LprJobProgressChanged
event also passes the local and remote pathnames (in the case of LPR and LPRng protocols) of the data file being transferred.
The LprJobStarted
event is raised once when the job transfer begins. The Total
event argument property will contain the total number of bytes to be transferred, which your application can use to set up a progress indicator.
The LprJobProgressChanged
event is raised every time a new block of data is sent to the server. The LprJobProgressEventArgs
event argument properties are populated to reflect the current status of the print job.
Property
|
Content
|
Total
|
Total number of bytes to be sent for the entire print job.
|
Used
|
Number of bytes sent so far.
|
Pathname
|
Local pathname of the current file being transferred.
|
ServerFilename
|
Remote filename of the current file being transferred.
|
Command
|
LPR command number being executed.
|
The LprJobComplete
event is guaranteed to be raised once at the end of the job, whether it succeeded or failed. The Used
event argument property is the number of bytes that had been sent when the job ended. The LprJob
object should not be disposed of while any operation is pending; in other words, wait until IsCompleted
is true.
The LastException
property records any errors encountered. When the job is completed, it will contain the terminating exception, if any. No exceptions are raised after a job has been started, and this is the only mechanism available to determine if the job failed. How a job failure is handled is up to your application.
Supported Print Job Commands
The LPR protocol specifies additional commands used for supporting print jobs. For the Direct protocol, only the SubmitJob
method performs any real function.
Method
|
Function Performed
|
SubmitJob
|
Transfers the data files to the print server. For LPR and LPRng, this takes care of creating and removing the control file and sequencing the commands and data stream according to the protocol. Launches a background thread to perform the work.
|
PrintWaitingJobs
|
Simply tells the print server to start servicing the queue, if not already running. Usually unnecessary.
|
GetQueueStatus
|
Requests a list of queued jobs from the print server. A short and a long format are available. The structure of responses is undefined, and will vary by print server. Specific job numbers may be listed to restrict the output to those jobs only. Job numbers must be those generated by the server.
|
RemoveJobs
|
Removes jobs from the print queue. Specific job numbers may be listed to restrict removal to those jobs only, otherwise the currently printing job is removed, if permitted. Job numbers must be those generated by the server.
|
Launching the Print Job
When the connection has been defined, the data files attached, and the monitoring set up, you can launch the print job. This is simply done by calling the SubmitJob
method. Always call command methods within a try-catch
block. Any errors, whether originating from MobileLPR or the Operating System, will generate exceptions. After a job is launched, exceptions will be captured in the LastException
property to be examined by your application after the job transfer is completed.
Points of Interest
When the printer's marketing material states that it supports LPD/LPR protocol, be suspicious. Unless the document clearly defines how to go about using it, don't believe it. In my case, the slick advertising sheet said in plain language that LPD/LPR was supported by the printer; however, in real life, there wasn't a single scrap of information about it in the printer's manual. Trial and error showed the printer's network interface responded on port 515. It would accept a print job and return status messages, but regardless of every print queue name I tried (including queue names taken from related manuals), it wouldn't print, and the status messages never changed. After all the work I put into my pretty little project, the printer was deaf to LPR.
A maximum of 52 data files (including copies) may be sent in one LPRng print job. Though RFC 1179 is silent on this point, some LPR servers can handle only a single data file per job. The LPRng limit is enforced for LPR and LPRng protocols, and will ignore additional files.
Some print servers take RFC 1179 very literally and treat its suggestions as gospel. With these print servers, specifying remote control files or data file names that do not strictly adhere to the guidelines result in a failure to output. The job may be accepted, but nothing will print. My advice is to stick to the LPR protocol when you don't know if LPRng extensions are supported.
I implemented only a few of the many LPRng enhancements: expanded reserved ports for the client side of the connection; the "A" unique job identifier and "Q" original printer queue name parameters in the control file; one of the multiple-copies mechanisms by sending a data file more than once distinguished by a ".Cn" suffix on additional copies for remote filenames; LPRng error messages for rejected jobs. Other features I considered unnecessary, like manipulations of queues on the server, I decided to omit.
Sample Usage
The following snippet of code illustrates how basic you can get with this library. This is a Windows Forms application for a Smart Device. It has one form with a button on it to print a two-line text file created by the program.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using MobileLPR;
using System.IO;
namespace LprSample
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Exception lastex = null;
LprJob job = null;
try
{
String tempname = Path.GetTempFileName();
StreamWriter file = new StreamWriter(tempname);
file.WriteLine("Hello, LPR server!");
file.WriteLine("How are you?");
file.Close();
job = new LprJob();
job.SetPrinterURI("lpr://lprserver");
job.AddDataFile(tempname, LprDataFormats.Formatted);
job.SubmitJob();
}
catch (Exception ex)
{
lastex = ex;
}
finally
{
if (job != null)
{
job.WaitForCompletion();
lastex = job.LastException;
}
MessageBox.Show(lastex.Message.ToString());
}
}
}
}
Conclusions and Future Possibilities
There is a lot of room for improvements in this project. After starting with the LPR protocol, then tacking on some of the LPRng features, then Socket API, I realized I could have structured the basic job transfer execution in a more flexible manner. The transfer protocols could be encapsulated in a set of related classes, each providing a common interface to connect, create any additional files (e.g., control files) or connections, transfer data, and monitor status. These objects would be used by the SendJobFiles
method of the LprJob
class in a more cleanly written transfer loop. For its normal intended purpose, I considered this over-engineering.
IPP is rapidly gaining ground as a standardized network printing protocol. It defines a rich, complex set of interactions among printers, servers, and clients. It would be a good candidate to include as a protocol class, as mentioned, but I have not investigated its use of resources or difficulty of implementation to determine if it would be an effective addition to this mobile printing library.
Documentation can always stand lots of improvements. If this article and the internal comments in the code are not clear enough, I will make efforts to edit one or the other to add missing information or clarify ambiguous points. It's my desire to present a quality product, and your feedback is welcome.
Programming alone and with an inadequate toolkit is a frustrating and thankless task. I hope what I have provided here helps save you from burning a lot of wasted hours in the dark like the many that I have had to. Good coding to all, and to all a good night!
History
- Released 15 December 2010
- Version 1.0 - Initial release.