Making money with Project Fugu

A presentation at DevFest Hamburg in November 2023 in Hamburg, Germany by Niels Leenheer

Slide 1

Slide 1

Making money with Project Fugu building a cash register with web technologies

Slide 2

Slide 2

Hi, I am Niels Leenheer.

Slide 3

Slide 3

I am the co-founder and CTO of Salonhub.

Slide 4

Slide 4

And we make software for hair salons. You know the whole point of sale system and appointment booking system. Just look how happy these people are using our system. Clearly we make great software.

Slide 5

Slide 5

So yeah… I don’t often talk about my job. I’ve done dozens of talks at conferences and often I don’t even mention my work at all. What I usually talk about is WebBluetooth and programming Lego cars with JavaScript. You know, fun stu . And nobody wants to hear a 30 minute talk about the exiting world of hair styling. But today is an exception. No fun stu today. Just a lot of frustration about 1980’s technology.

Slide 6

Slide 6

Earlier this year I got this email from Thomas at Google. I am a Google Developer Expert and I’ve spoken with him before about what we do at Salonhub and how I am building a completely browser based version of our cash register.

Slide 7

Slide 7

Well, it already is a web app, but what was missing was integrating it with all kinds of Point of Sale devices. You know, barcode scanners, receipt printers, cash drawer, payment terminals and customer displays and screens. We use Electron for that, but we want to move eventually to a PWA.

Slide 8

Slide 8

A couple of years ago that was simply not possible. But it is getting closer and closer. Thanks to Project Fugu and

Slide 9

Slide 9

And his question was… could we make a demo version of our product that shows o these API’s and… oh…

Slide 10

Slide 10

Could we do it in time for Google I/O Connect in Miami. In four weeks…

Slide 11

Slide 11

So… two weeks later…

Slide 12

Slide 12

We had a fully functioning demo that was completely browser based. Of course it wasn’t two weeks work. It was years in the making. And it gave me an opportunity to open source a lot of our code.

Slide 13

Slide 13

We could scan barcodes to add products. Items are shown on the customer pole. The receipt is visible on the display. And when we pay we can print a paper receipt.

Slide 14

Slide 14

So we went to Miami.

Slide 15

Slide 15

Slide 16

Slide 16

After that we went to Amsterdam.

Slide 17

Slide 17

Thomas took our demo to Bengalore and last week it was shown in I/O Connect Shanghai. All and all an amazing experience.

Slide 18

Slide 18

So, who wants to see it? And afterwards I’ll tell you how we created it.

Slide 19

Slide 19

So, yeah. That was a little bit underwhelming maybe. It is just a cash register. You scan a product, you pay and you’ll get a receipt. You can see that every day, in pretty much every shop or store. But it is more complicated than you might think. So how did we build it? Like I said it was years in the making. I’ve been playing with this in some way or form for the last 10 years.

Slide 20

Slide 20

Receipt printer Let’s start with receipt printers. This is an essential part of a cash register. Nowadays most are USB, but they still sell serial or even parallel port versions. And you’ve got Mobile versions using Bluetooth and even networked versions.

Slide 21

Slide 21

But the one we’re using is a USB device. So we’re going to use WebUSB. The API allows us to request a device with specific filters. The user then gets a list of all devices that they can choose from. And the browser then gets access to the device that is chosen.

Slide 22

Slide 22

Then we have to open the device. Claim the interface and transfer out our data to the endpoint.

Slide 23

Slide 23

Yeah, this may require some extra explanation.

Slide 24

Slide 24

USB devices have one or more interfaces. This usually depends on functionality.

Slide 25

Slide 25

Most have one interface, but if you have a device that combines multiple functions, like a scanner and printer combination, you would usually have one interface for the printer and one for the scanner.

Slide 26

Slide 26

And each interface has one or more endpoints. Endpoints can be use to send data, or receive data. So in case of a printer you have two endpoints, one for sending what you want to print and one for receiving for example status information about how much paper you have.

Slide 27

Slide 27

So we claim the right interface. And we claim it, so we can use the device exclusively and there are no other applications that can interfere while we use it. And then we send the data using the proper endpoint.

Slide 28

Slide 28

And now we can talk to our printer, but we still have no idea about what to tell it. We need to learn their language rst.

Slide 29

Slide 29

Let’s start really simple. Say we want to print this. Just two lines. One line in bold text. And then cut the receipt. Most printers have an build in cutter. If we were to do this with the web platform, this would be really, really simple.

Slide 30

Slide 30

Just npm install react. npm install tailwindcss. Think about what build system you want to use. Add some directives to your css le. Add some div’s to the root and apply the font-bold utility. Simple….

Slide 31

Slide 31

Okay, okay. Let’s use some proper HTML. And I am keeping it simple here. Just a couple of <br> and <b> tags. And that is it. But that is not how printers do it. The language printers use long predate HTML.

Slide 32

Slide 32

Let’s travel back to 1990. Epson is launching their first receipt printer. Nowadays all receipt printers use thermal paper, but back then it used dot matrix technology. And Epson was famous for creating the MX-80 dot matrix printer back in 1980.

So they took a shortcut and basically used the ESC/P language from that dot matrix printer and adapted it a bit and called it ESC/POS. And that is what receipt printers have been using ever since.

Slide 33

Slide 33

And the language is basically 7 bit ASCII. And with ASCII we have all the printable characters you would ever need. 26 of them. A to Z. And you get numbers. Some special characters. But that is not all, you also have lowercase version of A to Z… Whoooowww. What more do you need… It is the American Standards Code for Information Interchange after all.

Slide 34

Slide 34

And we have some control characters for data transmission, including LF or Line Feed. Or what we nowadays commonly call a newline.

Slide 35

Slide 35

So we can just send this to the printer and it will work. But this is not entirely what we wanted. It’s not bold…

Slide 36

Slide 36

So let’s fix the bold part. We’re not using tags, like HTML, but we send a special escape character that signals to the printer that what follows is a command.

Slide 37

Slide 37

ESC E-for-emphasis. And 1 to turn emphasis on. That makes the text bold. And ESC E 0 is to turn it o .

Slide 38

Slide 38

And there are many commands for all kinds of formatting and commands for inserting things like barcodes and images. Too many to list here and too many to go into details. There is a manual.

Slide 39

Slide 39

We need to send a command for cutting the paper. GS V 0. But we’re not done yet.

Slide 40

Slide 40

First we need to initialise our printer. We forgot to do that. And by initialising we make sure the printer is back to the default state and we don’t have the styles of the previous user of the printer interfering with what we want to do.

Slide 41

Slide 41

But what about these letters? We don’t have Unicode. Just ASCII… What about the Eszet? What about umlauts? What about the Aleph and the Omega. What about the Euro and the Pound. I would think these characters would be quite useful for receipts around these parts of the world. Not everybody lives in the US and uses dollars the only currency symbol in ASCII.

Slide 42

Slide 42

And even in the US they have IKEA…

Slide 43

Slide 43

While ASCII was acceptable, it wasn’t anymore in the 80s when computers were becoming more common across the whole globe. So we need to go on a tangent and talk about internationalisation before Unicode. Internationalisation in the 1980’s.

Slide 44

Slide 44

In fact, this could a whole talk in itself. Internationalisation is so complex that we’re not even going to scratch the surface. But that is okay. This is i18n in the 1980’s and the technology was still pretty limited. Instead of proper i18n, it is more like, how can we still sell printers and computers abroad, without people complaining too much and with the least amount of effort.

Slide 45

Slide 45

So ASCII is just 7 bits. That means just 128 different characters and we still have another 128 left over.

Slide 46

Slide 46

And for that we have codepages. We can swap out a di erent set of 128 characters that are in the upper range of an 8 bit byte.

Slide 47

Slide 47

It can be Western European, which contains the characters we want. Which is by the way exactly the reason why you should never test your software with that particular string.

Slide 48

Slide 48

Or Hebrew…

Slide 49

Slide 49

Or Greek…

Slide 50

Slide 50

Ukrainian, which has it’s own codepage, which is different from Russian, because Ukrainian uses number of different characters.

Slide 51

Slide 51

Or even Thai…

Slide 52

Slide 52

We have some many codepage. So….

Slide 53

Slide 53

Many codepages.

Slide 54

Slide 54

But we want codepage 858, which contains all the letters that have here.

Slide 55

Slide 55

To get the byte value that we need to send to the printer we need to take our unicode string, letter by letter convert the non-ascii letters to the values de ned in our codepage.

Luckily there is a library to do that. But unfortunately the codepages used by printers can be pretty obscure, so it only supports a fraction.

Slide 56

Slide 56

So I created a library called CodepageEncoder, specifically for receipt printers. And it even allows us to things like automatically choosing the right codepage and for the characters you want to print and switching on the y when needed.

Slide 57

Slide 57

Of course, not every printer supports every codepage.

Slide 58

Slide 58

Some support even less.

Slide 59

Slide 59

Some support more, but use a different identifier than what Epson does. And Epson created ESC/POS, so why…. Why!? It is really annoying.

Slide 60

Slide 60

But it is not just cheap Chinese printers that do that…

Slide 61

Slide 61

But what about images?

Slide 62

Slide 62

In our demo we are printing our logo on top of the receipt. How are we doing that? How are we going to send images to a completely text based printer?

Slide 63

Slide 63

Let’s take this image.

Slide 64

Slide 64

First we need to scale it to the right size.

Slide 65

Slide 65

And of course make it grayscale.

Slide 66

Slide 66

But not just grayscale. We need to turn it into pure black and white dots. And we can use a dithering algorithm to simulate grayscale with just black and white dots. Just a few dots and it is light grey. A lot of dots, it is dark grey. There are many different algorithms to do this. Some are bad. Some are okay, but in my opinion there is only one good one.

Slide 67

Slide 67

The Atkinson algorithm created by Bill Atkinson in the 80’s when he was working on some of the basic graphic API’s and apps on the original Mac. But I could not nd a library that did Atkinson dithering using vanilla javascript. There were node libraries, but I want it to work in the browser.

Slide 68

Slide 68

So yeah. I just had to write my own.

Slide 69

Slide 69

So we have our dithered image. We now need to split it into lines. Receipt printers have a 24 dot line height.

Slide 70

Slide 70

So we have these 3 different lines we need to print.

Slide 71

Slide 71

If we zoom in we can see the individual pixels.

Slide 72

Slide 72

So every column contains 24 pixels, that is 1

Slide 73

Slide 73

Slide 74

Slide 74

Slide 75

Slide 75

And that is what you do for every column until to read the end of the line.

Slide 76

Slide 76

So for every line we…

Slide 77

Slide 77

Get a whole bunch of 8-bit numbers or bytes. In fact 320 - the width - times 3 = 960 bytes. And we have three lines, so times 3. And for each line…

Slide 78

Slide 78

Get a whole bunch of 8-bit numbers or bytes. In fact 320 - the width - times 3 = 960 bytes. And we have three lines, so times 3. And for each line…

Slide 79

Slide 79

Get a whole bunch of 8-bit numbers or bytes. In fact 320 - the width - times 3 = 960 bytes. And we have three lines, so times 3. And for each line…

Slide 80

Slide 80

We turn on image mode… send the image data and a line feed to go to the next line. But there is extra space between each line. Well, I said there is a line height of 24? Well that is not true. The characters are 24 dots, but each line has an extra 10 dots of whitespace. For readability.

Slide 81

Slide 81

But this is not text. We don’t want that extra whitespace. So we need to change height of each line to 24 bits and afterwards set it back to 34 - 24 for the characters and 10 dots extra space between the lines.

Slide 82

Slide 82

And that is it. We have our image on a text printer.

Slide 83

Slide 83

Cash drawer But wait… what about cash drawers. As you can see with this model there is an integrated cash drawer. How do we open the cash drawer?

Slide 84

Slide 84

Well, all receipt printers have a DK port. You connect your cash drawer to that port and then you can tell the printer to send a pulse to that port and that activates the solenoid in the cash drawer and it opens. And with the combined printer cash drawer it works in the same way. So the cash drawer is not really a di erent device, it is a function of the receipt printer.

Slide 85

Slide 85

When I started this journey I really wished there was somebody who would that created a javascript library to make my life easier. And there were libraries, but mostly server side, badly written, php or python. Or JavaScript, but for Node. Not for the browser. So I created one.

Slide 86

Slide 86

And to get the receipt that we want we just need to do this. This is much easier and you can forget everything I just mentioned. Even the whole image dithering and encoding thing. This library supports that too.

Slide 87

Slide 87

But wait… it can’t be that simple.

Slide 88

Slide 88

No of course not. I mentioned all printers support ESC/POS, but yeah… I lied.

Slide 89

Slide 89

Introducing StarPRINT. Exactly the same as ESC/POS, except that everything is different. Great!

Slide 90

Slide 90

Instead ESC @ we now need to initialise…

Slide 91

Slide 91

with ESC @ CAN… because…. We can?

Slide 92

Slide 92

ESC t turns into ESC GS t….

Slide 93

Slide 93

And instead of ESC Emphasis On and O . You now just have ESC Emphasis and ESC F for ehh…. F this.

Slide 94

Slide 94

So if you have a Star printer, EscPosEncoder is completely useless. You need something specifically for StarPRNT. But there was nothing. I could not nd one single library for StarPRNT.

Slide 95

Slide 95

So I created a library called StarPrntEncoder….

Slide 96

Slide 96

But don’t use it… I’ll get back to that.

Slide 97

Slide 97

There were several points during this almost 5 year long journey that I just wanted to give up. There are no standards. And I haven’t even talked about different methods of image encoding. Barcode support that differs between even models from the same manufacturer. Printers that use 203 dpi, or 180 dpi. How many characters can’t on one line? Yes that can be 44, or 42. Or 35. Or 32. It depends. It’s a big mess.

Slide 98

Slide 98

This is how my office currently looks like. I’ve got more receipt printers than what is considered healthy. It’s starting to mess with me and and every shop or store that I visit I try to look for the model and make of their printer. Wondering if I should get one more.

Slide 99

Slide 99

If somebody says to you: “You don’t need another receipt printer. You already have more than enough receipt printers”. Just ignore them. You don’t need that kind of negativity in your life.

Slide 100

Slide 100

The result this madness is a library called ThermalPrinterEncoder that supports both ESC/POS and StarPRNT. It supports all the di erent resolutions and codepage mappings. And it’s backwards compatible with EscPosEncoder.

Slide 101

Slide 101

So if we want to print our original receipt. This is how we do it. And send the payload to the printer. Any receipt printer. StarPRNT or ESC/POS.

Slide 102

Slide 102

So if we want to print our original receipt. This is how we do it. And send the payload to the printer. Any receipt printer. StarPRNT or ESC/POS.

Slide 103

Slide 103

As a companion to ThermalPrinterEncoder, also I created a library that wraps WebUSB and allows you to simply connect to any supported printer and print. No need to worry about endpoints, interfaces.

Slide 104

Slide 104

And it is actually a set of libraries, with both WebSerial and WebBluetooth versions, that use the same API.

Slide 105

Slide 105

And that allows me to support all three protocols in our demo. The di erence is just which library is instantiated. So now we’re done with printers. We’re done with ESC/ POS and codepages. We can nally discuss some of the other devices.

Slide 106

Slide 106

Like this customer display. It is basically two lines of 20 vacuum fluorescent characters that we use to show the current item added to the receipt and the price, so that the customer can check if the cashier added the right item for the right price.

Slide 107

Slide 107

So the import question. Do we need to learn another obscure language from the 80s? What language do customer displays speak?

Slide 108

Slide 108

Ehh… yeah. They speak printer. The next question would obviously be…. Why?

Slide 109

Slide 109

And the reason is again found in the 1980s… Because computers back then usually only had one serial port. So if you had two devices, you needed share that port. And to make that sharing work, both need to work together and speak the same language.

Slide 110

Slide 110

So when you initialise your printer slash display, both are active. Everything you print is also shown on the display. But you can send ESC = to turn o the printer, or turn o the display. When you turn it o , it will ignore everything it receives, until the next initialisation where you can make another choice.

Slide 111

Slide 111

So WebSerial. We request a port.

Slide 112

Slide 112

We open the port… specify the right speed.

Slide 113

Slide 113

We get our writable stream for sending data to the device… and we write our payload to the stream, e ectively sending it to the printer.

Slide 114

Slide 114

And that is it. Well… we still have to deal with codepages, still need to deal with ESC/POS….

Slide 115

Slide 115

But wait… This isn’t right. Our display is connected using USB. Why is it using WebSerial? And not WebUSB? Because of the 1980s…

Slide 116

Slide 116

And to see why, we need to look at USB classes.

Slide 117

Slide 117

USB defines a number of default classes. If you have a device that uses these default classes you don’t need to install a driver. It just works. ADC for audio. Human Input Device or HID for keyboards and mice. Mass storage for hard drives. And many more. And we have CDC for communication, which includes ACM - which is a modern replacement for legacy serial port communication.

Slide 118

Slide 118

Some of these classes have lower level API’s. Like WebHID for HID over USB, but also for HID over Bluetooth. So WebHID is a low level API that spans multiple protocols, including USB. The same for WebSerial. WebSerial for legacy serial devices, Bluetooth based serial devices, but also the USB ACM class. And we have WebUSB for all other devices that do not t into these classes. So this is why we need WebSerial, even though it is USB, and not WebUSB.

Slide 119

Slide 119

But ACM is a replacement and not backwards compatible, so USB to serial port dongles don’t use ACM. In fact most USB serial devices don’t use ACM. They just took their old 1980s serial port device and added a custom Serial to USB chip from FTDI or Proli c with a custom driver.

Slide 120

Slide 120

And the driver creates a virtual serial port. And you use WebSerial for any type of serial port. Legacy, ACM or virtual. But… it is the custom class… so in theory you could write a WebUSB implementation… You’d be really crazy to do it. It would be completely useless… But you could.

Slide 121

Slide 121

So I did…

Slide 122

Slide 122

But don’t use it.

Slide 123

Slide 123

So in conclusion. Almost nobody uses actual legacy serial ports anymore. But for some USB connected devices you still need to use WebSerial.

Slide 124

Slide 124

So I created a library that uses WebSerial to connect to that display and show some simple text. It will handle the codepages and ESC/POS encoding for you.

Slide 125

Slide 125

Barcode scanner And that brings us to barcode scanners. And that is really easy. We do not need to do anything to support barcode scanners, because

Slide 126

Slide 126

Barcode scanners are just keyboards.

Slide 127

Slide 127

When you scan a barcode they just literally type out the numbers of the barcode. So you could use keyboard events. Of course you need to gure out what belongs to the barcode and where it starts and ends, but that could be solved. Well, maybe not perfect, but good enough… Next…

Slide 128

Slide 128

Oh, but a barcode scanner is a HID device… what if we could use WebHID to get the actual barcodes instead of the keystrokes.

Slide 129

Slide 129

We request the device.

Slide 130

Slide 130

And we actually get back a list of devices, because most HID devices are composite devices. This barcode scanner, also. It is a keyboard… like we just said. And a barcode scanner. So we need to figure out which devices is the barcode scanner part, and not the keyboard part.

Slide 131

Slide 131

Then we need to tell it to turn off the keyboard emulation and use plain HID reports instead.

Slide 132

Slide 132

And listen for the input reports.

Slide 133

Slide 133

And then we get this back.

Slide 134

Slide 134

And we just need to parse this and extract the value of the barcode.

Slide 135

Slide 135

But you don’t need to do any of that, because I have this library for you.

Slide 136

Slide 136

And if you have a serial based scanner, I also have a similar library that uses WebSerial instead.

Slide 137

Slide 137

That brings us to our final device. The customer screen. We use it to show a copy of our receipt. And this is probably the most modern and most web like API that we are discussing here today. Because the extra screen is just that. Just another screen.

Slide 138

Slide 138

Any screen will do. There is nothing special about it. You could use a special Point of Sale screen or a tv if you want. As long as you have two screens and you con gured it to extend your desktop. So you need to turn of screen mirroring.

Slide 139

Slide 139

Thanks to Project Fugu we now have the Window Management API which we can use to find out which screens are connected to your machine. You get a name and the size and position.

Slide 140

Slide 140

Which you can use to show something like this. It is basically a representation of the screens that are connected to your system. The salon then con gures which screen you want to use for the customer receipt.

Slide 141

Slide 141

And then you can make the current screen fullscreen on that display by specifying the new screen property when you call requestFullscreen.

Slide 142

Slide 142

So we have this. We call the openFullscreen function and then…

Slide 143

Slide 143

We get this. But that is not what you want. We want a preview of the receipt on that screen. Not a full screen version of the main cash register window. So we need something else.

Slide 144

Slide 144

Want we want is two completely independent browser windows. One on the main screen. And that opens a second browser window, full screen on the second monitor. Two html documents, rendered independently. So how do we do that?

Slide 145

Slide 145

We use window.open function. We set the location, width and height of the new window based on the information we got from getScreenDetails(). And we specify the new option “fullscreen” which opens a new full screen window on the screen of our choice. So it is a completely new separate browser window and we can use any web technology we want to render our receipt.

Slide 146

Slide 146

It just needs the right data to render.

Slide 147

Slide 147

Since these are two independent browser windows, we can’t simply call a render function from one on the other. We can’t postMessage data from one to the other. They are completely independent from each other. And we want to keep the data local, so connecting both to a WebSocket server to relay the data will work, but that is not what we want.

Slide 148

Slide 148

So we’re using broadcast channel for that. We open the same channel on the fullscreen preview window and the main register window. And because they are served from the same origin, we can send messages back and forth. So whenever the receipt is updated in the register, we posMessage the data to the channel…

Slide 149

Slide 149

And the preview listens for messages on the same channel and then renders the data independently.

Slide 150

Slide 150

And that is our final device. Still think that demo was underwhelming? The demo made it look easy. And that is great. It is supposed to be easy for the end user. But is still a huge e ort to get this working.

Slide 151

Slide 151

We’ve always used web technologies for our app. But we used wrappers around it for integrating hardware. About 5 years ago we transitioned away from Adobe AIR to Electron and I wish we could have moved to PWAs back then. But now all the puzzle pieces are in place. But this is what I love about Project Fugu. It really allows us to move towards a future where we don’t need Electron for our application.

Slide 152

Slide 152

But Project Fugu is not just about cash registers or hardware. It is about all kinds of API’s to make the web more capable and making sure that PWA’s can compete with native applications. Be it on mobile or on desktop. And for us that really works. And it might work for you too.

Slide 153

Slide 153

Thank you!