Turn {ggplot2} into a PoS (point of sale) system

In late March, I caught a short-lived tweet that featured an Epson thermal receipt printer generating a new “ticket” every time a new GitHub issue was filed on a repository. @aschmelyun document it well in this blog post. This is a pretty cool, standalone hack on a Pi Zero.

Andrew’s project gave birth to an idea: could I write an R package that will allow me to plot {ggplot2}/{grid} objects on it? The receipt printer’s form factor is tiny (~280 “pixels” wide), but the near-infinite length of the paper means you can play around with some data visualizations that can’t be done in other computers. other formats (and it would be cool to be able to play with other content to print it in and out of R).

One of the features that makes Andrew’s hack even cooler is that he used a USB connected Epson receipt printer model. I don’t see the need to dedicate an extra piece of plastic, metal, and silicon to handle the printing experience, especially since I already have a big Linux server on which I’m running personal work from large-scale data science. I ended up using it (a lot of restaurants close every week) Epson TM-T88V off eBay as it has Ethernet and supports ESC/POS commands.

After unpacking it, I needed to have it on the local network. There are many guides for this, but this one sums up the process pretty well:

  • Plug in the printer and reset it
  • Connect a system directly to it (Ethernet to Ethernet)
  • Configure your system to use Epson’s default IP addressing scheme
  • Access the web configuration page
  • Configure it to work on your network
  • Disconnect and restart the printer

To make sure everything was working, I caught a the (strangely) many projects on GitHub that provided a way to format graphics files into an ESC/POS compatible raster bitmap and processed a simple png generated by R and then used netcat to branch the binary blob to the printer on the default port of 9100.

I did some initial experimentation with {magick}, extracting the graphical elements from the generated plots, then wrapping some R code around the conversion. It was clunky and tedious, and I knew there had to be a better way, so I looked for C/C++, Rust, or Go code that had already done the conversion and found png2escpos by the working group. (NOTE: I’m likely to switch to png2pos by Petr Kutalek because the dithering he does won’t require the R user to produce only black and white plots for them to look good.

I thought about implementing a graphics device to support any R graphics output, but there are enough methods to convert a basic R plot to a grid/grob object that I decided to mimic the functionality of ggsave() and make a ggpos() a function. The comment annotations in the code snippet below walk you through the extremely basic process:

ggpos <- function(plot = ggplot2::last_plot(),
                  host_pos,
                  port = 9100L,
                  scale = 2,
                  width = 280,
                  height = 280,
                  units = "px",
                  dpi = 144,
                  bg = "white",
                  ...) {

  # we generate a png file using ggsave()

  png_file <- tempfile(fileext = ".png")

  ggplot2::ggsave(
    filename = png_file,
    plot = plot,
    scale = scale,
    width = width,
    height = height,
    units = units,
    dpi = dpi,
    bg = bg,
    ...
  )

  # we call an internal C function to convert the generated png file to an ESC/POS raster bitmap file

  res <- png_to_raster(png_file)

  if (res != "") { # if the conversion ended up generating a new file

    # read in the raw bytes

    escpos_raster <- stringi::stri_read_raw(res)

    # open up a binary socket to the printer 

    socketConnection(
      host = host_pos,
      port = port,
      open = "a+b"
    ) -> con

    on.exit(close(con))

    # shunt all the bytes over to it

    writeBin(
      object = escpos_raster,
      con = con,
      useBytes = TRUE
    )

  }

  invisible(res)

}

The only work I had to do on the original C code was to output it directly to a file instead of stdout.

Now plotting to the printer is as easy as:

library(ggplot2)
library(ggpos)

ggplot(mtcars) +
  geom_point(
    aes(wt, mpg)
  ) +
  labs(
    title = "Test of {ggpos}"
  ) +
  theme_ipsum_es(grid="XY") +
  theme(
    panel.grid.major.x = element_line(color = "black"),
    panel.grid.major.y = element_line(color = "black")
  ) -> gg

ggpos(gg, host_pos = HOSTNAME_OR_IP_ADDRESS_OF_YOUR_PRINTER)

This code produces this output (I’m still tearing the wrapped paper from this thing):

Here’s it all in action:

One of the topics for 2022’s #30DayChartChallenge was “party to everything”, so I revamped my tree map entry into a very long storyline that would make CVS cashiers feel quite inferior.


You can find {ggpos} on on GitHub.

FIN

A big caveat to this is that these printers have a small buffer, so very long and complex plots won’t work out of the box. I had to break down my individual faceted heatmaps and derive them one by one.

I will be switching to the new C library soon and adding a little DSL to handle text formatting and printing from R (the device has 2 fonts and almost no styles). I even threatened to make a ShinyPOS app, but we’ll see how the motivation for that goes.

Toss the tires and let me know if you end up using the package (+ share your creation with the 🌎).

*** This is a syndicated blog from the Security Bloggers Network of rud.east written by hrbrmstr. Read the original post at: https://rud.is/b/2022/04/03/turning-ggplot2-into-a-pos-point-of-sale-system/

Comments are closed.