# 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));
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;

break;

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

// ... 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++;

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;

bool has_paper=true;

for(;;){
char buf;

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;
}
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";

break;

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

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.

# Adafruit mini thermal printer, part 2/?: CUPS and other vessels

I bought a printer and have blogged about it because it’s literally the most interesting thing ever.

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 is about integrating with CUPS so I can print from normal programs.

## Integrating with CUPS

So, I have a sort of working example of CUPS integration in the existing ZJ-58 driver. It is, I suspect not very good. Nonetheless, I’ll start there since it’s vastly easier starting from a working example than the documentation.

Note from the future: The documentation…

It does exist, but it’s scattered over the various projects, those being PostScript, other Adobe printer guff, CUPS, the GhostScript interpreter, the printer working group and so on). It’s the type of documentation where you can’t find anything so you do 95% of the work the hard way, get stuck on an obscure API call/keyword/etc and then that string turns up the documentation you needed at the beginning.

Anyway, Here’s the install script from the existing driver:

#!/bin/bash

# Installs zj-58 driver
# Tested as working under Ubuntu 14.04

/etc/init.d/cups stop
cp rastertozj /usr/lib/cups/filter/
mkdir -p /usr/share/cups/model/zjiang
cp ZJ-58.ppd /usr/share/cups/model/zjiang/
cd /usr/lib/cups/filter
chmod 755 rastertozj
chown root:root rastertozj
cd -
/etc/init.d/cups start

That’s pretty simple: basically it dumps some files into the CUPS tree and restarts CUPS.

From what I understand, CUPS essentially has some sort of specification of various filter chains (which can vary based on the input, e.g. a plain text file, a postscript file and a JPG will have different input filters). A given filter lists its accepted inputs and CUPS works backwards to figure out how to generate what’s required. For raster things (i.e. not plain text when the printer can accept plain text) CUPS will rasterise the input and you need to then get it sent to the converter to convert it to the right control codes (mostly the topic of the previous post).

Many of those are controlled by a PPD file. This stands for “PostScript Printer Description” and tells CUPS all about the printer capabilities. It also has extensions beyond the Adobe PPD spec to allow you to specify rasterisation and filters for non PostScript printers.

The driver of course comes with a PPD (it has to), but it’s long, complicated, has fragments of PostScript in it and doesn’t even pass the tests run by the cupstestppd command. And there’s a lot of duplicated information about pages sizes which I suspect needs to be consistent. Not great but it’s a start.

So, while PPD is documented (or some approximation thereof) and the CUPS extensions are likewise, apparently you’re not really meant to write PPDs anyway. The easy/approved way is to write DRV files and then compile them into one or more PPD files using ppdc.

Either way the documentation is poor. There are lots of attributes in existing PPD and DRV files like “Filesystem” that it’s very hard to find any kind of documentation for and others like “PSVersion” which are weakly documented (what are the acceptable range of values?).

Some information is here. On the subject of PSVersion, typing revision = into a GhostScript interpreter reveals that my machine (Ubuntu 18.04) has revision 926 for whatever that’s worth. Either way it seems optional. Anyway, I’ve tried to pare down my DRV file to the absolute minimum which covers what I want and I got this:

#include <font.defs>

DriverType custom  //Required I believe to set downstream filters
ManualCopies Yes //Set to yes if the driver doesn't know how to print multiples of pages
Attribute "LanguageLevel" "" "3" //Default is 2 (from 1991), latest version is from 1997
Attribute "DefaultColorSpace" "" "Gray" //Self explanatory except does this mean something else can change it?
Attribute "TTRasterizer" "" "Type42" //Default is none, Type42 is the only extant useful one.
Filter application/vnd.cups-raster 0 rastertoadafruitmini //Arguments are datatype to feed to the filter, the expected CPU load, and the name of the filter executable
ColorDevice False

Font * //Include all fonts

// Manufacturer, model name, and version of the driver
ModelName "Mini"
Version 1.0
ModelNumber 579 //That's the product number on the website.

//I believe this allows users to specify custom sizes in the
//print dialog, or on the command line.
VariablePaperSize Yes
MinSize 58mm 5mm
MaxSize 58mm 1000mm

//#media creates media definitions which may or may not be used
//The paper is always 58mm wide, and have for now three different
//lengths
#media "58x50mm" 58mm 50mm
#media "58x100mm" 58mm 100mm
#media "58x200mm" 58mm 200mm

//The print area is always 48mm wide, centred
HWMargins 5mm 0 5mm 0

//This actually uses the media definitions above
*MediaSize "58x50mm"
MediaSize "58x100mm"
MediaSize "58x200mm"

// Supported resolutions
// Use as: Resolution colorspace bits-per-color row-count row-feed row-step name
// Apparently mostly the row stuff is 0 in most drivers. The last field
// (name) needs to be formatted correctly
*Resolution k 8 0 0 0 "208dpi/208 DPI"

// Name of the PPD file to be generated
PCFileName "mini.ppd"

OK, strictly speaking this isn’t the absolute minimum, since I’ve specified several virtual page sizes and variable sized pages, which is how CUPS deals with roll media. Here’s the corresponding install shell script to dump things in the right place:

/etc/init.d/cups stop

/etc/init.d/cups start

Now, running that and going to http://localhost:631 and going through the motions shows the printer there with the options I’d expect (i.e. paper size). The printer device appears as “unknown” in CUPS since it works as a USB parallel port (/dev/usb/lp0), but doesn’t report anything back to CUPS. Even with that , it won’t work yet, because I need in no particular order

• Proper information logging to stderr in a format that CUPS likes
• Deal with commandline arguments that CUPS hands me
• Handle SIGTERM (used to cancel jobs) and not leave the printer in a bad state

In addition, you can add arbitrary choices to the driver which get passed on to the filter so I think I’ll add ones for feeding paper after the job has done (so the end of the last page ends at tearoff on the printer), auto cropping pages (removing white space at the top and bottom–useful for roll media), and marking page boundaries. Because why not? I only have to implement them later.

Options are implemented using an option directive followed by a bunch of choice directives, e.g.

Option "TestOption" PickOne DocumentSetup 0
*Choice "A" ""
Choice "B" ""
Choice "C" ""

You can have Boolean, PickOne or PickMany. I don’t really see the point of Boolean: all of them need to have choice directives (for reasons which will soon become clear), so there’s little difference between a Boolean and a PickOne with two options.

The only difference seems to be that it renders a boolean as a radio group not a drop down list in the web interface:

hmmm. I wonder…

OK Confirmed! You can have as many “boolean” choices as you like, though note that the troolean choices don’t appear in the print dialog boxes, whereas booleans appear as checkboxes. Neither the compiler nor the validator complained which seems like a mild oversight.

With that silly aside out of the way, the next bit is how those options are passed to the printer driver. It turns out there are two ways, both of which are applied simultaneously.

The first, is that the options are passed as a command line argument to the filter, along with the PPD file (in the PPD environment variable). The CUPS API provides some handy functions for parsing PPD files and option strings and generally dealing with it.

The second is that each choice comes with an arbitrary snippet if PostScript code which is run at the point specified by the option directive (it can be at places like document start, page start). Now PostScript has a setpagedevice command which basically accumulates a dictionary for device specific use. The CUPS driver will put certain elements in that dictionary into the raster page headers, and you can access them from C in the filter. It doesn’t support arbitrary dictionaries, and in fact what it has is:

unsigned cupsInteger[16];
float cupsReal[16];
char cupsString[16][64];

You can fill these up by putting appropriately named things into the dictionary, e.g.:

<</cupsInteger1 10 /cupsReal7 2.2 /cupsString3 (a string)>> setpagedevice

W00t! I just found the documentation (by searching for cupsInteger0 to see if it was 0-based or 1-based; it’s 0-based). Turns out there are loads of parameters you can pass this way. Many have “accepted” meanings but you can abuse them to pass arbitrary data since you control both sides.

The two choices are pretty much equivalent, so I’ll pick… uh. Ummm OK wow I’m suffering from choice indecision here. OK, I’ll go for option 2. The API for option 1 is the usual annoying C faff, plus apparently it’s been deprecated since 2012 and I don’t have a nice example of the new API to copy from.

Putting all that together code added to the DRV file looks like this:

//The last argument is the order in which the order in which the options
//are executed (each one comes with a snippet of code to execute). In this
//case, all snippets are empty.
Option "PageFeed/Feed paper between pages" PickOne DocumentSetup 0
*Choice "None" "<</cupsInteger0  0>>setpagedevice"
Choice "1mm"   "<</cupsInteger0  1>>setpagedevice"
Choice "2mm"   "<</cupsInteger0  2>>setpagedevice"
Choice "5mm"   "<</cupsInteger0  5>>setpagedevice"
Choice "10mm" "<</cupsInteger0 10>>setpagedevice"

Option "PageMark/Mark where to cut pages" Boolean DocumentSetup 1
*Choice "No" "<</cupsInteger1 0>>setpagedevice"
Choice "Yes" "<</cupsInteger1 1>>setpagedevice"

Option "EjectFeed/Feed paper after printing" PickOne DocumentSetup 2
Choice "None"  "<</cupsInteger2  0>>setpagedevice"
*Choice "5mm"  "<</cupsInteger2  5>>setpagedevice"
Choice "10mm" "<</cupsInteger2 10>>setpagedevice"

Option "AutoCrop/Crop page to printed area" Boolean DocumentSetup 3
*Choice "No" "<</cupsInteger3 0>>setpagedevice"
Choice "Yes" "<</cupsInteger3 1>>setpagedevice"

The *’s indicate the default choices. And this so far appears to work! The web interface shows this:

Sweet!

## Writing a valid CUPS filter

This is actually documented reasonably well if you know where to look. I believe I can ignore all arguments (I’m using the other method for options, and I’ve told the driver I don’t know how to make copies myself) except the optional argv[6] which is the file to print if it’s not stdin. Yay.

Cancellation is easy: ignore SIGPIPE and clean up on SIGTERM. Since it’s a simple program, I can use a simple solution where I just poll a global variable:

volatile sig_atomic_t cancel_job = 0;
//...
signal(SIGPIPE, SIG_IGN);
signal(SIGTERM, [](int){ cancel_job = 1;});

Logging likewise is easy and involves writing to stderr something like TYPE: data where TYPE is the message type. The type has things such as ERROR, DEBUG, etc for logging, PAGE for recording the current page number, STATE for indicating things like paper empty and so on. The format of the data depends on the message type.

Paper empty and so on can be queried from the printer using special control codes and CUPS looks like it has a way to read back anything returned. I’m not so sure how this works yet. I’ll deal with that later.

Dealing with options took me far too long. I started with the following code snippet:

cups_raster_t *ras;
//...
{

and it didn’t really work. And by “didn’t work”, I mean that I tried adding -dcupsInteger0=1 to the GhostScript invocation (this sets an integer variable and somehow these magically wind up in setpagedevice, I don’t know how) and I could only set 0, 1 and 2. None of the other integers could be set.

If you cast your mind back to the first post in this series, I mentioned that I cargo-culted an invocation of GhostScript and wasn’t sure what everything did. Well, it came to bite me here. It has the innocuous looking argument -sMediaClass=PwgRaster (-s just sets a variable in the interpreter). This is now getting in quite deep. MediaClass is a variable which affects the setpagedevice command (page 21 of the PostScript® Language Reference Manual Supplement published in 1996 on April 1 and it is deadly serious) in various nonspecific (vendor defined) ways. And one such vendor is the shadowy cabal known as the “Printer Working Group” or PWG for short (its more exciting if they are a shadowy cabal). I sort of unearthed them by forlornly digging through cups/raster.h looking for clues and found this (edited) for display:

// The following PWG 5102.4 definitions specify indices into the
// cupsInteger[] array in the raster header.
#  define CUPS_RASTER_PWG_TotalPageCount	0
#  define CUPS_RASTER_PWG_CrossFeedTransform	1
// etc...
#  define CUPS_RASTER_PWG_VendorLength		15

Turns out they have defined their own meanings for the user-defined extensions and brazenly took all of them. What I don’t understand is why I could set 0, 1 and 2, but not 3 onwards. No clues there. It also stopped cupsReal and cupsString from working and set PWG_AlternatePrimary to 224-1. ¯\_(ツ)_/¯

## What went wrong

So that all sort of worked, and I can print out cats using lpr. Except…

### Inverted cats. And junk

The cats come out inverted, like this:

*Resolution k 8 0 0 0 "203dpi/203 DPI"

which is the “black” colour model. If I change the “k” to “w”, I get what I expect except with some junk at the top.

What I actually need is:

*ColorModel Gray/Grayscale w chunky 0
*Resolution - 8 0 0 0 "203dpi/203 DPI"

I don’t know why. The colour model specifies the white model (along with chunky which is means packed for colour data and no compression), then the resolution says to not modify the colour model. Ok, sure…

Nope!!

Turns out that wasn’t it. I must have just reset things when making that change. The junk was because… well I don’t know exactly. It doesn’t appear on the first printout, it only appears on the third. And if I send enough text to the printer then the next image is fine. It therefore appears as if something was getting flushed before the last line was complete. Then the first few bytes (including the start bitmap control code) were getting eaten up finishing the previous line and then it was printing data out as text.

void printerInitialise(){
cout << ESC << '\x40';
}

calls to which I sprinkled liberally around, and these are messing things up. Here’s the funny thing though: putting a cout << flush after the first one fixed it. That ought to make sense: the printer gets data asynchronously then starts processing it while the UART asynchronously fills the receive buffer. It processes the initialise command and loses the first few control codes. Or something.

Except… the symptoms only manifested after several images, making it look like it was state being carried over. It’s weird, I don’t get it. Clearly there’s some internal state somewhere, and part of me things is might be in CUPS because I suspect the original driver used to work just fine.

### Page Sizes

The print dialog boxes seemed to get deeply confused about the smallest page size (58x50mm). The reason for this it turns out is that it’s really a landscape page not a portrait one and pages need to be specified in portrait orientation. Except that would make the width wrong. If I’d paid attention to the warnings from cupstestppd, then I would not have had this problem.

ppd/mini.ppd: PASS
WARN    Size "58x50mm" should be the Adobe standard name "50x58mmRotated".

And it turns out all I have to do is switch the name:

#media "50x58mmRotated" 58mm 50mm
#media "58x100mm" 58mm 100mm
#media "58x200mm" 58mm 200mm

HWMargins 5mm 0 5mm 0

*MediaSize "50x58mmRotated"
MediaSize "58x100mm"
MediaSize "58x200mm"

and things seem to be much more sensible.

### Booleans

The print dialog box renderers don’t really know which option is meant to correspond to a check mark and which isn’t. I tried changing the keyword to “True” and “False” and putting true first in the list, e.g.:

Option "PageMark/Mark where to cut pages" Boolean DocumentSetup 1
Choice "True/Yes" "<</cupsInteger1 1>>setpagedevice"
*Choice "False/No" "<</cupsInteger1 0>>setpagedevice"

That seemed to do the job. I believe it’s the ordering that matters, I’m not sure though.

### Other stuff

There were a few other miscellaneous bits and bobs to fix too. In addition I implemented the various features I mentioned above. I decided also to emit blank lines as a feed rather than a blank line because it’s a fair bit faster. Except I had to suppress that in enhanced resolution mode, because otherwise the first few lines printed after a gap were too dark.

I also want the printer to print plain text as plain text. This isn’t necessary but it’s always been idiomatic to pass through like that, rather than relying on the postscript rasteriser. I can fix that with one extra line in the DRV file:

Filter text/plain 0 -

That tells CUPS that it accepts text, is no cost and to use a null filter program.

### Cancellation

Oh wow this turned out to be hard. Way harder than expected because it reveals deep problems. It’s going to be a whole other blog post.

## Result!

OK so basically it works!

I can print using lp (or lpr), and set options like -o Enhance=True -o PageMark=True and it obeys them.

Recognise this?

# Adafruit mini thermal printer, part 1/?: getting better pictures

I bought an AdaFruit Mini thermal printer.

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.

It’s pretty cute, and it’s actually very old school in terms of its function. Firstly in a very old fashioned twist, it comes with a full manual documenting every single control code. Not only that but the printer is surprisingly capable and it’s designed to work with very low end driving systems. It doesn’t just print bitmaps, it has various fonts and modes (double height, width, etc), you can download custom fonts and bitmaps to print on demand.  You can print upside down and back to front so the text looks the right way round if you’re facing the printer (super cute!). I has justification modes, bold and underline. It can even print barcodes!

You know this reminds me of when I was 14(?) and got my first computer, a BBC Micro complete with a 5.25″ floppy drive and a printer. The printer came with a manual with full documentation of all the control codes and I devoured them and wrote a basic typesetter like system in which I did my school projects.

So where was I?

Oh yes, well I don’t actually need most of those features. I’m planing on driving it from Linux (on a Pi), which means it’ll be driven by GhostScript via CUPS and will print bitmaps. And not use any of those features.

Turns out Adafruit provide a CUPS driver. Apparently provided from one provided by the printer maufacturer? So, I installed it and this is the result:

woo! it prints! Except… the output isn’t great. The printer is monochrome and the pictures come out halftoned using a halftone screen. While that’s a fine choice for various kinds of printing, it’s not great for a device with independent pixels. For that, a dithering method such as Floyd-Steinberg would be much better. Also it’s messing up the first line and printing junk, but you know details, details.

PostScript, being designed for proper printing has native support for halftoning. It doesn’t for dithering, and it turns out there’s no way to persuade it to emit a monochrome bitmap using dithering instead of halftone screens. If you want dithering, you need to do it in the driver. So, I’m going to need a custom driver.

So I first need to understand printing.

## Printing on Linux greatly simplified

Printing on Linux isn’t simple. Partly this is because printing in general is not simple. And partly it’s because printing has changed a lot over the years and there are lots of vestigial bits lying around. For common, modern systems the order of operations is roughly:

1. CUPS accepts jobs (and provides information to the print dialogs).
2. CUPS examines the file type and decides what to do next, e.g. whether to run it through GhostScript.
3. CUPS runs it through ghostscript generating a stream in CUPS raster format. This is a simple bitmap format with a C API.
4. CUPS runs some arbitrary filter program.
5. Filter program transforms CUPS bitmap into printer control codes.
6. CUPS routes the resulting data to the correct device.

GhostScript also has some printer drivers built in, an there are various other filter schemes (GhostScript is one of many) such as foomatic, and of course printers can accept plain text too. I’m not really interested in those so I’ll stick to the sequence above.

Steps 2-4 are controlled by a PPD (PostScript Printer Description) file, and 5 is a program which reads in CUPS bitmap data and emits control codes. The CUPS raster format is well documented but it seems simpler to use the C API, especially as I have a working driver to cadge from.

What I’m going to do first is figure out how to print out what I want (i.e. the right control codes) and then figure out how to work it into CUPS.

## Getting CUPS raster data and a simple driver

The first job is to get the input data. After a bunch of cargo-culting, I got this script:

DPI=203.2
gs -dPARANOIDSAFER -dNOPAUSE -dBATCH -sstdout=%stderr -sOutputFile=%stdout \
-sDEVICE=cups -sMediaClass=PwgRaster -sOutputType=Automatic -r${DPI}x${DPI} \
-dDEVICEWIDTH=384 -dDEVICEHEIGHT=384 -dcupsBitsPerColor=8 -dcupsColorOrder=0 \
-dcupsColorSpace=0 -dcupsBorderlessScalingFactor=0.0000 -dcupsInteger1=1 \
-dcupsInteger2=1 -scupsPageSizeName=na_letter_8.5x11in -I/usr/share/cups/fonts \
"$@" I don’t remember precisely how I found all the various bits. The important things are that it’s colourspace 0 (white), 8 bits per colour, CUPS raster format, 384 pixels wide and 8 pixels per mm. Everything else is just necessary guff (IO redirection, batch and no pause) or irrelevant stuff I never deleted. I then basically deleted everything except the stream processing from the driver, then deleted that and started writing from scratch. After lots of head scratching and making a lot of mistakes I read the manual more carefully (bitmaps are always a multiple of 8 pixels wide) and got this code up and running: #include <cups/raster.h> #include <iostream> #include <vector> #include <array> #include <utility> #include <cmath> using std::clog; using std::cout; using std::endl; using std::vector; using std::array; constexpr unsigned char ESC = 0x1b; constexpr unsigned char GS = 0x1d; // Write out a std::array of bytes as bytes. This will form the basis // of sending data to the printer. template<size_t N> std::ostream& operator<<(std::ostream& out, const array<unsigned char, N>& a){ out.write(reinterpret_cast<const char*>(a.data()), a.size()); return out; } array<unsigned char, 2> binary(uint16_t n){ return {{static_cast<unsigned char>(n&0xff), static_cast<unsigned char>(n >> 8)}}; } void printerInitialise(){ cout << ESC << '\x40'; } // enter raster mode and set up x and y dimensions void rasterheader(uint16_t xsize, uint16_t ysize) { // Page 33 of the manual // The x size is the number of bytes per row, so the number of pixels // is always a multiple of 8 cout << GS << 'v' << '0' << '\0' << binary((xsize+7)/8) << binary(ysize); } int main(){ cups_raster_t *ras = cupsRasterOpen(0, CUPS_RASTER_READ); cups_page_header2_t header; int page = 0; while (cupsRasterReadHeader2(ras, &header)) { /* setup this page */ page ++; clog << "PAGE: " << page << " " << header.NumCopies << "\n"; clog << "BPP: " << header.cupsBitsPerPixel << endl; clog << "BitsPerColor: " << header.cupsBitsPerColor << endl; clog << "Width: " << header.cupsWidth << endl; clog << "Height: " << header.cupsHeight << endl; // Input data buffer for one line vector<unsigned char> buffer(header.cupsBytesPerLine); clog << "Line bytes: " << buffer.size() << endl; printerInitialise(); /* read raster data */ for (unsigned int y = 0; y < header.cupsHeight; y ++) { if (cupsRasterReadPixels(ras, buffer.data(), header.cupsBytesPerLine) == 0) break; //Print in MSB format, one line at a time rasterheader(header.cupsWidth, 1); unsigned char current=0; int bits=0; for(const auto& pixel: buffer){ current |= (pixel>128)<<(7-bits); bits++; if(bits == 8){ cout << current; bits = 0; current = 0; } } if(bits) cout << current; } /* finish this page */ } cout << "\n\n\n"; cupsRasterClose(ras); } To run the program, make an eps file, ideally with a cat in it. Then assuming the above script is called “to_cups.sh” and the compiled executable is called “rastertoadafruitmini”, you can run it with: bash to_cups.sh cat.eps | ./rastertoadafruitmini | sudo dd of=/dev/usb/lp0 Note that the quantisation is simply “greater than 128”, and the result is: Note the false start at the top, and the slightly stretched image due to me converting to EPS badly. The underlying image is this: It works! You’ll note I got the colours inverted, because I had 1 for white, and 0 for black, whereas 1 means print a pixel (i.e. black). The black bar is because of that and the page being white. The funny thing is that black areas feel incredibly wasteful of ink even though that makes no sense on a thermal printer. ## Dithering the output Clearly a simple threshold is not a very good way of converting greyscale to black and white. In fact it’s somewhat worse than the original halftoned image. The key is to employ some sort of dithering and this is best done by some sort of error diffusion algorithm. The process works like this. While going in raster scan order: 1. Quantize the pixel current to 0 or 255 2. Work out the error between the quantized output and the pixel 3. Add fractions of the error to nearby pixels which haven’t been processed yet (this is the error diffusion step) There are quite a few articles on it, such as this excellent one. The most common/well know algorithm for images is the Floyd-Steinberg dithering algorithm. It’s popular because it’s low resource and efficient on simple processors. Since the target machine for this will be lavishly resourced (a Raspberry Pi) I decided to go for the Jarvis, Judice, Ninke algorithm which is essentially identical to Floyd-Steinberg but with a larger error diffusion window and is more expensive and gives slightly better results. Here’s the code (with the new bits highlighted): #include <cups/raster.h> #include <iostream> #include <vector> #include <array> #include <utility> #include <cmath> #include <algorithm> using std::clog; using std::cout; using std::endl; using std::vector; using std::array; constexpr unsigned char ESC = 0x1b; constexpr unsigned char GS = 0x1d; // Write out a std::array of bytes as bytes. This will form the basis // of sending data to the printer. template<size_t N> std::ostream& operator<<(std::ostream& out, const array<unsigned char, N>& a){ out.write(reinterpret_cast<const char*>(a.data()), a.size()); return out; } array<unsigned char, 2> binary(uint16_t n){ return {{static_cast<unsigned char>(n&0xff), static_cast<unsigned char>(n >> 8)}}; } void printerInitialise(){ cout << ESC << '\x40'; } // enter raster mode and set up x and y dimensions void rasterheader(uint16_t xsize, uint16_t ysize) { // Page 33 of the manual // The x size is the number of bytes per row, so the number of pixels // is always a multiple of 8 cout << GS << 'v' << '0' << '\0' << binary((xsize+7)/8) << binary(ysize); } constexpr array<array<int, 5>, 3> diffusion_coefficients = {{ {{0, 0, 0, 7, 5}}, {{3, 5, 7, 5, 3}}, {{1, 3, 5, 3, 1}} }}; constexpr double diffusion_divisor=42; int main(){ cups_raster_t *ras = cupsRasterOpen(0, CUPS_RASTER_READ); cups_page_header2_t header; int page = 0; while (cupsRasterReadHeader2(ras, &header)) { /* setup this page */ page ++; clog << "PAGE: " << page << " " << header.NumCopies << "\n"; clog << "BPP: " << header.cupsBitsPerPixel << endl; clog << "BitsPerColor: " << header.cupsBitsPerColor << endl; clog << "Width: " << header.cupsWidth << endl; clog << "Height: " << header.cupsHeight << endl; // Input data buffer for one line vector<unsigned char> buffer(header.cupsBytesPerLine); //Error diffusion data vector<vector<double>> errors(diffusion_coefficients.size(), vector<double>(buffer.size(), 0.0)); clog << "Line bytes: " << buffer.size() << endl; printerInitialise(); /* read raster data */ for (unsigned int y = 0; y < header.cupsHeight; y ++) { if (cupsRasterReadPixels(ras, buffer.data(), header.cupsBytesPerLine) == 0) break; //Print in MSB format, one line at a time rasterheader(header.cupsWidth, 1); unsigned char current=0; int bits=0; for(int i=0; i < (int)buffer.size(); i++){ //The actual pixel value with gamma correction double pixel = pow(buffer[i]/255., 1./2.2) + errors[0][i]; double actual = pixel>.5?1:0; double error = pixel - actual; //This error is then distributed //Diffuse forward the error for(int r=0; r < (int)diffusion_coefficients.size(); r++) for(int cc=0; cc < (int)diffusion_coefficients[0].size(); cc++){ int c = cc - diffusion_coefficients[0].size()/2; if(c+i >= 0 && c+i < (int)buffer.size() && diffusion_coefficients[r][cc]){ errors[r][i+c] += error * diffusion_coefficients[r][cc] / diffusion_divisor; } } current |= (pixel<0.5)<<(7-bits); bits++; if(bits == 8){ cout << current; bits = 0; current = 0; } } if(bits) cout << current; //Roll the buffer round. std::rotate(errors.begin(), errors.begin()+1, errors.end()); for(auto& p:errors.back()) p=0; } /* finish this page */ } cout << "\n\n\n"; cupsRasterClose(ras); } And here’s the result KITTY!!!!!!!!!!!!! But can we do better? I’m not sure, but look at this: You can draw on the paper using a finger nail. The faster you move at a given pressure the darker the line. I believe this is due to getting more heating. So, the paper is definitely analogue. Turns out the printer is too, kind of in that you can set the heat output per line (though not per pixel). The command is on page 47 and is the general control command. So what I did is print 255 solid lines, each one with a different heat output. The code (in AWK) is: BEGIN{ for(i=0; i < 255; i++){ printf("%c7%c%c%c", 27, 64, i, 2) printf("\x1dv0\0%c\0\x01\0", 40) for(j=0; j < 40; j++) printf("\xff") } print "\n\n\n\n" } What got me going for ages is that the locale wasn’t C, to characters above 127 were getting mangled. Anyway the results is this: That’s a yes! It’s a bit speckly, but it can definitely output greyscale. After a bit of messing around, I got the range. It goes from a timing (heat output is essentially controlled by setting the time the heating elements dwell on the paper) range of about 16 (full white) to 112 (full black) and empirically, raising the input to a power of 2 makes it look a little better. Working it into the dithering code is pretty straightforward: find the darkest pixel and set the black level to be able to reproduce that. #include <cups/raster.h> #include <iostream> #include <vector> #include <array> #include <utility> #include <cmath> #include <algorithm> using std::clog; using std::cout; using std::endl; using std::vector; using std::array; constexpr unsigned char ESC = 0x1b; constexpr unsigned char GS = 0x1d; // Write out a std::array of bytes as bytes. This will form the basis // of sending data to the printer. template<size_t N> std::ostream& operator<<(std::ostream& out, const array<unsigned char, N>& a){ out.write(reinterpret_cast<const char*>(a.data()), a.size()); return out; } array<unsigned char, 2> binary(uint16_t n){ return {{static_cast<unsigned char>(n&0xff), static_cast<unsigned char>(n >> 8)}}; } void printerInitialise(){ cout << ESC << '\x40'; } // enter raster mode and set up x and y dimensions void rasterheader(uint16_t xsize, uint16_t ysize) { // Page 33 of the manual // The x size is the number of bytes per row, so the number of pixels // is always a multiple of 8 cout << GS << 'v' << '0' << '\0' << binary((xsize+7)/8) << binary(ysize); } void set_heating_time(int time_factor){ // Page 47 of the manual // Everything is default except the heat time cout << ESC << 7 << (char)7 << (unsigned char)std::max(3, std::min(255,time_factor)) << '\02'; } constexpr array<array<int, 5>, 3> diffusion_coefficients = {{ {{0, 0, 0, 7, 5}}, {{3, 5, 7, 5, 3}}, {{1, 3, 5, 3, 1}} }}; constexpr double diffusion_divisor=42; double degamma(int p){ return pow(p/255., 1/2.2); } int main(){ cups_raster_t *ras = cupsRasterOpen(0, CUPS_RASTER_READ); cups_page_header2_t header; int page = 0; while (cupsRasterReadHeader2(ras, &header)) { /* setup this page */ page ++; clog << "PAGE: " << page << " " << header.NumCopies << "\n"; clog << "BPP: " << header.cupsBitsPerPixel << endl; clog << "BitsPerColor: " << header.cupsBitsPerColor << endl; clog << "Width: " << header.cupsWidth << endl; clog << "Height: " << header.cupsHeight << endl; // Input data buffer for one line vector<unsigned char> buffer(header.cupsBytesPerLine); //Error diffusion data vector<vector<double>> errors(diffusion_coefficients.size(), vector<double>(buffer.size(), 0.0)); clog << "Line bytes: " << buffer.size() << endl; printerInitialise(); /* read raster data */ for (unsigned int y = 0; y < header.cupsHeight; y ++) { if (cupsRasterReadPixels(ras, buffer.data(), header.cupsBytesPerLine) == 0) break; //Estimate the lowest value pixel in the row double low_val=1.0; for(int i=0; i < (int)buffer.size(); i++) low_val = std::min(low_val, degamma(buffer[i]) + errors[0][i]); //Add some headroom otherwise black areas bleed because it can't go //dark enough low_val*=0.99; //Set the darkness based on the darkest pixel we want //Emperical formula for the effect of the timing double full_white=16; double full_black=16*7; set_heating_time(pow(1-low_val,2.0)*(full_black-full_white)+full_white); //Print in MSB format, one line at a time rasterheader(header.cupsWidth, 1); unsigned char current=0; int bits=0; for(int i=0; i < (int)buffer.size(); i++){ //The actual pixel value with gamma correction double pixel = degamma(buffer[i]) + errors[0][i]; double actual = pixel>(1-low_val)/2 + low_val?1:low_val; double error = pixel - actual; //This error is then distributed //Diffuse forward the error for(int r=0; r < (int)diffusion_coefficients.size(); r++) for(int cc=0; cc < (int)diffusion_coefficients[0].size(); cc++){ int c = cc - diffusion_coefficients[0].size()/2; if(c+i >= 0 && c+i < (int)buffer.size() && diffusion_coefficients[r][cc]){ errors[r][i+c] += error * diffusion_coefficients[r][cc] / diffusion_divisor; } } current |= (actual!=1)<<(7-bits); bits++; if(bits == 8){ cout << current; bits = 0; current = 0; } } if(bits) cout << current; //Roll the buffer round. std::rotate(errors.begin(), errors.begin()+1, errors.end()); for(auto& p:errors.back()) p=0; } /* finish this page */ } cout << "\n\n\n\n\n\n"; cupsRasterClose(ras); } And it works!! OK, the results aren’t spectacular, but look if you hear a dog talking, you’re impressed that it can talk at all, not disappointed that it can’t talk well. The enhanced grey level image definitely has some horizontal streaking. I don’t know if that’s due to the printer or the really ad-hoc calibration of grey levels that I did. I should probably limit the rate at which the temperature changes vertically to mitigate that. Overall I’m really pleased. The finer details are clearer and there are definitely some whiskers which you can make out in the left image which are washed out in the speckle right one. Bare in mind there are optimistically 64 distinct grey levels this printer can produce which means this technique is adding about 6 bits per line of 384 bits. This also pushes the printer far, far beyond what it was ever supposed to do. The heating time is really a way to reduce print time and/or save on total energy draw, presumably for battery powered chip and pin machines. I expect there is more fiddling to do, but the next stage is to integrate it into CUPS so I can print the usual way (i.e. using lp of course). # P0533 will ride again Unfortunately, P0533 (see here for previous posts) didn’t make it into C++20 either (originally targeted at C++ 17). It seems that there were just too many good papers and they couldn’t work through them all in the available time. There’s lots of good stuff and clearly a strong and growing interest in constexpr’ing everything that can be constexpr’d, so I hold out hope for both it and P1383 in C++2.. uh… 2b? Or not 2b? Follow their progress in the trackers here: # More constexpr in C++20 The C++ Kona meeting starts tomorrow or if you prefer on February the 18th, 2019, since you’re probably not reading this on the day I’m writing it. Apart from looking forward to C++20 in general, it matters to me because I’ve got two closely related papers under consideration. TL;DR: Except since they’re C++ committee papers, it’s actually more like: Since the C++ committee doesn’t accept memes as proposals, what we actually submitted is: 1. P0533R4, a proposal to add constexpr to the elementary cmath functions. Elementary is in this case as on the same level as +,-,* and /, which we defined formally as closed over the rationals.Floating point numbers aren’t rationals of course, but arithmetic operations have the property that if you apply them to a rational number you get a rational number (unlike, say, sqrt) and importantly, there are no domain errors when fed with a finite number. Our property seemed to be the simplest one that fitted. Looking at the other functions, we found things like floor, ceil also fitted neatly. We though this paper would be straightforward since those functions really aren’t fundamentally different from arithmetic, so the same rules should apply. 2. P1383R0 a proposal to constexpr most of the rest of cmath. The LEWG pretty much liked P0533 and asked if we could submit a further paper to add constexpr to the non elementary functions.This paper adds most of the rest (sin, cos, exp, log, lgamma, etc). After some useful discussion, we excluded the cmath special functions, on the grounds that there are very few people in the world who know how to implement them so having them in could be a serious burden on compiler vendors. If they get in then this post will finally be legal C++. # Linear 3D triangulation I came across this 3D linear triangular method in TheiaSFM: bool TriangulateNView(const std::vector<Matrix3x4d>& poses, const std::vector<Vector2d>& points, Vector4d* triangulated_point) CHECK_EQ(poses.size(), points.size()); Matrix4d design_matrix = Matrix4d::Zero(); for (int i = 0; i < points.size(); i++) { const Vector3d norm_point = points[i].homogeneous().normalized(); const Eigen::Matrix cost_term = poses[i].matrix() - norm_point * norm_point.transpose() * poses[i].matrix(); design_matrix = design_matrix + cost_term.transpose() * cost_term; } Eigen::SelfAdjointEigenSolver eigen_solver(design_matrix); *triangulated_point = eigen_solver.eigenvectors().col(0); return eigen_solver.info() == Eigen::Success; } I was aware of the DLT (direct linear transform), but it didn't look like any formulation I've seen before. It's actually pretty neat. Let's say you're trying to find an unknown homogeneous point in 3D, $\mathbf{X} = [X, Y, Z, 1]$. What we have is $N$ poses, $P$, represented as $3\times 4$ matrices and the corresponding 2D coordinates represented as homogeneous points in $\mathbb R^3$. The 2D points are written as $\mathbf{x} = [ x, y, 1]$. Since we're triangulating the 3D point, and we have homogeneous coordinate (i.e. $\alpha \mathbf{x} \equiv \mathbf{x}$) then for all $i$ we should have: $\alpha_i \mathbf{x}_i \approx P_i \mathbf X$ given an scale factor $\alpha$. Now let's pick apart the code above. Let's call design_matrix $D$ and cost_term $C$. On line 12, we have: $\displaystyle D = \sum_{i=1}^{N} C_i^\top C_i$ And line 15 we’re finding the eigenvector corresponding to the smallest eigenvalue of D (SelfAdjointSolver produces them in a sorted order), i.e. $\mathbf{X} \approx \displaystyle \underset{\mathbf{v}, |\mathbf{v}|=1}{\text{argmin}}\ \mathbf{v}^\top D \mathbf{v}$ We can rewrite $D = \mathbf{C}^\top\mathbf{C}$ where: $\mathbf{C} = \left[ \begin{matrix} C_1\\ C_2\\ \vdots\\ C_N\\ \end{matrix}\right]$, which substituting in above gives: $\mathbf{X} \approx \displaystyle \underset{\mathbf{v}, |\mathbf{v}|=1}{\text{argmin}}\ \|\mathbf{C v}\|_2^2$, which is of course the right singular vector corresponding to the smallest singular value of $C$. Using eigen decomposition is much more efficient the size is $O(1)$, not $O(N)$, but probably at the penalty of less numerical precision. Either way we’re trying to find the approximate nullspace of $\mathbf{C}$, which means finding something that’s roughly in the null space of all the $C_i$s. But why? On lines 8–11, we have: $C_i = P_i - \mathbf{\hat{x}\hat{x}^\top}P_i$, and we’re claiming $\mathbf{X}$ is about in the null space. Let’s see what happens when we multiply by it: $(P_i - \mathbf{\hat{x}\hat{x}^\top}P_i) \mathbf{X} = P_i \mathbf{X} -\mathbf{\hat{x}\hat{x}^\top}P_i \mathbf{X}\\$ Now, substituring in the first equation we have all the way back at the top gives: $\approx \alpha \mathbf{x} - \alpha\mathbf{\hat{x}\hat{x}^\top x} = \alpha \mathbf{x} - \alpha\mathbf{\hat{x} |x|} = \alpha \mathbf{x} - \alpha\mathbf{x} = 0$ taa daa! So there you go! If there is no noise, $\mathbf{X}$ is in the right null space of $C_i$ and so the null space of $\mathbf C$ and of course $D$. If there is noise, then it’s closest to being in the null space of all of $C_i$ measured in a least squares sense. Note though that it’s just an algebraic error, not a reprojection error so it will give slightly oddly scaled results which will give more weight to some points than others. It is however a rather elegant solution. # Neutrogena light mask part 3: about that LCD… In Part 1 I hacked it to get 99 lives. In Part 2 I did an excessively thorough analysis of the current limiting. In Part 4 I found it keeps resetting on life 83. The light mask comes with an LCD. I’ve always been curious about driving them, but never really taken the time to look. So, I figured I’d get my scope out and have a go. First off, it’s two seven segment displays. Rather handily, Neutrogena have left nice large test points for me. And if you look closely they even saved me the hassle of counting the number of them! There are 8. So, it must be a multiplexed display. Assuming it’s even vaguely symmetric, it’s got to be something like 5×3 or 4×4 (with some dead spots). So, time to break out the scope! First though, I have to solder some wires on: Except they don’t fit under the LCD. Plus if you connect the power up backwards, then it appears to stop working. Why knew? Fortunately I have one more spare. The failure mode seems to be that one digit is no longer working (the LCD works though—I rubbed some power supply leads across the zebra strip and they all lit up.). Weird. Um… Yes OK, so it’s actually working fine (which is weirder) it’s just that it displays “0”, not “00” because it’s made for end users who aren’t expecting everything to be in nicely formatted hex… So, I’m vaguely aware of some things about LCDs, in no particular order: • They activate off DC, but that eventually destroys them so you should use AC. • Driving simple (non multiplexed) LCDs is easy. • Driving multiplexed ones is harder than you’d expect . • There’s a threshold voltage below which they don’t turn on. And here’s what the waveform for the first 4 pins looks like: I blame my scope. No way it’s a PEBKAC error. There are like, 4 levels and they go all over the place. It’s crazy. OK, that is indeed harder than I’d expect. Reading around was a bit boring, confusing and badly written. There’s some information which indicates it’s possible to drive them off only 2 levels. This would make sense: if you want to make a particular row/column pair light up, then you drive those two out of phase. Everything else… Oh I see. So, I guess the other rows have to be driven in-phase with the column, and the other columns… hmm. OK, I guess that’s why they have multiple levels. If you have a 5V supply, and you drive the row/column out of phase, the one intersecting segment sees 10v pk-pk. If you drive the other rows and columns with an idle voltage (say 2.5V DC) then the segments see either 0V if neither the row/column is driven or 5V pk-pk if a row/column is driven and the column/row is idle. Backing up, imagine driving a matrix of light bulbs (not LEDs because I don’t want to get bogged down in Charlieplexing) from switches: A multiplexed display matrix with one rwo switch and two column switches closed. Switches are either closed, which on a microcontroller means driving the pin as 1 or 0 depending on which rail the switch is connected to, or open which means tristated. For current driven things, like light emitters, it’s easy: tristated means no current flows. For voltage devices, it simply means “not actively driven”, so something needs to be done with it, i.e. bias it to some neutral voltage. I have no idea which things are rows and which things are columns on the display. However, I do know that the voltages are 0, 1.6, 3.3 and 5V. I’d hazard a guess that 3v pk-pk (i.e. a neighbouring pair) won’t drive the display but 10V pk-pk will. Not sure about 6.7. Probably? Well, I’ve got an Arduino and some voltage regulators. For a 5V drive, I can easily get 0, 2.5V and 5V by tristating an output and pulling it to a central value: Pulling the pins to a 2.5V rail made with a potential divider and a TS358 opamp (costs 6p!) I used 47k because it’s high ish (won’t load the Arduino or opamp much) and I have plenty of them. Anything in the range of about 1k to several meg would probably work OK. I could use the 3.3V output as the central value but honestly that seems to be tempting fate and I don’t know if it can sink current. Instead, I’ll use the entertainingly cheap TS358CD op-amp. So, time to cut out the useless remains of the device and wire up the Arduino! I can still fix this! Also, the test pads aren’t all they’re cracked up to be: the joints need to be filed down. Even now I’m not sure I’m getting perfect contact between the LCD and the board. I had to file the tops of the joints down very carefully (totally unnecessary as it transpired). Anyway I’ve wired up pins 1-8 on the arduino (not 0!) to 1-8 on the LCD, more for my own sanity than anything else. And with a simple program: void setup(){} void loop() { pinMode(1, OUTPUT); digitalWrite(1, HIGH); delay(1); digitalWrite(1, LOW); delay(1); pinMode(1, INPUT); delay(1); } I get this scope trace: This had me going for quite a while. The pin set to input should be getting 2.5V. But it’s not; it’s being pulled up. I looked up: the internal pullup is 20k. The voltage is consistent with that: 2.5 * 47 / (47 + 20) + 2.5 ≈ 4 ish. Well, that had me going for a while. I went back and forth with trying to figure out how to turn the pullup off (it’s off by default), turning it on and seeing what happened, plugging and unplugging wires and all that. Turns out that I was using pin1 which on the Arduino is the TX pin for the serial port if you want it to be. That means it has an LED attached which is doing extra pullup effectively to almost exactly the right value. Ho ho ho. So looks like I’ll be using pins 2-9 instead and I won’t get to keep my sanity. But at least that works. Also I realised after a, er, little debugging that the reason the device has screws next to the LCD is so that they push the board against the zebra strip ensuring good contact. I wonder if I should use those screws… Anyhow, now I think what remains is to so a somewhat exhaustive search over all pairs of wires driving them in opposition, to see what happens when they’re activated. static const int A=2, B=3; void setup(){ pinMode(B, OUTPUT); pinMode(A, OUTPUT); } void loop() { digitalWrite(A, 1); digitalWrite(B, 0); delay(1); digitalWrite(A, 0); digitalWrite(B, 1); delay(1); } That only took a few minutes: it’s only actually 28 combinations. Here are the results I’ve noted down, along with the matrix that’s implied by the numbers. I’ve written the numbers as pairs indicating the two pins that have been activated: Oh actually! It’s even better! This proves that not only is 10v pk-pk sufficient to drive the segments (I was sure of this), but 5v pk-pk isn’t, which I wasn’t so sure about. That’s nice: no extra circuitry is required. So, what we have is a 4×4 matrix. What I’m going to do is drive each of the rows in turn, while driving all 4 columns simultaneously. The mapping is very regular though and we actually have essentially two 4×2 matrices for the two digits. The plan: each digit will be represented by a 7 bit number, one bit for each segment. Then, a 4×2 matrix will be represented as 4 2 bit numbers. The next step is a little tedious and involves designing the segment pattern for each digit and figuring out the mapping of segments to columns for each row. I’ve gone for A-Z (excluding k,m,v,w,x), picking the lowercase letter in general when there’s no other criteria and of course 0-9: And that’s most of it. All that remains is to write a program which drives the rows in sequence and sets the correct column pattern for each row. Also, I’m going to have a function that maps ASCII to the segment patterns so I can write stuff on the display. My choice of driving is to drive each row low in turn, then repeat with each row high in turn to make it AC. I did try row 1 high then low, then row 2 high then low etc too but it didn’t make much difference. Here’s he code: void setup(){}; //Decode uppercase ASCII and numbers into //a list of segments, stored as a bit pattern. uint8_t ascii_to_code(uint8_t c) { static const uint8_t letters[26] = { 0x77,0x7c,0x39,0x5e, 0x79,0x71,0x6f,0x74, 0x06,0x0e,0x00,0x38, 0x00,0x54,0x5c,0x73, 0x67,0x50,0x6d,0x78, 0x1c,0x00,0x00,0x00, 0x6e,0x5b }; static const uint8_t numbers[10]={ 0x3f,0x06,0x56,0x4f, 0x66,0x6d,0x79,0x07, 0x7f,0x67 }; if(c >= 65 && c = 48 && c >4 | (n&64)>>6; } uint8_t r3(uint8_t n){ return (n&16)>>3 | (n&4)>>2; } uint8_t r2(uint8_t n){ return (n&8)>>2; } //Set the column outputs as driven or inactive //according to the input bit pattern. Polarity //determins whether to drive high or low. void set_columns(uint8_t n, bool polarity) { for(uint8_t i=0; i < 4; i++) if(n&(1<<i)) { pinMode(i+6, OUTPUT); digitalWrite(i+6, polarity); } else pinMode(i+6, INPUT); } void display_digit(uint8_t left, uint8_t right) { //Columns entries for both digits for //the 4 rows. uint8_t rows[4]; rows[3] = r5(left)<<2 | r5(right); rows[2] = r4(left)<<2 | r4(right); rows[1] = r3(left)<<2 | r3(right); rows[0] = r2(left)<<2 | r2(right); //Do positive columns/negative rows first //then repeat with the polarity inverted //Activate each row in turn and set the //column entries for that row. for(int p=0; p 100){ p++; q=0; } } There was a lot of fiddling around, most of which did very little. Basically, driving a multiplexed display off 3 levels is pretty marginal. I found that often some digits would ghost on, and others would be very faint. I could increase the contrast by lengthening the amount of time it took to draw the whole display, by driving a row with many cycles then moving on to the next row, but it only got really good when it was far far too slow. I did find putting a slight pause in between driving rows did help. Removing it darkened everything including ghosted on digits, lengthening it lightened everything. The value I got was about the best one I could find. Here's what it looks like: It's not perfect, you can see the contrast is a bit poor and so "hELLo" comes out looking a bit like "hECCo". Nonetheless I'm moderately pleased, since it does kind of work. I have to be careful with the choice of symbol though because they're not all that good. # Small hack of the day Two things: 1. syntax highlighting is nice 2. wouldn’t it be nice to copy /paste it The main problem is many terminals don’t for no especially good reason allow copy/paste of formatted text. There are three tools which help: 1. xclip -t text/html this eats standard input and allows pasting of it as HTML, so it can include formatting and color and so on. By default, xclip does plain text, so you have to specify that it’s HTML. 2. aha this takes ANSI formatted text (i.e. the formatting scheme used by terminals and turns it into HTML). 3. unbuffer many tools will only write color output to a terminal, not a pipe. Through the magic of PTYs (pseudoterminals) this program fools other programs into thinking they’re running on a terminal when they’re not. Now all you have to do is pipe them together. So, take a silly C++ program like this: void silly; And compile it like this: unbuffer g++-7 error.cc -std=c++1z | aha | xclip -t text/html Then a simple paste into a box which accepts HTML (e.g. email, etc) gives: error.cc:2:6: error: variable or field 'silly' declared void void silly; ^~~~~ # Server Send Events Server send events are a way of getting data pushed over an http connection to a browser. The JavaScript interface is very simple. Here’s some useful info: I thought it would be a neat way of debugging some GIS code in C++: all I had to do was write the data to a socket, and have a page that collected it in JavaScript and plugged it into Google maps. It was that simple, sort of, except that it was actually incredibly awkward getting it up and running since web browsers are both finicky and don’t provide much error information. All you have to do is open a socket, write the right HTTP headers and send the correct data. I eventually ended up sending it in chunked encoding, which means each message is essentially preceded by a length so the browser knows how much data to accept and put together into a message. The alternative is to use Content-Length and have fixed length messages (like the w3schools example), but I couldn’t manage to get my browser (Firefox) to accept more than one message before closing the connection due to an error. No idea why, but the chunked encoding is much more flexible anyway. Probably the biggest hurdle is that my HTLM page was just a file, but the server send events were from a (local) server. Either way that meant it was cross domain and so Firefox would block the request because of CORS. Turns out the fix was a simple headre tweak but that had me going for a while! Anyway, here’s the code: #include <iostream> #include <cstring> #include <cerrno> #include <vector> #include <iomanip> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> using namespace std; class VeryBasicServer { private: int sockfd=-1; int connection_fd{-1}; public: VeryBasicServer() { //Incantation to create a socket //Address famile: AF_INET (that is internet, aka IPv4) //Type is a reliable 2 way stream, aka TCP //Note this just creates a socket handle, it doesn't do anything yet. //The 0 is the socket type which is pretty redundant for SOCK_STREAM (only one option) sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) throw runtime_error("Error opening socket:"s + strerror(errno)); //Set options at the socket level (a socket has a stack of levels, and //all of them have options). This one allows reuse of the address, so that //if the program crashes, we don't have to wait for the OS to reclaim it, before //we can use this socket again. Useful for debugging! int true_ = 1; setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&true_,sizeof(int)); //Binding a socket is what sets the port number for a particular //socket. sockaddr_in serv_addr = {}; serv_addr.sin_family = AF_INET; //internet address family serv_addr.sin_addr.s_addr = INADDR_ANY; //allow connections from any address serv_addr.sin_port = htons(6502); //Still fighting the 80s CPU wars. 6502 > 8080 if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) throw runtime_error("Error binding socket:"s + strerror(errno)); //This is necessary for stream sockets (e.g. TCP). When a client_ connects //a new socket will be created just for that connection on a high port. listen(sockfd,5); } void read_ignore_and_dump_incoming_data() { vector<char> buf(65536, 0); int n = read(connection_fd,buf.data(),buf.size()); if (n < 0) throw runtime_error("Error reading from socket:"s + strerror(errno)); cout << "Message reads: "; cout.write(buf.data(), n); } void accept() { //Listen blocks until a cnnection is made, then hands over the newly created //socket for the connection connection_fd = ::accept(sockfd, nullptr, nullptr); if (connection_fd < 0) throw runtime_error("Error accepting:"s + strerror(errno)); //We can actually ignore the HTTP header just to get up //and running. For ServerSendEvents, there's only one accepted //MIME type, and uncompressed is always a valid compression option //even if the client_ doesn't request it. read_ignore_and_dump_incoming_data(); //Construct a valid working header. // //Two important things to note. One is the access control. Since //this server isn't serving anything except SSEs, the web page //which is using them ust come from elsewhere. Unless we allow //connections from places other than the originating server, then //the web browser will block the attempt for security. // //The other point is the chunked encodeing. The browser connecting //has to know when we've finished sending an event in the stream //of data. Chunked encoding allows us to send data blocks along with a //length so the server knows when a block is finished. The other option //is to have a fixed Content-Length instead. I never got it working, //but it's much less flexible so I didn't try hard. // //Note also the \r\n line terminations, not just \n. Part of the HTTP spec. write("HTTP/1.1 200 OK\r\n" //Standard HTTP header line "Content-Type: text/event-stream\r\n" //This is the only allowed MIME type for SSE "Transfer-Encoding: chunked\r\n" //Chunked encoding lets it know when an event is done without knowing packet boundaries. "Access-Control-Allow-Origin: *\r\n" //Because the HTML comes from a file, not this server, we have to allow access "\r\n"); //End of header indicator } //Write data with less than the minimal amount of error checking. void write(const string& str) { int n = ::write(connection_fd, str.data(), str.size()); if(n < 0) throw runtime_error("Error writing:"s + strerror(errno)); } void write_chunk(const string& str) { //Chunked encoding is the size in hex followed by the data. Note that both //the size and data fields are terminated by HTTP line terminations (\r\n) ostringstream o; o << hex << str.size() << "\r\n" << str << "\r\n"; write(o.str()); } ~VeryBasicServer() { cerr << "closing\n"; close(connection_fd); close(sockfd); } }; int main() { VeryBasicServer hax; hax.accept(); cout << "Press enter to send an event" << endl; for(int i=1;;i++) { if(cin.get() == EOF) break; //Build up an event in server-send-event format. The message consists of //one or more fields of the form: //field: value\n //Followed by an empty line. // //The main tag is "data:" which carries the data payload. //See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events //for more info (e.g. different message types and dispatch) string sse; sse += "data: " + to_string(i) + "\n"; //Send the current number as the data value sse += "\n"; //Empty field ends the message. //Send the message data using chunked encoding hax.write_chunk(sse); } } And the corresponding HTML page is: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <h1>Getting server updates</h1> <div id="result"></div> <script> if(typeof(EventSource) !== "undefined") { var source = new EventSource("http://127.0.0.1:6502"); source.onmessage = function(event) { document.getElementById("result").innerHTML += "Data: " + event.data + "<br>"; }; source.onerror = function(event) { document.getElementById("result").innerHTML += "Connection failed<br>"; }; } else { document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events..."; } </script> </body> </html> And that’s basically it. It’s a handy and rather silly chunk you can put in a C++ file. It’s a lot of very poor practices and does all the wrong things for scalability, security, generality and good sense, but it’s handy for debugging hacks. Code is on github. # Blog as you go: sigma delta DAC I have some piezo speakers from another project. That one used a bi-level amp to drive them. I figured it would be fun to try a tri-level drive, using an H bridge allows you to have +, – and 0V across the device. And for fun, why not make it a direct sigma delta encoder? It’s going to run on a microcontroller (an arduino). It’ll need very precise timings, so I’ll not be using the arduino environment. Here’s a first pass in C++ for linux: #include <iostream> #include <cmath> #include <utility> using namespace std; float signal(int t) { //Roughly 0-1 return (1 + sin(t/20. - 1))/2.1; } float quantize(float f, int levels) { return min(levels-1.f, max(0.f, floor(f*levels)))/(levels-1); } int main() { float integral = 0; for(int i=0; i < 200; i++) { float output = quantize(integral, 3); float difference = signal(i) - output; integral += difference; cout << signal(i) << " " << output << endl; } } And this produces output like this (mildly prettified): Which looks about right. The code will pretty much stink on an arduino since it’s all floating point. It’s easy to convert to integer code though: #include <iostream> #include <cmath> #include <utility> using namespace std; uint16_t signal(int t) { //Roughly 0-1 return 65535 * (1 + sin(t/20. - 1))/2.1; } uint16_t quantize(int32_t i) { if(i < 21845) return 0; else if (i < 43690) return 32768; else return 65535; } int main() { int32_t integral = 0; for(int i=0; i < 200; i++) { uint16_t output = quantize(integral); float difference = signal(i) - output; integral += difference; cout << signal(i) << " " << integral << " " << output << endl; } } I’ve used a uint16_t for the value, which will effectively represent both positive and negative levels with 32768 being the zero point. Note that the error integral must be both signed and wider since the errors can grow beyond the signal range: Now to port to the arduino. So, I’ll get my Makefile from here. I’m going to pick pins 8 and 9 on the UNO, that is PB0,1 on the chip for my outputs. and here’s how you get 3 way opposed outputs as +/-/0. To demo, I connected a pair of LEDs in parallel but facing the other way: #include <avr/io.h> #include <util/delay.h> int main() { int i=0; while(1) { if(i==0) { DDRB = 3; PORTB = 1; } else if(i==1) { DDRB = 3; PORTB = 2; } else { DDRB=0; PORTB = 0; } i++; if(i > 2) i=0; _delay_ms(200); } } So I started the port and BOOM! 😦 It stopped working. The reason was simple: the simple makefile takes one object file and converts it to HEX. Since I’m using sin(), we actually need to link the .o and .a into a .elf file, then convert THAT to HEX. The snippet is: %.hex: %.elf avr-objcopy -j .text -j .data -O ihex$< $@ prog.elf: delta_sigma.o avr-gcc$(FLAGS) -o prog.elf delta_sigma.o -lm

Obvious, really, in hindsight…

So, OK, now to convert the modulator code to the arduino. Lots of things went wrong. But first, here’s the code:

#include <math.h>
#include <stdint.h>
#include <avr/io.h>

uint16_t signal(int32_t t)
{
float u = t / 1024.f;

//Roughly 0-1
return 65535 * (1 + sin(2*3.151592f*u))/2.1;

}

uint16_t quantize(int32_t i)
{
if(i < 21845)
return 0;
else if (i < 43690)
return 32768;
else
return 65535;
}

int main()
{

int32_t integral = 0;

DDRB|=32;

for(uint32_t i=0; ; i++)
{
uint16_t output = quantize(integral);
int32_t difference = (int32_t)signal(i) - output;
integral += difference;

if(output == 0)
{
DDRB=255;
PORTB=1; //Output 1 0
}
else if(output == 65535)
{
DDRB =255;
PORTB=2; //Output 0 1
}
else
{
DDRB=255;
PORTB=0; //Output 0 0
}
}
}

What didn’t go wrong? Nothing! I wasn’t nearly careful enough with my ints (only 16 bits on AVR), ints of specific width, overflow and that sort of thing. Also, initially, I decided to output a 0 level by tri-stating the two outputs, so they both float to the middleish. Turns out that didn’t work well since they float extremely slowly (not surprising really!). Forcing them both down to 0 worked much better.

After all that, I then connected a simple RC filter across it so you an see the results:

That’s actually a pretty nice sine wave there! It ought to be: there’s really not much room for nonlinearity and other distortions to creep in. I’ve zoomed in a few levels so you can see how it looks in detail.

It is however really really slow. I’m using full floating point, and a transcendental operation every iteration of the sigma delta encoder. That is really slowing down the cycle time since the AVR isn’t very fast. That accidentally solves the other problem which I’ve made no attempt to make sure every path takes the same number of cycles. But that sin() is dominating so heavily that it doesn’t matter.

And that’s it for part 1: a working sigma delta encoder. For part 2, I’ll make it fast enough to generate audio tones which aren’t simply the sigma-delta encoder transitions (I hope).