Since the software that I contribute to as part of my day job involves printing to receipt printers, I’ve been keeping my finger on the pulse of eBay and watching for cheap listings for models I’m familiar with. Recently I stumbled upon a “too good too be true” listing- an Epson TM-T88IV listed for $35. The only caveat that I could see was that it was a serial printer; that is, it used the old-style RS-232 ports. I figured that might be annoying but, hey, I’ve got Windows 10 PCs that have Serial Ports on the motherboard, and a Null Modem Serial cable, how hard could it be?.
Famous last words, as it happened, because some might call the ensuing struggle to be a nightmare.
When the printer arrived, my first act was to of course verify it worked on it’s own. it powered up, And would correctly print the test page, so it printed fine. Next up, of course, was to get it to communicate with a computer. I used a Null-modem cable to connect it, adjusted the DIP switches for 38400 Baud, 8 data bits, 1 stop bit, no parity DSR/DTR control, printed the test page again, then installed the Epson OPOS ADK for .NET (as I was intending to use it with .NET). I configured everything identically, but Checkhealth failed. I fiddled with it for some time- trying all the different connection methods and trying to get better results to no avail.
I fired up RealTerm, and squirted data directly over the COM port. I could get text garbage to print out- I tried changing the COM Port settings in Device Manager to set a specific baud rate as well, but that didn’t work.
I had a second computer- my computer built in 2008, which while it didn’t have a COM *port*, It did have the header for one. I took the LPT and COM bracket from an older Pentium system and slapped it in there for testing, and spent a similar amount of time with exactly the same results. I was starting to think that the printer simply was broken, or the Interface card inside it was broken in some way.
Then, I connected it to a computer running Windows XP. I was able to get it to work exactly as intended; I could squirt data directly to the printer and it would print, I could even set up an older version of the OPOS ADK and CheckHealth went through. Clearly, the receipt printer was working- so there was something messed up with how I was using it. I put an install of Windows 7 on one of the Windows 10 PCs I was testing and found I got the same results. Nonetheless, after some more research and testing, it seems like Windows 10 no longer allows the use of motherboard Serial or Parallel ports. Whether this is a bug or intentional it’s unclear. I would guess it was imposed at the same time during development that Windows 10 had dropped Floppy support; people spoke up and got Floppy support back in, but perhaps parallel and Serial/RS-232 stayed unavailable. Unlike that case though they do appear in Device Manager and are accessible as devices, they just don’t work correctly when utilized.
Since the software I wanted to work on would be running on Windows 10- or if nothing else, certainly not Windows XP, I had to get it working there. I found that using a USB Adapter for an RS-232 Port worked, which meant I could finally start writing code.
The first thing was that a Receipt printer shouldn’t be necessary to test the code, or for, say, unit tests. So I developed an interface. This interface could be used for mocking, and would implement basic features as required. The absolute basics were:
- Ability to print lines of text
- Enable and Disable any underlying device
- Ability to Claim and Release the “printer” for exclusive use
- Ability to Open, and Close the Printer
- Ability to retrieve the length of a line in characters
- Ability to Print a bitmap
- Ability to Cut the paper
- boolean property indicating whether OPOS Format characters were supported
I came up with a IPosPrinter interface that allowed for this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public interface IPosPrinter { void PrintNormal(String sText); bool DeviceEnabled { get; set; } bool Claim(int Timeout); void Open(); void Release(); void Close(); int RecLineChars { get; } void PrintBitmap(String pFileName,int useWidth,int Alignment); void CutPaper(int percent); bool SupportsFormatChars { get; } } |
From there, I could make a “mock” implementation, which effectively implemented a ‘receipt print’ by sending it directly to a text file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public class PosTextFilePrintAdapter : IPosPrinter { private StringBuilder _MockOutput = null; public String _MockResult { get; private set; } = null; public bool ShellResult { get; set; } = true; public PosTextFilePrintAdapter(StringBuilder Target,int pRecLineChars = 48,int pRecLineWidth=48) { RecLineChars = pRecLineChars; RecLineWidth = pRecLineWidth; _MockOutput = Target; } public void PrintNormal(string sText) { _MockOutput.Append(sText); } public bool SupportsFormatChars { get { return false; } } public bool DeviceEnabled { get; set; } //no op here public void Release() { } public bool Claim(int Timeout) { return true; //no op, no need to claim a Mocked instance. } public void Open() { //no op } public void Close() { // print to a text file and shell to it. _MockResult = _MockOutput.ToString(); if (ShellResult) { String sFileName = Path.GetFileNameWithoutExtension(Path.GetTempFileName()) + ".txt"; String sTargetPath = Path.Combine(Path.GetTempPath(), sFileName); using (StreamWriter sw = new StreamWriter(new FileStream(sTargetPath, FileMode.Create))) { sw.Write(_MockOutput.ToString()); } ProcessStartInfo psi = new ProcessStartInfo(sTargetPath) { UseShellExecute = true }; Process.Start(psi); } } public int RecLineChars { get; private set; } public int RecLineWidth { get; private set; } public void PrintBitmap(string pFileName, int useWidth, int Alignment) { //_MockOutput.AppendLine("Bitmap:" + pFileName + ""); //throw new NotImplementedException(); } public void CutPaper(int percent) { //_MockOutput.AppendLine("Cut Paper - " + percent + "%"); } } |
This implementation can also optionally shell the resulting text data to the default text editor, providing a quick way of testing a “printout”. However, this interface isn’t sophisticated enough to be usable for a nice receipt printer implementation; In particular, The actual code to print is going to want to use columns to separate data. That shouldn’t be directly in the interface, however- instead, a separate class can be defined which composites an interface implementation of IPOSPrinter and provides the additional functionality. This allows any implementation of IPOSPrinter to benefit, without requiring they have additional implementations.
Since our primary feature is having columns, we’ll want to define those columns. a ColumnDefinition class would be just the ticket. We can then tell the main ReceiptPrinter class the columns, then have a params array accept the print data and it could handle the columns automatically. Here is the ColumnDefinition class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
/// <summary> /// Defines information for possible columns used in a receipt. /// </summary> public class ColumnDefinition { public enum AlignmentConstants { Alignment_Left, Alignment_Right, Alignment_Center_SpaceLeft, Alignment_Center_SpaceRight } public enum DisplayFormat { Format_Text, Format_Currency, Format_Custom } private int _ShrinkPriority; public delegate String FormatCustom(Object value); private int _DesiredWidth; private int _MinimumWidth; private AlignmentConstants _Alignment; private DisplayFormat _Format; private FormatCustom CustomFormatter = null; private String _Name; public String Name { get { return _Name; } set { _Name = value; } } /// <summary> /// Gets or sets the maximum Width of this column. /// </summary> public int DesiredWidth { get { return _DesiredWidth; } set { _DesiredWidth = value; } } public int MinimumWidth { get { return _MinimumWidth; } set { _MinimumWidth = value; } } public AlignmentConstants Alignment { get { return _Alignment; } set { _Alignment = value; } } public DisplayFormat Format { get { return _Format; } set { _Format = value; } } public int ShrinkPriority { get { return _ShrinkPriority; } set { _ShrinkPriority = value; } } public ColumnDefinition(int pDesiredWidth,int pMinimumWidth,AlignmentConstants pAlignment=AlignmentConstants.Alignment_Left) { _DesiredWidth = pDesiredWidth; _MinimumWidth = pMinimumWidth; _Alignment = pAlignment; } public ColumnDefinition(String pName,int pDesiredWidth, int pMinimumWidth, AlignmentConstants pAlignment = AlignmentConstants.Alignment_Left) { _Name = pName; _DesiredWidth = pDesiredWidth; _MinimumWidth = pMinimumWidth; _Alignment = pAlignment; } public static String RepeatStr(String str,int count) { return String.Join("", Enumerable.Repeat(str, count)); } /// <summary> /// used to retrieve a Monochrome (1-bit) bitmap of the passed in bitmap. /// </summary> /// <param name="bitmap">Bitmap to rekey.</param> /// <returns></returns> private static BitmapData GetBitmapData(Bitmap bitmap,int threshold=127) { var index = 0; var dimensions = bitmap.Width * bitmap.Height; var dots = new BitArray(dimensions); for (var y = 0; y < bitmap.Height; y++) { for (var x = 0; x < bitmap.Width; x++) { var color = bitmap.GetPixel(x, y); var luminance = (int)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11); dots[index] = (luminance < threshold); index++; } } return new BitmapData() { Dots = dots, Height = bitmap.Height, Width = bitmap.Width }; } public static String Truncate(String target,int Length,AlignmentConstants alignment) { //if the string is larger than the target length, truncate it. if (target.Length > Length) { return target.Substring(0, Length); } if (target.Length == Length) return target; if(alignment==AlignmentConstants.Alignment_Left) { //align left by adding spaces to the end of the string. return target + RepeatStr(" ", Length - target.Length); } if(alignment==AlignmentConstants.Alignment_Right) { //align right by adding spaces to the start of the string. return RepeatStr(" ", Length - target.Length) + target; } if (alignment == AlignmentConstants.Alignment_Center_SpaceLeft || alignment == AlignmentConstants.Alignment_Center_SpaceRight) { //if the number of spaces is even, we're good- divide it in half and put a pair on each side. bool needsSpace = (Length - target.Length) % 2 != 0; int numspaces = (Length - target.Length)/2; String useside = RepeatStr(" ", numspaces); if(!needsSpace) { return useside + target + useside; } //otherwise, we'll have to use the preference setting. if (alignment == AlignmentConstants.Alignment_Center_SpaceLeft) return useside + " " + target + useside; if (alignment == AlignmentConstants.Alignment_Center_SpaceRight) return useside + target + useside + " "; } return target; } public String GetResult(Object data,int useLength=-1,bool PrintingHeader=false) { if(useLength==-1) useLength = DesiredWidth; String useString=null; try { if (Format == DisplayFormat.Format_Text || PrintingHeader) { useString = data.ToString(); } else if (Format == DisplayFormat.Format_Currency) { useString = String.Format("{0:C}", data); } else if (Format == DisplayFormat.Format_Custom) { if (CustomFormatter == null) throw new ArgumentException("Custom Format specified without Custom Formatter."); useString = CustomFormatter(data); } } catch(Exception exx) { useString = data.ToString(); } return Truncate(useString, useLength, this.Alignment); } } |
At this point, we just want a primary helper routine within said ReceiptPrinter class. That could be used within that class to handle printing from more easily used methods intended for use by client code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public static String GetPrintOutput(bool PrintingHeaders, List<ColumnDefinition> ColumnData, Object[] Values, int ColumnLineChars = 42) { if (Values.Length != ColumnData.Count) throw new ArgumentException("Given " + Values.Length + " Arguments. Expected:" + ColumnData.Count); else { int[] useLengths = (from p in ColumnData select p.DesiredWidth).ToArray(); if ((from p in ColumnData select p.DesiredWidth).Sum() > ColumnLineChars) { Dictionary<ColumnDefinition, int> IndexMap = new Dictionary<ColumnDefinition, int>(); for (int i = 0; i < ColumnData.Count; i++) IndexMap.Add(ColumnData[i], i); //iterate over the columnDefs, in order of descending shrink priority, until we are skinny enough to fit in the POS char width. foreach (var col in from p in ColumnData orderby p.ShrinkPriority descending select p) { useLengths[IndexMap[col]] = col.MinimumWidth; if (useLengths.Sum() < ColumnLineChars) { break; //finished! } } } StringBuilder buildline = new StringBuilder(); for (int i = 0; i < Values.Length; i++) { Object currvalue = Values[i]; ColumnDefinition coldef = ColumnData[i]; String columndata = coldef.GetResult(currvalue, useLengths[i], PrintingHeaders); buildline.Append(columndata); } String strPrint = buildline.ToString(); String strSpaces = ""; if (strPrint.Length < ColumnLineChars) { strSpaces = String.Join("", Enumerable.Repeat(' ', (ColumnLineChars / 2) - (strPrint.Length / 2))); } return strSpaces + buildline.ToString() + strSpaces + "\n"; } } |
This implementation also incorporates a shrink priority that can be given to each column. Columns with a higher priority will be given precedence to remain, but otherwise columns may be entirely eliminated from the output if the width of the receipt output is too low. This allows for some “intelligent” customization for specific printers, as some may have less characters per line and redundant columns or less needed columns can be eliminated, on those, but they can be included on printers with wider outputs. The actual ReceiptPrinter class in all it’s glory- not to mention the implementations of IPOSPrinter beyond the text output, particularly the one that actually delegates to a .NET OPOS ADK Device and outputs to a physical printer, will require more explanation, so will appear later in a Part 2.
Have something to say about this post? Comment!