Tell a printer she's a plotter!

An example: produce production result reports
for an imaginary production facility.

It is possible to use HPGL commands and have a laserprinter produce plotter artwork. Please read the other file for more details. This file is an example of how to use a high level language for producing high volumes of production administration sheets in the shortest time.
I like using Modula-2 because it is self explanatory: the comments are embedded in the procedure names. Mostly.

As each Modula-2 program, this one too starts with the declarations:

Module Sheets06 (yes, I made 5 buggy versions before..)

MODULE Sheets06;

(* Version 0.1 : first attempt to get it working              OK : 06-06-2000
   Version 0.2 : Add some Zing to it all                          OK : 07-06-2000
   Version 0.3 : Make it output the production result sheets      OK : 08-06-2000
   Version 1.0 : Same as 0.3, debugged and printing all 5         OK : 08-06-2000
   Version 1.1 : Add QA visual inspection sheet                   OK : 08-06-2000
   Version 1.2 : Add monthly production reports                   OK : 09-06-2000
   Version 1.3 : Add titles for the monthlyproduction reports     OK : 09-06-2000 *)
   
If you look at the version history, it will be clear that I program in small steps. Get it working, add some make-up, make it print a few sheets, get the obvious errors out, add some more functionality, add some more make-up. As a QA engineer, my motto is simple:

don't fix the car, if it's not broken!

If a feature works, keep it working, but do not (try to) fix non-existing errors. For this kind of program, optimisation is hardly necessary. It saved my company over 20 man-hours a month, so one minute less is non relevant.

The IMPORT, TYPE and VAR sections.

FROM    ASCII              IMPORT  EOL, ESC, FF;
FROM    FileSystem         IMPORT  Close, File, Lookup, ReadChar, Response;
FROM    InOut              IMPORT  CloseInput, CloseOutput, Done, RedirectInput,
                                   RedirectOutput, Read,ReadString, Write, WriteCard,
				   WriteLn, WriteLine, WriteString;
FROM    NumberConversion   IMPORT  StringToCard;
FROM    Strings            IMPORT  CompareStr;
FROM    System             IMPORT  GetArg, Terminate;
FROM    SYSTEM             IMPORT  ASSEMBLER;
FROM    TimeDate           IMPORT  GetTime, Time;
FROM    Xchar              IMPORT  UpperString;


TYPE   PrinterStatus   = (Ready, Offline);
       OneWord         = ARRAY [0..31] OF CHAR;


VAR    WorkDay                : ARRAY [1..31] OF BOOLEAN;
       Word                   : OneWord;
       Month, Year, MaxDay,
       MaxLine, ActiveDays    : CARDINAL;
       Magic                  : BOOLEAN;
   
Here ends the definition and declarations part. From now, only code is generated. We start out with the easy part: the contact with the user. The user is always doing it's best to try to fool the software. So, the software must be foolproof, which is a difficult task, since the development of the fools has not stopped either. Nowadays, better fools are being made than ever before in history. I dare say that the quality of fools is at a peak. Lucky for us, the majority of fools are using Windows, so most of the target audience is knowledgable. But you never know: fools easily loose their way and end up testing a fine piece of code.

Talk to the user via StdOut.

PROCEDURE UserMessage (Number  : CARDINAL);

BEGIN
    WriteLn;
    CASE Number OF
      0 :   WriteLn;
            WriteLine ("Thank you for using this software. This is GNU GPL style FREE SOFTWARE.");
            WriteLine ("This software should not be used outside the factory of Ushio Tilburg.");
            WriteLn;
            WriteLine ("CopyLeft 2000, Ushio Europe (Tilburg) BV, 5046 AT 14, The Netherlands");
            |
      1 :   WriteLine ("One of the '.DAT' files is not present. Program aborted.");
            UserMessage (0);
            |
      2 :   WriteLine ("The printer does not respond. Please check.");
            UserMessage (0);
            |
      3 :   WriteLine ("Sorry, I cannot process dates before May 1, 2000.");
            UserMessage (0);
            |
      4 :   WriteLine ("Usage instructions: SHEETS <Month> <Year> [EXCLUDE <list of dates>]");
            WriteLn;
            WriteLine ("After 'EXCLUDE' you can mention the days without scheduled production.");
            WriteLn;
            WriteLine ("Example  : Sheets september 2000 exclude 12 13 25");
            WriteLn;
            WriteLine ("Please consult the manual for more details. If that fails, consult jv.");
            UserMessage (0);
            |
      5 :   WriteLine ("The month you specified is not part of the Julian calendar.");
            WriteLine ("Please refrain from using oriental month designators.");
            UserMessage (4);
            |
      6 :   WriteLine ("Error while entering numbers to EXCLUDE. Please adjust and retry.");
            UserMessage (4);
            |
      7 :   WriteLine ("Could not open file 'Monthly.Lst'. Program aborted.");
            UserMessage (0)
    END;
    WriteLn
END UserMessage;
   

Telling time.

The software needs to know which month we are going to process. To make life easier on the user, only the first three letters have significance. Why three and not four? Because of May.

PROCEDURE FindMonth (Month : ARRAY OF CHAR) : CARDINAL;

VAR     month       : ARRAY [0..2] OF CHAR;
        Number, n   : CARDINAL;

BEGIN
    FOR n := 0 TO 2 DO
        month [n] := CAP (Month[n]);        (*  Just use first 3 letters of month   *)
    END;
    IF CompareStr (month, 'JAN') = 0 THEN
        Number := 1
    ELSIF CompareStr (month, 'FEB') = 0 THEN
        Number := 2
    ELSIF CompareStr (month, 'MAR') = 0 THEN
        Number := 3
    ELSIF CompareStr (month, 'APR') = 0 THEN
        Number := 4
    ELSIF CompareStr (month, 'MAY') = 0 THEN
        Number := 5
    ELSIF CompareStr (month, 'JUN') = 0 THEN
        Number := 6
    ELSIF CompareStr (month, 'JUL') = 0 THEN
        Number := 7
    ELSIF CompareStr (month, 'AUG') = 0 THEN
        Number := 8
    ELSIF CompareStr (month, 'SEP') = 0 THEN
        Number := 9
    ELSIF CompareStr (month, 'OCT') = 0 THEN
        Number := 10
    ELSIF CompareStr (month, 'NOV') = 0 THEN
        Number := 11
    ELSIF CompareStr (month, 'DEC') = 0 THEN
        Number := 12
    ELSE
        UserMessage (5);
        Terminate (5)
    END;
    RETURN Number
END FindMonth;
   

Check the printer

We are going to rely on a working laserprinter. It's going to output around 50 sheets of paper, unattendedly. So we want to make sure it is connected and responding. That's why I used some InLine assembly to interrogate the machine via INT 17h.

PROCEDURE CheckPrinter () : PrinterStatus;

VAR     Status      : CARDINAL;

BEGIN
    ASM
        MOV     AH, 1
        MOV     DX, 0
        INT     17H
        MOV     AL, AH
        MOV     AH, 0
        MOV     [Status], AX
    END;
    IF Status = 90H THEN
        RETURN Ready
    ELSE
        RETURN Offline
    END
END CheckPrinter;
   

DOW: Day of week

We need to know what kind of day the first of this month is, in order to isolate the working days from the saturday and sundays. For this, we need to know how many days have passed since May 1, 2000 (which was a Monday).

PROCEDURE DayOfWeek (Month, Year    : CARDINAL) : CARDINAL;

VAR     month, year, days       : CARDINAL;

BEGIN
    month := 5;
    year  := 2000;
    days  := 0;
    LOOP
        IF (month = Month) AND (year = Year) THEN EXIT END;
        CASE month OF
          1,3,5,7,8,10,12   : INC (days, 31);  |
                 4,6,9,11   : INC (days, 30);  |
                        2   : IF (year MOD 4) = 0 THEN
                                INC (days, 29)
                              ELSE
                                INC (days, 28)
                              END
        END;
        INC (month);
        IF month = 13 THEN
            month := 1;
            INC (year)
        END
    END;
    RETURN (days MOD 7) + 1;                (*  Monday = 1, Sunday = 7  *)
END DayOfWeek;
   

Drawing lines in sheets

We must be able to draw a sheet of lines and other artwork without using I/O redirection. When we are in the midst of a subroutine, we will have redirected output to the printer and redirected input to a customisation file. So we need a way to pump out data to the printer without having to close and re-open files through piping.
The DrawSheet procedure does just that: it opens a file in binary mode and transfers it character by character to the printer. This procedure may only be used when the standard output (stdout) has been redirected to the printer.

PROCEDURE DrawSheet (SheetName : ARRAY OF CHAR);

VAR     ch      : CHAR;
        InFile  : File;

BEGIN
    Lookup (InFile, SheetName, FALSE);
    IF InFile.res = notdone THEN
        CloseOutput;                (*  If file not found, tell the moron   *)
        UserMessage (1);
        Terminate (1)
    END;
    ReadChar (InFile, ch);
    WHILE InFile.eof = FALSE DO
        Write (ch);                 (*  Write to (redirected) StdOut    *)
        ReadChar (InFile, ch)       (*  Fetch new token from file       *)
    END;
    Close (InFile)                  (*  If done, close file             *)
END DrawSheet;
   

Write the month to paper

WriteMonth should be clear. If it isn't, send a mail to a friend.

PROCEDURE WriteMonth;

BEGIN
    CASE Month OF
        1 : WriteString ("January");    |
        2 : WriteString ("February");   |
        3 : WriteString ("March");      |
        4 : WriteString ("April");      |
        5 : WriteString ("May");        |
        6 : WriteString ("June");       |
        7 : WriteString ("July");       |
        8 : WriteString ("August");     |
        9 : WriteString ("September");  |
       10 : WriteString ("October");    |
       11 : WriteString ("November");   |
       12 : WriteString ("December")
    END;
    WriteCard (Year, 5)
END WriteMonth;
   

Reading a name from (redirected) StdIn

ReadName is a handy procedure. It reads quoted text. It looks for a quote and considers all other characters as whitespace. After it has found the first quote (") it starts accepting everything it finds as being non-whitespace. All characters are stored in an array, until the second quote is found and control is transferred back to the caller.

PROCEDURE ReadName (VAR buffer : ARRAY OF CHAR);

VAR     ch      : CHAR;
        n       : CARDINAL;

BEGIN
    n := 0;
    REPEAT
        Read (ch)
    UNTIL ch = '"';         (*  get rid of spaces  *)
    LOOP
        Read (ch);
        IF ch = '"' THEN EXIT END;
        buffer [n] := ch;
        INC (n)
    END;
    IF n <= HIGH (buffer) THEN
        buffer [n] := 0C
    END
END ReadName;
   

Making white paper appear 7% gray

From here, the actual sheets are being produced. The output of the program must comply to the rules of the HPGL: ASCII text only. So we must produce tekst like

PA 200, -300;

where all numbers must be in the form of ASCII characters, not as binary numbers. This means that all positions must be printed to the printer, and not copied to it.
We start by checking if the printer is on-line. If not, the program is halted with an appropriate message. Next, a sheet is printed for each production facillity. The maximum number of the production facility has been defined in the Initialize routine (further on in the source). If the user has supplied a magic word (spelled "MaGiC"), the software comes into debugmode and will only print one sheet per section in order to prevent 500 wrong sheets of paper during tests of new features.
PROCEDURE MakePRsheets;     (*  Make the Production Results sheets  *)

VAR     i, n, Xpos  : CARDINAL;

BEGIN
    IF CheckPrinter () = Offline THEN
        UserMessage (2);
        Terminate (2)
    ELSE
        RedirectOutput ("PRN")
    END;
    FOR n := 1 TO MaxLine DO
        Write (ESC);    WriteString ("%1B");        (*  Make printer think it's a plotter   *)

        ActiveDays := 0;
        DrawSheet ("ProdRep.Dat");
   

The general purpose part of sheet is printed

At this point, we have succesfully written the raw outline sheet to the printer. Now we will have to customize the sheet for this production facility and for this month. We start out by lifting the pen and moving it across the paper to a new position. There we lower the pen and write some text with the LB command. Don't forget to terminate it, since it will halt execution of the software here and now.

        WriteString ("pu; pa  400, 10550; pd; lbT-");   WriteCard (n, 1);   Write ("#");
        WriteString ("pu; pa 5400, 10550; pd; lb");     WriteMonth;         Write ("#");

        WriteString ("si 0.12, 0.25;");             (*  Define character height         *)
        Xpos := 1920;
        FOR i := 1 TO MaxDay DO
            IF WorkDay [i] = TRUE THEN
                WriteString ("pu; pa");  WriteCard (Xpos, 5);  WriteString (",   220; pd; lb");
                WriteCard (i, 2);        Write ("#");
                WriteString ("pu; pa");  WriteCard (Xpos, 5);  WriteString (", 10080; pd; lb");
                WriteCard (i, 2);        Write ("#");
                INC (ActiveDays);
                INC (Xpos, 250)
            END
        END;

        WriteString ("si 0.12, 0.25;");
        WriteString ("pu; pa 6400, 50; pd; lbWorkingdays : ");   
	WriteCard (ActiveDays, 2);  Write ("#");

        Write (ESC);    WriteString ("%1A");        (*  Tell printer she's a printer    *)
        Write (FF)                                  (*  Eject ready page                *)
    END;
    CloseOutput
END MakePRsheets;
   

Two more sheets

By now we have output MaxLine copies of this sheet to the printer. The following two procedures are comparable to this one. They only differ in the shape of the sheet being produced and the numbers. See if you can decipher what the code does. If in doubt, send me a mail.

PROCEDURE MakeVIsheet;      (*  Produce QA Visual Inspection sheet  *)

VAR     i, n, Xpos      : CARDINAL;

BEGIN
    IF CheckPrinter () = Offline THEN
        UserMessage (2);
        Terminate (2)
    ELSE
        RedirectOutput ("PRN")
    END;
    FOR n := 1 TO MaxLine DO
        Write (ESC);    WriteString ("%1B");        (*  Make printer think it's a plotter   *)

        DrawSheet ("QAsheet.Dat");
        WriteString ("pu; pa  400, 10550; pd; lbT-");   WriteCard (n, 1);   Write ("#");
        WriteString ("pu; pa 5400, 10550; pd; lb");     WriteMonth;         Write ("#");

        WriteString ("si 0.12, 0.25;");             (*  Define character height         *)
        WriteString ("pu; pa 6400, 50; pd; lbWorkingdays : ");
                                                WriteCard (ActiveDays, 2);  Write ("#");
        Xpos := 1920;
        FOR i := 1 TO MaxDay DO
            IF WorkDay [i] = TRUE THEN
                WriteString ("pu; pa");  WriteCard (Xpos, 5);  WriteString (", 1420; pd; lb");
                WriteCard (i, 2);        Write ("#");
                WriteString ("pu; pa");  WriteCard (Xpos, 5);  WriteString (", 8740; pd; lb");
                WriteCard (i, 2);        Write ("#");
                INC (Xpos, 250)
            END
        END;

        Write (ESC);    WriteString ("%1A");        (*  Tell printer she's a printer    *)
        Write (FF)                                  (*  Eject ready page                *)
    END;
    CloseOutput
END MakeVIsheet;


PROCEDURE MakePOsheets;     (*  Produce Process Output sheet        *)

VAR     i, n, Xpos      : CARDINAL;
        Pline           : ARRAY [0..3] OF CHAR;
        Pprocess        : ARRAY [0..63] OF CHAR;

BEGIN
    IF CheckPrinter () = Offline THEN
        UserMessage (2);
        Terminate (2)
    ELSE
        RedirectOutput ("PRN")
    END;
    RedirectInput ("Monthly.Lst");
    IF NOT Done THEN                    (*  Tell the user the file is absent    *)
        CloseOutput;
        UserMessage (7);
        Terminate (7)
    END;
    REPEAT
        ReadString (Word)
    UNTIL CompareStr (Word, "BEGIN") = 0;

    LOOP
        ReadString (Pline);
        IF CompareStr (Pline, "END") = 0 THEN EXIT END;
        ReadName (Pprocess);
        Write (ESC);    WriteString ("%1B");        (*  Make printer think it's a plotter   *)

        DrawSheet ("Monthly.Dat");
        WriteString ("pu; pa  400, 10550; pd; lb");     WriteString (Pline);    Write ("#");
        WriteString ("pu; pa 5400, 10550; pd; lb");     WriteMonth;             Write ("#");
        WriteString ("pu; pa 3000, 10200; pd; lb");     WriteString (Pprocess); Write ("#");

        WriteString ("si 0.12, 0.25;");             (*  Define character height         *)

        WriteString ("pu; pa 6400, 50; pd; lbWorkingdays : ");
                                                    WriteCard (ActiveDays, 2);  Write ("#");
        Xpos := 1920;
        FOR i := 1 TO MaxDay DO
            IF WorkDay [i] = TRUE THEN
                WriteString ("pu; pa");  WriteCard (Xpos, 5);  WriteString (", 1540; pd; lb");
                WriteCard (i, 2);        Write ("#");
                INC (Xpos, 250)
            END
        END;

        Write (ESC);    WriteString ("%1A");        (*  Tell printer she's a printer    *)
        Write (FF);                                 (*  Eject ready page                *)
        IF Magic THEN EXIT END
    END;
    CloseInput;
    CloseOutput
END MakePOsheets;
   

Initialisations: process the command line arguments

Next comes the initialisation part of the program. We are going to read the commandline and see how good a fool the user is. We are going to decrypt the month, year. The magic and the exceptions (like national and religious holidays).

PROCEDURE Initialize;

VAR     Option          : ARRAY [0..15] OF CHAR;
        count, i, d     : CARDINAL;
        ok              : BOOLEAN;

BEGIN
    Magic := FALSE;
    MaxLine := 5;
    GetArg (Option, count);             (*  Try to fetch MONTH      *)
    IF count = 0 THEN
        UserMessage (4);                (*  If none, inform user    *)
        Terminate (4)
    END;
    Month := FindMonth (Option);        (*  Else determine month    *)

    GetArg (Option, count);             (*  Try to retrieve YEAR    *)
    IF count = 0 THEN
        UserMessage (4);
        Terminate (4)
    END;
    StringToCard (Option, Year, ok);    (*  Convert ASCII to number *)
    IF (NOT ok) OR (Year < 2000) THEN
        UserMessage (3);
        Terminate (3)
    END;
    IF (Year = 2000) AND (Month < 5) THEN
        UserMessage (3);
        Terminate (3)
    END;
                            (*  Here both Month and Year are known  *)
    CASE Month OF
        2         : IF (Year MOD 4) = 0 THEN
                        MaxDay := 29
                    ELSE
                        MaxDay := 28
                    END;            |
        4,6,9,11  : MaxDay := 30
    ELSE
        MaxDay := 31
    END;
    d := DayOfWeek (Month, Year);

    FOR i := 1 TO MaxDay DO         (*  Fill up array with workingdays  *)
        IF d < 6 THEN
            WorkDay [i] := TRUE
        ELSE
            WorkDay [i] := FALSE
        END;
        INC (d);
        IF d > 7 THEN d := 1 END
    END;
    GetArg (Option, count);         (*  is there an EXCLUDE option?     *)
    IF count = 0 THEN RETURN END;
    IF CompareStr (Option, 'MaGiC') = 0 THEN
        Magic := TRUE;
        MaxLine := 1;
        GetArg (Option, count);
        IF count = 0 THEN RETURN END
    END;
    UpperString (Option);
    IF CompareStr (Option, "EXCLUDE") # 0 THEN      (*  Check for proper syntax *)
        UserMessage (4);
        Terminate (4)
    END;
    LOOP
        GetArg (Option, count);
        IF count = 0 THEN EXIT END;
        StringToCard (Option, i, ok);
        IF (NOT ok) OR (i > MaxDay) THEN
            UserMessage (6);
            Terminate (6)
        ELSE
            WorkDay [i] := FALSE
        END
    END
END Initialize;
   

Some debug-routines (of course I never needed them..)

So far the actual code which is compiled all the times. What follows is a debugging routine. It has been commented out since it will clobber up the screen.

(*
PROCEDURE ShowDebugData;

VAR     i           : CARDINAL;

BEGIN
    WriteString ("Month  : ");  WriteCard (Month, 4);   Write (11C);
    WriteString ("Year  : ");   WriteCard (Year, 8);    WriteLn;

    WriteString ("MaxDay : ");  WriteCard (MaxDay, 4);  WriteLn;

    WriteLine ("The current days are scheduled:");      WriteLn;
    FOR i := 1 TO MaxDay DO
        IF WorkDay [i] THEN     WriteCard (i, 5)    END;
        IF (i MOD 7) = 0 THEN   WriteLn             END
    END
END ShowDebugData;      *)
   

The MAIN routine, which has no name in Modula-2

What remains is the actual main routine: four lines of code. This is what I like about Modula-2 (and Forth, for that matter). They force you to define sub-modules of a program and have these executed.

BEGIN
    Initialize;             (*  Interpret commandline and such      *)

(*  ShowDebugData;          (*  Show some data on screen            *)  *)

    MakePRsheets;           (*  Produce Production Report sheets    *)
    MakeVIsheet;            (*  Produce QA Visual Inspection sheets *)
    MakePOsheets;           (*  Produce Process Output sheets       *)

END Sheets06.
   

Rounding down

So far this program. If you want to access the source code, just download the entire package. It is only 7 Kb.

Here is also a sample of what the sheets look like. The software completes the page by filling in all the production dates and month/year information. Plus all other production related labels.

Ushio QA inspection chart
I hope this piece of code has given you some joy and has clarified yet another part of the marvelous Modula-2 language.

Feet back

If you have something to tell me, just send me a note using the link in the navigator frame on the right.

Page created June 2002 and