Delphi: PJL commands to printer and reading the responses back - delphi

Im struggling with getting information back from the printer when sending PJL to the printer. The printer supports PJL and it is a USB printer. Now getting information / examples seems to be a problem or I'm looking at the wrong places. I know on MSDN there are a lot of information, but I've tried everything I got there, from docinfo's to write/read printers and nothing seems to work.
Some people say you can use writeprinter and readprinter. I've tried this, when I writeprinter, the printer seems to "do" something, but readprinter returns or errors or blanks. Now I think this could be because the print driver is "locking" the port, so you cant read information back from it?
The other option I saw somewhere is to use writefile and readfile. Here you would get the physical port part for the printer, for example '\?\USB#VID_05CA&PID_0403#S5208603411#{28d78fad-5a12-11d1-ae5b-0000f803a8c2}'. Then you change the port to "FILE". Use the writefile and readfile with the path as the physical path from above. Get the information you need and then set the port back to the original port. Tried this as well, also getting errors.
Im just trying to do a simple #PJL INFO PAGECOUNT (I left out the escape chars etc, etc). The string is correct, because using the string on networked printers, its working 100% and I can get the information. But local printers is a problem.
Is there anyone that has this working or a working example? Any help would be much appreciated.
PS: Below is 1 of the 100's of examples I've tried. This is the writeprinter example:
procedure TForm1.Button5Click(Sender: TObject);
Const
Defaults: TPrinterDefaults = (
pDatatype : Nil;
pDevMode : nil;
DesiredAccess : PRINTER_ACCESS_USE or PRINTER_ACCESS_ADMINISTER ) ;
Var
Device : array[0..255] of char;
FPrinterHandle:THandle;
DocInfo1: TDocInfo1;
Buffer, Buffer2:PChar;
Written, Len:Cardinal;
i: Integer;
sPath: String;
Begin
StrCopy(Device,PChar('RICOH Aficio SP 4210N PCL 6'));
OpenPrinter(#Device, FPrinterHandle, #Defaults);
DocInfo1.pDocName := 'test';
DocInfo1.pOutputFile := Nil;
DocInfo1.pDatatype := 'RAW';
StartDocPrinter(FPrinterHandle, 1, #DocInfo1);
StartPagePrinter(FPrinterHandle);
Buffer := #27+'%-12345X#PJL COMMENT'+#13+#10+'#PJL INFO PAGECOUNT'+#13+#10+#27+'%-12345X';
WritePrinter(FPrinterHandle,#Buffer,Length(Buffer), Written);
EndPagePrinter(FPrinterHandle);
EndDocPrinter(FPrinterHandle);
// everithing is OK here, BUT
ReadPrinter(FPrinterHandle, #Buffer2, Length(Buffer2), len );
end;

Check http://www.undocprint.org/winspool/tips_and_tricks for an explanation of what to do, and some sample C code.
Even with this code the chances of this working for you are minimal. To be able to read back from the printer, the port monitor must support bidirectional mode, and the standard USB port monitor does not.
Also, in your code above, Buffer2 passed to ReadPrinter() is not correct. You need to pre-allocate buffer space, and then pass the address of the buffer, not the address of the pointer to the buffer...
var
Buffer2 : array[0..255] of Char;
begin
...
ReadPrinter( FPrinterHandle, #Buffer2[0], Length(Buffer2), len );
end;

Related

Access Violation - how do I track down the cause?

I'm getting an access violation when I close a form in my application. It seems to happen only after I have access a database a couple of times, but that doesn't seem to make sense.
I have traced through and put outputdebugstring messages in all the related OnDestroy() methods, but the AV appears to be outside of my code.
This is the text of the message:
Access violation at address 00405F7C in module
'MySoopaApplication.exe'. Read of address 00000008.
How do I find where in the application 00405F7C is?
What tools are available in Delphi 10.1 Berlin to help me with this?
Edit: added a bit more info ... when clicking "Break" the IDE always takes me to this piece of code in GETMEM.INC:
#SmallPoolWasFull:
{Insert this as the first partially free pool for the block size}
mov ecx, TSmallBlockType[ebx].NextPartiallyFreePool
Further edit: well, I found the culprit, though I can't honestly say that the debug tools got me there - they just seemed to indicate it wasn't in my code.
I had used code from the net that I used to find the Windows logged in user - this is it:
function GetThisComputerName: string;
var
CompName: PChar;
maxlen: cardinal;
begin
maxlen := MAX_COMPUTERNAME_LENGTH +1;
GetMem(CompName, maxlen);
try
GetComputerName(CompName, maxlen);
Result := CompName;
finally
FreeMem(CompName);
end;
end;
once I had replaced the code with a simple result := '12345' the AVs stopped. I have no changed it to this code:
function GetThisComputerName: string;
var
nSize: DWord;
CompName: PChar;
begin
nSize := 1024;
GetMem(CompName, nSize);
try
GetComputerName(CompName, nSize);
Result := CompName;
finally
FreeMem(CompName);
end;
end;
which seems to work and, as a bonus, doesn't cause AVs.
Thanks for your help, much appreciated.
Under Tools|Options in the IDE go to Embarcadero Debuggers | Language Exceptions and make sure Notify on Language Exceptions is checked. Also under Project Options | Compiling, make sure Debugging | Use debug DCUs is checked.
Allow the exception to happen, then go to View | Debug Windows | Call stack and you should be able to see exactly where it occurred. The fact that it occurs after db access is probably because that causes some object to be created which generates the AV when it is destroyed. Possibly because it is being Free()ed twice.
If that doesn't solve it, you may may need an exception-logging tool like madExcept mentioned by DavidH.
Read of address 00000008.
The fact that this address is a low number is suggestive of it being the address of a member of an object (because they are typically at low offsets from the base address of the object).
How do I find where in the application 00405F7C is?
With your app running, in the IDE go to Search | Go to Address. This should find it if the exception is in your application and not in some related module like a .DLL it is using. The menu item is enabled once the application is running in the IDE and stopped at a breakpoint. Also there is a compiler command line switch to find an error by address.
Others have explained how to diagnose an AV.
Regarding the code itself, there are issues with it:
Most importantly, you are not allocating enough memory for the buffer. GetMem() operates on bytes but GetComputetName() operates on characters, and in this case SizeOf (Char) is 2 bytes. So you are actually allocating half the number of bytes that you are reporting to GetComputerName(), so if it writes more than you allocate then it will corrupt heap memory. The corruption went away when you over-allocated the buffer. So take SizeOf(Char) into account when allocating:
function GetThisComputerName: string;
var
CompName: PChar;
maxlen: cardinal;
begin
maxlen := MAX_COMPUTERNAME_LENGTH +1;
GetMem(CompName, maxlen * SizeOf(Char)); // <-- here
try
GetComputerName(CompName, maxlen);
Result := CompName;
finally
FreeMem(CompName);
end;
end;
In addition to that:
you are ignoring errors from GetComputerName(), so you are not guaranteeing that CompName is even valid to pass to Result in the first place.
You should use SetString(Result, CompName, nSize) instead of Result := CompName, since GetComputerName() outputs the actual CompName length. There is no need to waste processing time having the RTL calculate the length to copy when you already know the length. And since you don't check for errors, you can't rely on CompName being null terminated anyway if GetComputerName() fails.
You should get rid of GetMem() altogether and just use a static array on the stack instead:
function GetThisComputerName: string;
var
CompName: array[0..MAX_COMPUTERNAME_LENGTH] of Char;
nSize: DWORD;
begin
nSize := Length(CompName);
if GetComputerName(CompName, nSize) then
SetString(Result, CompName, nSize)
else
Result := '';
end;

Cant seem to recive a reply vir com port

Im trying to send a command to a dev. board and then receive a reply
E.G
(me) set attrib=yes
(Dev. Board) O.K
or
(Dev. Board) E.R.R
But it doesn't bounce back anything ... not an O.K or an E.R.R
while the board is booting echo is on .. so if I send the commands while the board is booting it
it will bounce back an 'set attrib=yes' and once booted an 'E.R.R' because you cant send commands while booting.
my best guess is that it isn't reading the reply in time or trying to read it too soon.
procedure TReaderProgrammer.Button2Click(Sender: TObject);
Var
Buffer: AnsiString;
RxData: string;
Count : integer;
begin
if ComPort1.Connected then
begin
ComPort1.WriteStr(edtSendText.Text+#13);
comport1.ReadStr(RxData,Count);
Buffer := Buffer + Rxdata;
memoRxData.Text := memoRxData.Text+Buffer+#13+#10;
end;
end;
Here are several open questions in the air, so I have to make some assumptions that might be wrong, but let's see.
I don't know what comm port library you are using, so I'm assuming it is the CPort library from SourceForge. I have never used it myself, but I have read that it is made Unicode aware, such that you can call the write methods with a unicodestring parameter which will be converted by the library to ansistring before sending. Similarily when receiving ansistring from the outer world, the library will convert to unicodestring for the Read methods.
Due to the asynchronous nature of serial communication, it is important to understand that when you send something using the write method, the method returns immediately while the library and OS spit out the characters one at at time at a pace defined by the baud rate. As a result your first code never received anything, because you were already attempting to read from the comm port before the external device even received the first character.
It is good to see that you have now taken the first step to success by implementing an event handler for (presumably library event) OnRxChar.
The OnRxChar probably fires for each character (or couple of characters). You need to have a buffer that is persistent between these events. A local var (as you have it now and which is allocated on the stack) in the event handler is not persistent, it is lost every time the event handler exits.
You should declare the Buffer variable as a field of TReaderProgrammer. I don't know why you defined the buffer to be AnsiString, but I suggest you try with string (ref discussion above regarding Unicode awareness).
type
TReaderProgrammer = class
..
RxBuffer: string;
..
end;
The buffer needs to be cleared when you send a new command to the external device in order for it to be ready to receive new data as a response to your command.
EDIT: Alternatively you can clear the RxBuffer immediately when you have received and processed a full response.
The TReaderProgrammer.ComPort1RxChar should look like this sofar:
procedure TReaderProgrammer.ComPort1RxChar(Sender: TObject; Count: Integer);
var
RxData : string;
begin
(Sender as TComPort).ReadStr(RxData,Count);
RxBuffer := RxBuffer + Rxdata;
...
end;
The rest of the event handler is, I guess, probably just to see progress of reception, so nothing more about that.

How to redirect stdout in a DLL to a TMemo

I have a third-party dll that I can't change and sends its output, at irregular intervals, to stdout. I would like to capture the stdout and display it in a TMemo control. A previous answer (Delphi - Capture stdout and stderr output from statically linked MSVC++ compiled DLL) shows how to capture such output to a file, I'd like to capture it to a TMemo.
Possible solution: I could read the file as it fills up with output from stdout and polling the file but I'd need to identify the new output from the previously saved output. PLus it doesn't seem like a real solution. I've done a lot of internet searching and most common related answer I've found is how to capture stdout from an external application, that's not what I want to do, I want to capture output from a dll. There is also this code fragment, but I didn't really understand what it was doing or how to use it - http://embarcadero.newsgroups.archived.at/public.delphi.language.delphi.win32/201010/10101510449.html but it seems like its solving the same problem. Has anyone found anything related to this topic in their internet travels?
I have something working based on Pipes, wasn't as difficult as I thought but there is stil one thing that troubles me. Here is the code:
var
TextBuffer: array[1..32767] of AnsiChar;
TextString: AnsiString;
BytesRead, BytesRem: cardinal;
PipeSize: cardinal;
Security : TSecurityAttributes;
begin
Security.nlength := SizeOf(TSecurityAttributes) ;
Security.binherithandle := true;
Security.lpsecuritydescriptor := nil;
if CreatePipe(outputPipeRead, outputPipeWrite, #Security, 0) then
begin
SetStdHandle(STD_OUTPUT_HANDLE, outputPipeWrite);
end
else
showmessage ('Error in creating pipe');
.... Call dll here so that it sends output to stdout
PipeSize := Sizeof (textbuffer);
PeekNamedPipe (outputPipeRead, nil, PipeSize, #BytesRead, #PipeSize, #BytesRem);
if BytesRead > 0 then
begin
if ReadFile(outputPipeRead, TextBuffer, PipeSize, BytesRead, nil) then
begin
// a requirement for Windows OS system components
OemToChar(#TextBuffer, #TextBuffer);
TextString := AnsiString(TextBuffer);
SetLength(TextString, BytesRead);
mOutput.Lines.Add (TextString);
end;
end
else
showmessage ('No text');
This does capture the output and deposit it in the TMemo, but what I don't understand is why (after much trial and error), setstdhandle assigns the outputPipeWrite to stdout but reading the pipe is via outputPipeRead? Inspiration for this code came from http://www.tek-tips.com/faqs.cfm?fid=7402
As I explained in comments, the way to solve this is to create a pipe. Arrange that the write end of the pipe is attached to the standard output with a call to SetStdHandle. And read from the read end of the pipe and put that content into the memo.

Zebra Printer direct communication

Based on this question I have implemented the following code to send direct commands to my Zebra TLP2844
var
cmm: AnsiString;
i: integer;
begin
commands.saveToFile('path\to\a\file');
Printer.BeginDoc;
cmm := '';
for i := 0 to commands.Count-1 do
cmm := cmm + commands[i] + #10;
Escape(Printer.Canvas.Handle, PASSTHROUGH, Length(cmm), PAnsiChar(cmm), nil);
Printer.EndDoc;
end;
commands is a TSringList containing all the commands I want to send to the printer.
Note that I save all the commands to a text file.
Well, if I send this text file to print, via the driver preferences, using tools -> Action -> Send File, it prints perfectly.
If I use the code above, it spits some extra rows of labels after printing.
It shows me, obviously, that I am doing something wrong here, but I can't figure out what.
What I have tried
Send commands one by one and not concatenating them like in the code. Results: Nothing gets printed.
Changing #10 for a #13#10. Results: Same crazy behaviour (indeed Zebra EPL documentatins says it will ignore any #13 it finds)
What else should I try in order to send to the printer the commands the exact same way Zebra's tool does?
AFAIK you need to format the buffer as expected by the ExtEscape() API layout. I never used Escape(), but ExtEscape() - and it worked with a Zebra printer.
Here is what the MSDN doc states:
lpszInData [in] A pointer to the input structure required for the
specified escape. The first word in the buffer contains the number of
bytes of input data. The remaining bytes of the buffer contain the
data itself.
So you may code this as such:
cmm := '00'; // reserve space for the initial `word`
for i := 0 to commands.Count-1 do
cmm := cmm + commands[i] + #10;
pword(cmm)^ := length(cmm)-2; // store the length
if ExtEscape(Printer.Canvas.Handle, PASSTHROUGH, Length(cmm), pointer(cmm), 0, nil)<0 then
raise Exception.Create('Error at printing to printer');
Printer.EndDoc;
Be aware that if your command is not well formatted (e.g. missing chars), it may just create an out of memory error in the printer spooler - yes, I've seen that! In this case, you may have to kill then restart the Printer Spooler service... fix your code... and try again...
And do not forget to put the ESC character at the beginning of each of your commands[], as requested by the Zebra doc.
you can use this procedure: where the LabelFile is the full path of the label file,we are using this code and works with generic text driver printer and the printer is set as the default printer. it works with zebra printer and windows xp operating system.
https://stackoverflow.com/a/27647044/2977139
i hope this will help you.
If you want to use the Windows printer driver, you should use WritePrinter defined un the WinSpool unit. If I see this correctly, the TPrinter object from the Printers unit doesn't expose it's FPrinterHandle member, so you might need to use OpenPrinter and ClosePrinter yourself.
Having worked with MarkPoint printers at work, which are somewhat similar to Zebra printers: if the printer is connect to a serial port, I would warmly suggest to try and access the printer directly by connecting to the serial port with one of the several components available.

Sending commands directly to Zebra EPL

I am trying to send commands directly to a Zebra TLP2844 printer. I followed the suggestion made here and my final code came to be as follows:
var
cm: String;
p: TPrinter;
i: integer;
begin
p := Printer;
p.BeginDoc;
for i := 0 to memo1.Lines.Count-2 do
begin
cm := memo1.Lines[i];
if Escape(p.Canvas.Handle,
PASSTHROUGH,
Length(cm),
PAnsiChar(cm),
nil) = 0 then
ShowMessage('Command error: ' + IntToStr(GetLastError));
end;
p.EndDoc;
end;
The content of memo1 is (first line is empty) as purposed here:
N
q609
Q203,26
B26,26,0,UA0,2,2,152,B,"603679025109"
A253,26,0,3,1,1,N,"SKU 6205518 MFG 6354"
A253,56,0,3,1,1,N,"2XIST TROPICAL BEACH"
A253,86,0,3,1,1,N,"STRIPE SQUARE CUT TRUNK"
A253,116,0,3,1,1,N,"BRICK"
A253,146,0,3,1,1,N,"X-LARGE"
P1,1
The commands don't seem to be properly received or interpreted by the printer. I checked that the printer is in Page Mode (EPL2), with the suggested code I am able to open the printer handle. But nothing is printed, only a new line of labels is feeded.
I tried to completely change the commands to something obviously wrong and the behaviour is the same.
What else should I be looking to get things printed?
Most printers that take raw commands require a prefix (starting sequence of characters) and suffix (ending sequence of chars) wrapping each command. I don't know what the prefix and suffix are for the Zebra, but the documentation should tell you.
Just add a pair of constants to define the prefix and suffix, and add them to your command before sending it.
The other issue might be that you're reading the content of your commands from a TMemo, which in Delphi 2009 and higher contains Unicode strings. You're then casting them down to PAnsiChar, which may be truncating the content. Do the conversion ahead of time by defining cm as an AnsiString, and then assigning to it first (as you are) before typecasting to pass to the Escape function. I've done this in my code to illustrate it.
var
cm: AnsiString;
p: TPrinter;
i: integer;
const
ZPrefix = AnsiString('$('); // Replace values for each of these with what
ZSuffix = AnsiString(')$'); // your documentation says you should use
begin
p := Printer;
p.BeginDoc;
for i := 0 to memo1.Lines.Count-2 do
begin
cm := ZPrefix + memo1.Lines[i] + ZSuffix;
if Escape(p.Canvas.Handle,
PASSTHROUGH,
Length(cm),
PAnsiChar(cm),
nil) = 0 then
ShowMessage('Command error: ' + IntToStr(GetLastError));
end;
p.EndDoc;
end;
I program in php which is like C
I can send things to the printer just fine
my code looks like your code the only thing is I am not sure how your programming language handles the newline in php it's \n at the end of each line
if the newline is not there the print job will not print
and if the " are not sent it will not print
your EPL looks fine and should print
there is somewhere on the zebra web site a download where you can send commands to a printer which is hooked up to your computer by USB cable
think it is called Zebra Setup Utilities

Resources