Adafruit mini thermal printer, part 3/?: Long jobs, cancellation and paper out

Writing a printer driver from scratch is quite involved. Who knew?

Code on github: https://github.com/edrosten/adafruit-thermal-printer-driver. Note: I wrote these posts as I went along so there may be bugs in the code snippets which are fixed later. I recommend checking the GitHub source before using a snippet.

This post appears to be about three unrelated things but it isn’t. It’s all about reading back data from the printer.

Cancellation

So, cancellation works in as much as things stop printing. Except none of the end of job stuff gets printed (the “cancelled” message and the paper eject). First I thought it was because I was lazy, so I changed the signal handler to:

	{
		struct sigaction int_action;
		memset(&int_action, 0, sizeof(int_action));
		sigemptyset(&int_action.sa_mask);
		int_action.sa_handler = [](int){
			cancel_job = 1;
		};
		sigaction(SIGTERM, &int_action, nullptr);
	}

This is the approved method, since the signal method is ill specified in general and on Linux on entry to the handler, it causes the handler to get reset to the default (terminate). I thought maybe that was happening. Do you think this worked?

The next step was to add LogLevel debug to /etc/cups/cupsd.conf, so it records all my debug messages. It does, along with a bunch of other useful stuff and its all indexed by the print job number. A filtered log looks like this:

D [29/Dec/2019:14:21:18 +0000] [Job 184] envp[25]=\"PRINTER=pl\"
D [29/Dec/2019:14:21:18 +0000] [Job 184] envp[26]=\"PRINTER_STATE_REASONS=none\"
D [29/Dec/2019:14:21:18 +0000] [Job 184] envp[27]=\"CUPS_FILETYPE=document\"
D [29/Dec/2019:14:21:18 +0000] [Job 184] envp[28]=\"FINAL_CONTENT_TYPE=application/vnd.cups-raster\"
D [29/Dec/2019:14:21:18 +0000] [Job 184] envp[29]=\"AUTH_INFO_REQUIRED=none\"
D [29/Dec/2019:14:21:19 +0000] [Job 184] Start rendering...
D [29/Dec/2019:14:21:19 +0000] [Job 184] Set job-printer-state-message to "Start rendering...", current level=INFO
D [29/Dec/2019:14:21:19 +0000] [Job 184] Processing page 1...
D [29/Dec/2019:14:21:19 +0000] [Job 184] Set job-printer-state-message to "Processing page 1...", current level=INFO
D [29/Dec/2019:14:21:19 +0000] [Job 184] PAGE: DEBUG: Read 2 bytes of print data...
D [29/Dec/2019:14:21:19 +0000] [Job 184] 1 1
D [29/Dec/2019:14:21:19 +0000] [Job 184] bitsperpixel 8
D [29/Dec/2019:14:21:19 +0000] [Job 184] BitsPerColor 8
D [29/Dec/2019:14:21:19 +0000] [Job 184] Width 384
D [29/Dec/2019:14:21:19 +0000] [Job 184] Height799
D [29/Dec/2019:14:21:19 +0000] [Job 184] feed_between_pages_mm 0
D [29/Dec/2019:14:21:19 +0000] [Job 184] mark_page_boundary 0
D [29/Dec/2019:14:21:19 +0000] [Job 184] eject_after_print_mm 10
D [29/Dec/2019:14:21:19 +0000] [Job 184] auto_crop 0
D [29/Dec/2019:14:21:19 +0000] [Job 184] enhance_resolution DEBUG: Wrote 2 bytes of print data...
D [29/Dec/2019:14:21:19 +0000] [Job 184] 0
D [29/Dec/2019:14:21:19 +0000] [Job 184] Feeding 155 lines
D [29/Dec/2019:14:21:19 +0000] [Job 184] Feeding 47 lines

It has the outputs from various filters all mixed together, possibly with some race conditions… (can you spot them?). Anyway, the cancel message is coming through and getting processed correctly. But no output is happening.

Debugging this was tricky because there were several causes. What I eventually did was add a 100ms pause between lines in order to reduce the amount of paper wasted and that revealed something interesting. One case was simply that sometimes the heating level was too low and the text was invisible.

In the other case, I’m just not sure. If the buffer is too full, then the last bits of the job seem to get “lost” somehow, if a cancellation occurs. With a 100ms pause, I always get the cancellation message. If I make the pause shorter then the printer can’t keep up and after a while the buffers all become full. In that case, I get cancellation messages if done early (when the buffers aren’t yet full) but not late.

I don’t yet know if long jobs get truncated. I suspect that the same would happen because there appears to be nothing functionally different between cancellation and normal termination. I don’t know who is responsible for this, but I’d be surprised if it was CUPS. My guess is no one has ever tested printing large amounts of full page bitmaps on this printer simply because that’s not the intended use. Speaking of not the intended use…

Abuse of paper sensors

As far as I can tell there isn’t an obvious way to query the buffer status to avoid it getting too full. I don’t even know where the buffer is. I expect the USB system has one, as does the USB chip and the UART on the printer.

But the printer does have a “Transmit Status” command (Page 42) for which it warns that there may be a lag since it’s processed in sequence. Even worse/better you can’t use this one to detect paper out because once the paper ends, the printer goes offline and won’t execute the command (I expect the paper sensor status command may be more asynchronous). Also that appears to be untrue, I tried it with the following code:

exec 3<> /dev/usb/lp0
echo -ne '\x1dr1' >&3
dd bs=1 count=1 status=none <&3 | od -td1 

And I got back 0 with paper in and 12 with the door open.

That apparently useless synchronous mechanism may be just the ticket: I bet if I stuff the command stream with these then I can get an approximation of the number of lines printed. The code looks something like this:

void transmit_status(){
	cout << GS << "r1" << flush;
}


void wait_for_lines(const int lines_sent, int& read_back, int max_diff){
	for(;;){
		char buf;
		ssize_t bytes_read = cupsBackChannelRead(&buf, 1, 0.0);

		if(bytes_read > 0)
			read_back++;

		if(lines_sent - read_back <= max_diff)
			break;

		cerr << "DEBUG: buffer too full (" << lines_sent - read_back << "), pausing...\n";
		using namespace std::literals;
		std::this_thread::sleep_for(100ms);
	}
}

// ... and in the main print loop...
			//Stuff requests for paper status into the command stream
			//and count the returns. We allow a gap of 80 lines (1cm of printing)
			transmit_status();
			lines_sent++;
			wait_for_lines(lines_sent, read_back, 80);

Checking the print logs shows this does what is expected. Furthermore, cancellation works properly (it prints the cancelled message and ejects the job) and is pretty quick!

Paper out!

OK, so I’m already reading the paper status. The manual suggests I might not be as I mentioned except I’m reading it before/after every line, so in that case I think I’m safe. Besides, it’s not entirely clear how you’re meant to differentiate between all the async replies:

When Auto Status Back (ASB) is enabled using GS a, the status
transmitted by GS r and the ASB status must be differentiated using.

(page 42)

Maybe some of the undefined bits are actually set. Who knows?

Anyway, all that remains is to transmit that back to CUPS. It’s broadly covered here.

BAH!

It didn’t work. Turns out the manual is only not right in very specific circumstances. Fortunately it seems for the status command bit 5 is always set so I could test for that.

So I stuffed the command stream with the proper status reports too and, well, guess what?

I just got a big old stream of zeros back from the printer. I could try the async reporting. That might work, but the printer has only a single sensor and stops running when it’s tripped. What I could do is see if nothing has changed for some time and report that as a paper out event.

This seems a bit hacky and it is. I’m not all that surprised though. This family of printers are mostly RS/232 based with asynchronous status lines in addition for paper, not USB. They’re also not expected to print out vast amounts of data; receipts are usually a few pages at most of plain text. I expect these obscure paths haven’t been exercised much.

Oh yes, hacky. So, here’s the code, it’s pretty straightforward overall:

void wait_for_lines(const int lines_sent, int& read_back, int max_diff){
	using namespace std::literals;
	using namespace std::chrono;

	auto time_of_last_change = steady_clock::now();
	bool has_paper=true;

	for(;;){
		char buf;
		ssize_t bytes_read = cupsBackChannelRead(&buf, 1, 0.0);

		if(bytes_read > 0){
			read_back++;
			
			if(!has_paper){
				cerr << "STATE: -media-empty\n";
				cerr << "STATE: -media-needed\n";
				cerr << "STATE: -cover-open\n";
				cerr << "INFO: Printing\n";
			}
				
			has_paper = true;
			time_of_last_change = steady_clock::now();
		}
		else if(auto interval = steady_clock::now() - time_of_last_change; interval > 2500ms){
			cerr << "DEBUG: no change for " << duration_cast<seconds>(interval).count() << " seconds, assuming no paper\n";
			if(has_paper){
				cerr << "STATE: +media-empty\n";
				cerr << "STATE: +media-needed\n";
				cerr << "STATE: +cover-open\n";
				cerr << "INFO: Printer door open or no paper left\n";
			}
			has_paper = false;
		}

		cerr << "DEBUG: Lines sent=" << lines_sent << " lines printed=" << read_back << "\n";

		if(lines_sent - read_back <= max_diff)
			break;

		cerr << "DEBUG: buffer too full (" << lines_sent - read_back << "), pausing...\n";
		std::this_thread::sleep_for(100ms);
	}
}

I’ve gone for an all inclusive approach with the messages. The printer cannot distinguish between the door being open and a lack of paper, so I’ve reported both.

It works!

The driver is now feature complete for a first version at any rate. There’s some minor image quality problems in normal mode (caused by fast feeds before bitmaps) and a bit of stripyness caused by poor calibration in enhanced mode. And the plain text filter probably should be a proper filter that does status read back and buffering. But it isn’t.

One thought on “Adafruit mini thermal printer, part 3/?: Long jobs, cancellation and paper out

  1. Pingback: Adafruit mini thermal printer, part 2/?: CUPS and other vessels | Death and the penguin

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s