How to connect to a scanner with Golang on Linux

eye-catch Other techs

My host machine is Windows 10. I somehow need to implement to connect a scanner from Linux. I tried to transport the USB scanner device to WSL2 but it failed for my device. It seems that some devices are not supported.

The scanners that I tested are Zebra DS3678 and DS3608.

Sponsored links

Create a Linux machine in VirtualBox and configure USB settings

The alternative way of using WSL is to use a VM. It can pass the device info to the virtual machine. Create a Linux machine in VirtualBox and configure USB settings.

After the USB settings, the device should appear by lsusb command.

$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 023: ID 05e0:1300 Symbol Technologies Symbol Bar Code Scanner::EA
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

The scanner is available.

Check the following post if you haven’t installed the extensions for USB.

Sponsored links

Try to run the code

I prepared the code below written in Go lang.

package main

import (
    "bufio"
    "fmt"
    "os"

    "github.com/karalabe/hid"
)

func main() {
    vendorId := uint16(1504)
    productId := uint16(0)  
    devices := hid.Enumerate(vendorId, productId)
    for _, info := range devices {
        fmt.Printf("Path %s: VendorID %d - ProductId %d\n", info.Path, info.VendorID, info.ProductID)
    }
    device, err := devices[0].Open()
    if err != nil {
        fmt.Println("ERROR", err)
        os.Exit(1)
    }

    scanner := bufio.NewScanner(device)
    if scanner == nil {
        fmt.Println("scanner can't be instantiated.")
        os.Exit(1)
    }
    for scanner.Scan() {
        line := scanner.Text()
        fmt.Println(line)
    }
    device.Close()
}

It might fail for the first time. Actually, it failed to open the device in my environment.

$ /usr/local/go/bin/go run main.go 
Start
Path 0001:0017:00: VendorID 1504 - ProductId 4864
ERROR hidapi: failed to open device
exit status 1

Try to connect with Node.js

I tried to connect to the device on Windows with Node.js in order to confirm if it’s possible to connect without the specific driver.

const HID = require("node-hid");

const zebraScanner = HID.devices().find((x) => x.vendorId === 1504);
if (!zebraScanner) {
    console.log("Zebra scanner not found.");
}
console.log(zebraScanner)
const scanner = new HID.HID(zebraScanner.vendorId, zebraScanner.productId);

scanner.on("data", (data) => {
    console.log(`length: ${data.toString().length}`);
    console.log(data.toString());
});

This works on Windows but not on Linux because udev rule is not modified.

Modify udev rule and restart

I added the following line to the end of this file /etc/udev/rules.d/70-snap.snapd.rules.

KERNEL=="hidraw*", ATTRS{idVendor}=="05e0", ATTRS{idProduct}=="*", MODE="0666", GROUP="plugdev"

I’m not familiar with Linux but the file name might depend on the system. Then, restart the daemon to reflect the change.

$ sudo udevadm trigger
$ sudo udevadm control -R

This command restarts udev daemon.

Node.js application could connect to the device here.

It turned out that the scanner can be connected without the specific driver.

Run Go application again with sudo keyword

Go is not installed in admin.

$ sudo go run main.go
sudo: go: command not found

So it’s necessary to use an absolute path.

$ sudo /usr/local/go/bin/go run main.go 
Start
Path 0001:0017:00: VendorID 1504 - ProductId 4864
"       CN"
-----

It could connect to the device this time and the scanned code is displayed correctly with unnecessary spaces.

It can’t connect to the device without sudo. From my research, I thought it was because the user is not in the plugdev group specified in the udev rule.

$ groups vagrant
vagrant : vagrant sudo docker

I added the user to the group.

$ sudo usermod -a -G plugdev vagrant
$ groups vagrant
vagrant : vagrant sudo plugdev docker

But it can’t still connect.

Check the udev rule again if it is enough

The Readme on node-hid describes how to configure udev rule. I thought I used only hidraw, so I added only one line.

However, after I added the following 3 lines, Go application was able to connect to the device without sudo.

SUBSYSTEM=="input", GROUP="input", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="05e0", ATTRS{idProduct}=="*", MODE:="0666", GROUP="plugdev"
KERNEL=="hidraw*", ATTRS{idVendor}=="05e0", ATTRS{idProduct}=="*", MODE="0666", GROUP="plugdev"

Hmm… Node application could connect to the device even if I set HID.setDriverType("libusb"). It’s strange.

Scanned code is not displayed as expected

The default behavior of bufio.Scan split the string by end of line code. However, it doesn’t work as expected when scanning a barcode that contains end-of-line code. The string is displayed when scanning a barcode on the scanner’s manual.

To display all the scanned code, I defined the split function.

scanner := bufio.NewScanner(device)
if scanner == nil {
    fmt.Println("scanner can't be instantiatedd.")
    os.Exit(1)
}
split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
    if atEOF && len(data) == 0 {
        return 0, nil, nil
    }

    return len(data), data, nil
}
scanner.Split(split)

The split function always returns the scanned string when a code is scanned. Note that it returns only 64 bytes per scan. If you need longer bytes, you somehow need to change the code.

Unexpected “3” is in the scanned code

When I scanned a QR code by Zebra scanner DS3678, 3 is added to the end of the scanned code. When scanning a barcode, it also contains a character that can’t be displayed.

$ /usr/local/go/bin/go run main.go 
Start
Executor: vagrant
Path 0001:0006:00: VendorID 1504 - ProductId 5120

hey▒


hey3

I wrote the following code in JavaScript to check the received data.

const HID = require("node-hid");

const zebraScanner = HID.devices().find((x) => x.vendorId === 1504);
if (!zebraScanner) {
    console.log("Zebra scanner not found.");
    return;
}
console.log(zebraScanner)
const scanner = new HID.HID(zebraScanner.vendorId, zebraScanner.productId);

scanner.on("data", (data) => {

    let str = "";
    for (let i = 0; i < data.length; i++) {
        const control = table.get(data[i]);
        if (control) {
            str += control;
        } else {
            str += String.fromCharCode(data[i]);
        }
        str += " "
    }
    console.log(str);
});

// https://en.wikipedia.org/wiki/List_of_Unicode_characters
const table = new Map([
    [0, "NUL"],
    [1, "SOH"],
    [2, "STX"],
    [3, "ETX"],
    [4, "EOT"],
    [5, "ENQ"],
    [6, "ACK"],
    [7, "BEL"],
    [8, "BS"],
    [9, "HT"],
    [10, "LF"],
    [11, "VT"],
    [12, "FF"],
    [13, "CR"],
    [14, "SO"],
    [15, "SI"],
    [16, "DLE"],
    [17, "DC1"],
    [18, "DC2"],
    [19, "DC3"],
    [20, "DC4"],
    [21, "NAK"],
    [22, "SYN"],
    [23, "ETB"],
    [24, "CAN"],
    [25, "EM"],
    [26, "SUB"],
    [27, "ESC"],
    [28, "FS"],
    [29, "GS"],
    [30, "RS"],
    [31, "US"],
])

Then, the result is the following when scanning a QR code and barcode.

// QR Code
LF DLE ETX NUL h e y NUL 3 VT NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL 

// Barcode
LF DLE ETX NUL h e y NUL CAN VT NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL NUL 
  • LF: Line Feed
  • DLE: Data Link Escape (to indicate that the data includes control characters)
  • ETX: End of Text
  • VT: Vertical Tabulation
  • CAN: Cancel (There is an error on the preceding data. Ignore the data)

Hmm… I’m not sure whether this is Zebra specific or not but I guess so. I’ve used another scanner with Node.js application before but I didn’t have to work with the control characters. I guess those characters are basically handled by the driver called Zebra CoreScanner.

Anyway, it needs to be filtered.

Filtering the actual string

The actual string is in between two NUL control characters. Therefore, it just needs to find the indexes of the NULL character.

for scanner.Scan() {
    rawBytes := scanner.Bytes() // [10 16 3 0 104 101 121 0 24 11 0 0 0 0...]
    firstNullIndex := bytes.Index(rawBytes, []byte{0}) // 3
    restBytes := rawBytes[1+firstNullIndex:] // [104 101 121 0 51 11 0 0 0 0 0 0 ...]
    secondNullIndex := bytes.Index(restBytes, []byte{0}) // 3
    fmt.Println(restBytes[:secondNullIndex]) // [104 101 121]
    fmt.Println(string(restBytes[:secondNullIndex])) // hey
}

Note that some interfaces don’t work as expected because the format of the byte string is different. But I tested IBM Hand-held USB mode and it worked.

It’s better to move the filtering logic into the split function but this is just a test code. It’s ok.

Want to License problem

The license of this hid package https://github.com/karalabe/hid is the following.

Given the above, hid is licensed under GNU LGPL 2.1 or later on Linux and 3-clause BSD on other platforms.

https://github.com/karalabe/hid

This license is not allowed to use in my project. So I used a different package in the end. The following package is a replacement.

GitHub - zserge/hid: Simple HID driver for Go (pure golang, no dependencies, no cgo)
Simple HID driver for Go (pure golang, no dependencies, no cgo) - zserge/hid

Note that this supports only Linux. If you need the source code, check my GitHub repository.

play-with-go-lang/scanner/scanner1.go at main · yuto-yuto/play-with-go-lang
Playground for go. Contribute to yuto-yuto/play-with-go-lang development by creating an account on GitHub.

Overview

Use Linux VM if you are using Windows machine but need to use Linux Docker container.

Add the following 3 lines to udev rule /etc/udev/rules.d/something.rules

SUBSYSTEM=="input", GROUP="input", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="05e0", ATTRS{idProduct}=="*", MODE:="0666", GROUP="plugdev"
KERNEL=="hidraw*", ATTRS{idVendor}=="05e0", ATTRS{idProduct}=="*", MODE="0666", GROUP="plugdev"

Then restart the udev daemon.

$ sudo udevadm trigger
$ sudo udevadm control -R

Prepare the code like below.

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "os"
    "os/user"

    "github.com/karalabe/hid"
)

func main() {
    user, err := user.Current()
    if err != nil {
        fmt.Println(err.Error())
    }
    fmt.Printf("Executor: %s\n", user.Username)

    vendorId := uint16(1504) // vendorId needs to be adjusted to your brand
    productId := uint16(0)
    devices := hid.Enumerate(vendorId, productId)
    for _, info := range devices {
        fmt.Printf("Path %s: VendorID %d - ProductId %d\n", info.Path, info.VendorID, info.ProductID)
    }

    device, err := devices[0].Open()
    if err != nil {
        fmt.Println("ERROR", err)
        os.Exit(1)
    }

    scanner := bufio.NewScanner(device)
    if scanner == nil {
        fmt.Println("scanner can't be instantiatedd.")
        os.Exit(1)
    }
    split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        if atEOF && len(data) == 0 {
            return 0, nil, nil
        }

        return len(data), data, nil
    }
    scanner.Split(split)

    for scanner.Scan() {
        rawBytes := scanner.Bytes() // [10 16 3 0 104 101 121 0 24 11 0 0 0 0...]
        firstNullIndex := bytes.Index(rawBytes, []byte{0}) // 3
        restBytes := rawBytes[1+firstNullIndex:] // [104 101 121 0 51 11 0 0 0 0 0 0 ...]
        secondNullIndex := bytes.Index(restBytes, []byte{0}) // 3
        fmt.Println(restBytes[:secondNullIndex]) // [104 101 121]
        fmt.Println(string(restBytes[:secondNullIndex])) // hey
    }

    device.Close()
}

Run the Go application and play with a scanner. Scan function ends if the number of read bytes reaches the max buffer size. If you periodically need to scan it, you need to consider the way.

Comments

Copied title and URL