Price Scanner

From Egbert's Wiki

Introduction

To circumvent the time consuming process of pricing each and every product in a store, an auutonomous scanner in the shop would enable potentiial customers to check the price themselves at a central point in the shop. These scanners are available from specialized shops butare rather expensive (400+ Euro) and need to be integrated in the shop back office. Some use a (reverse) telnet session to a server, some can read special formatted webpages. With the currently available credit card sized Linux computers (Raspberry Pi etc.) it is possible to build a sophisticated price scanner yourself.

Requirements

As said, the core is a Raspberry Pi B+ running Linux. To do the scanning a (laser) barcode scanner is needed. A fixed or table top model is preferred over a hand held scanner. When switched on, the scanner must connect to a (web)server to obtain the data. The scans must be presented to the customer on a display. This does not need to be more than a 4 x 20 character LCD display. Connectivity via LAN or WIFI (or both) is mandatory until the barcode data is downloaded. After that no network is needed anymore.

Design Details

  • The Raspberry Pi runs Raspbian Linux, a debian clone specially crafted for the RPi. The RPi is often programmed in Python, a scripting language with lots of possibilities. It is different from C, which needs a compiler to make executable programs or PHP, which is more a web scripting language. The price scanner program is fully written in Python.
  • Ethernet or WiFi can be used as network connection.
  • The barcode scanner is a fixed or handheld model. Used was model is FARSUN FG-9000 for the autosensing fixed scanner. The device I received was labeled ACON. It is an USB HID device, meaning that id is recognized as a keyboard. When connected to the Raspberry it shows up as /dev/hidraw0. Worth mentioning is that the scan is NOT transmitted as a string of characters but as a sequence of keystroke codes! Communication can be done via the device but also with the event driven interface.
  • As display was a 4x20 LCD module choosen also from China. This is an industry standard display with I2C (TWI) interface. There are several Python libraries for these displays. The are available in white on blue or yellow on green, with or without backlight.
  • Network connectivity by wired LAN or WIFI (Edimax USB antenna ED-7612UAN)
  • As power supply a standard 5V 2A power adapter can be used. The Raspberry has a micro USB connector as power connector; most of the time these power adapters are already equipped wth a micro USB connector.
  • The program is stored on a micro SDcard. All sizes up to 32 Gb are useable. Look for a Class 10 card; a class 4 is very slow in both programming and makes the Raspberry slow too.

Cost estimate

An indication of total cost of the price scanner:

Raspberry PI B+ 40 Euro
4 Gb SDcard 10 Euro
display LCD 4x20 15 Euro
scanner 20 - 55 Euro
power adaptor 10 Euro
WIFI interface 15 Euro
housing 15 Euro
labour 4 hours 140 Euro
TOTAL 300 Euro

Impression

Scanner2.jpg Scanner3.jpg Scanner4.jpg


Scanner1.jpg Scanner5.jpg

Preparation of the RPi

The first thing to do is to create the SDcard with Raspbian Linux. Use the latest Raspbian Stretch Lite image on raspberrypi.org. This makes use of Device Tree and a 4.18.x kernel. Use Win32DiskImager (Windows) or any other tool (Balena-Etcher) to write the image to the SDcard. After booting from this image several things need to be done. Having an internet connection is essential. Then do some initial configuration with raspi-config. Check the timezone (EU/AMS) and enable serial/ssh/i2c. Set boot to console (runlevel 3). Set the password, expand the fs, hostname and wifi ID and passphrase (optional).

raspi-config
reboot
apt-get update
apr-get upgrade -y
reboot
rpi-update (update kernel to 4.19+)
reboot

Then install vim to make life easy:

apt-get install vim

If WiFi is going to be used, edit /etc/wpa_supplicant/wpa_supplicant.conf and add, if not done before:

network = {
          ssid="your ssid"
          psk="your key"
          }

There are some python specific libraries needed by the scanner applicaton, install pip for both python2 and python3 and build the libraries:

apt-get install python-pip python3-pip
pip install evdev
pip3 install evdev
pip install smbus
pip3 install smbus

Installing the application init.d

To have an autostarting application we need some more to do. When the files are not yet on the SDcard, use ftp (FileZilla) to copy them to the RPi. Once inplace (subdirectories /home/pi/speldorado and /home/pi/bin), move scanner to the right location and set the permissions:

login as 'pi'
sudo -s
cp bin/scanner /etc/init.d/scanner
chmod 755 /etc/init.d/scanner
update-rc.d scanner defaults
chmod 755 bin/auto_run.sh

Below is the intermediate script /home/pi/bin/auto_run.sh:

#!/bin/bash
# Script to start our application
python /home/pi/speldorado/scanner.py &

If there is a prepared wpa-supplicant.conf file available, copy that to /etc/wpa-supplicant

Installing the application in systemd

This is now the preferred way. Don't forget to disable/remove the init.d scripts if those were used before! The application now also runs as python3 script.

cd /home/pi/bin
chmod 755 scanner3*.sh
cp scanner3.service to /etc/systemd/system
chmod 644 /etc/systemd/system/scanner3.service
systemctl enable scanner3.service
systemctl start scanner3
Result:
root@raspberrypi00:/home/pi/bin# systemctl status scanner3
● scanner3.service - Barcode Scanner service
   Loaded: loaded (/etc/systemd/system/scanner3.service; enabled; vendor preset:
   Active: active (running) since Sun 2017-12-24 14:18:33 CET; 2min 2s ago
 Main PID: 403 (scanner3.sh)
   CGroup: /system.slice/scanner3.service
           ├─403 /bin/bash /home/pi/bin/scanner3.sh
           └─409 python3 /home/pi/speldorado/scanner3.py

Dec 24 14:18:33 raspberrypi00 systemd[1]: Started Barcode Scanner service.

systemctl stop scanner3

Simple shell script to start the application:

root@raspberrypi00:/home/pi/bin# cat scanner3.sh
#!/bin/bash
python3 /home/pi/speldorado/scanner3.py

Systemd service description:

root@raspberrypi00:/home/pi/bin# cat scanner3.service
[Unit]
Description=Barcode Scanner service
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
ExecStart=/home/pi/bin/scanner3.sh
ExecStop=/home/pi/bin/scanner3-off.sh
[Install]
WantedBy=multi-user.target

Daily Cron

The daily cron runs at a time that scanner is powered down. To get normal logrotate behaviour, change the hour (6) to 17 in the system wide crontab file /etc/crontab on the line with the daily run.

Application software description

The program is written in Python. It is started as a service via /etc/init.d. As such it can be stopped and started by an interactive user.

  • service scanner stop
  • service scanner start

The program itself falls apart in 4 parts:

  • initialization /display IP adresses obtained via DHCP
  • get the file with price data from the webserver
  • convert this file to an internal table (dictionary)
  • wait for scanner activity
  • lookup barcode in dictionary and display on LCD
  • go back waiting for scanner activity

The LCD library

The LCD library was adapted to the right i2c slave address. Centering the output string was removed. File: /home/pi/speldorado/lcd_display.py.

import i2c_lib
from time import sleep

# LCD Address
ADDRESS = 0x27

# commands
LCD_CLEARDISPLAY = 0x01
LCD_RETURNHOME = 0x02
LCD_ENTRYMODESET = 0x04
LCD_DISPLAYCONTROL = 0x08
LCD_CURSORSHIFT = 0x10
LCD_FUNCTIONSET = 0x20
LCD_SETCGRAMADDR = 0x40
LCD_SETDDRAMADDR = 0x80

# flags for display entry mode
LCD_ENTRYRIGHT = 0x00
LCD_ENTRYLEFT = 0x02
LCD_ENTRYSHIFTINCREMENT = 0x01
LCD_ENTRYSHIFTDECREMENT = 0x00

# flags for display on/off control
LCD_DISPLAYON = 0x04
LCD_DISPLAYOFF = 0x00
LCD_CURSORON = 0x02
LCD_CURSOROFF = 0x00
LCD_BLINKON = 0x01
LCD_BLINKOFF = 0x00

# flags for display/cursor shift
LCD_DISPLAYMOVE = 0x08
LCD_CURSORMOVE = 0x00
LCD_MOVERIGHT = 0x04
LCD_MOVELEFT = 0x00

# flags for function set
LCD_8BITMODE = 0x10
LCD_4BITMODE = 0x00
LCD_2LINE = 0x08
LCD_1LINE = 0x00
LCD_5x10DOTS = 0x04
LCD_5x8DOTS = 0x00

# flags for backlight control
LCD_BACKLIGHT   = 0b00001000
LCD_NOBACKLIGHT = 0b00000000

En = 0b00000100 # Enable bit
Rw = 0b00000010 # Read/Write bit
Rs = 0b00000001 # Register select bit

class lcd:
  """
  Class to control the 16x2 I2C LCD display from sainsmart from the Raspberry Pi
  """

  def __init__(self):
    """Setup the display, turn on backlight and text display + ...?"""
    self.device = i2c_lib.i2c_device(ADDRESS,1)

    self.write(0x03)
    self.write(0x03)
    self.write(0x03)
    self.write(0x02)

    self.write(LCD_FUNCTIONSET | LCD_2LINE | LCD_5x8DOTS | LCD_4BITMODE)
    self.write(LCD_DISPLAYCONTROL | LCD_DISPLAYON)
    self.write(LCD_CLEARDISPLAY)
    self.write(LCD_ENTRYMODESET | LCD_ENTRYLEFT)
    sleep(0.2)

  def strobe(self, data):
    """clocks EN to latch command"""
    self.device.write_cmd(data | En | LCD_BACKLIGHT)
    sleep(0.0005)
    self.device.write_cmd((data & ~En) | LCD_BACKLIGHT)
    sleep(0.001)

  def write_four_bits(self, data):
    self.device.write_cmd(data | LCD_BACKLIGHT)
    self.strobe(data)

  def write(self, cmd, mode=0):
    """write a command to lcd"""
    self.write_four_bits(mode | (cmd & 0xF0))
    self.write_four_bits(mode | ((cmd << 4) & 0xF0))

  def display_string(self, string, line):
    """display a string on the given line of the display, 1,2,3 or 4, string is truncated to 20 chars and centred"""
    centered_string = string.ljust(20)
    if line == 1:
      self.write(0x80)
    if line == 2:
      self.write(0xC0)
    if line == 3:
      self.write(0x94)
    if line == 4:
      self.write(0xD4)

    for char in centered_string:
      self.write(ord(char), Rs)

  def clear(self):
    """clear lcd and set to home"""
    self.write(LCD_CLEARDISPLAY)
    self.write(LCD_RETURNHOME)

  def backlight_off(self):
    """turn off backlight, anything that calls write turns it on again"""
    self.device.write_cmd(LCD_NOBACKLIGHT)

  def backlight_on(self):
    """turn on backlight"""
    self.device.write_cmd(LCD_BACKLIGHT)

  def display_off(self):
    """turn off the text display"""
    self.write(LCD_DISPLAYCONTROL | LCD_DISPLAYOFF)

  def display_on(self):
    """turn on the text display"""
    self.write(LCD_DISPLAYCONTROL | LCD_DISPLAYON)

Low level library

The low level library that implements the reading and writing to the i2c hardware. See /home/pi/speldorado/i2c_lib.py.

import smbus
from time import *

class i2c_device:
  def __init__(self, addr, port=0):
    self.addr = addr
    self.bus = smbus.SMBus(port)

  # Write a single command
  def write_cmd(self, cmd):
    self.bus.write_byte(self.addr, cmd)
    sleep(0.0001)

  # Write a command and argument
  def write_cmd_arg(self, cmd, data):
    self.bus.write_byte_data(self.addr, cmd, data)
    sleep(0.0001)

  # Write a block of data
  def write_block_data(self, cmd, data):
    self.bus.write_block_data(self.addr, cmd, data)
    sleep(0.0001)

  # Read a single byte
  def read(self):
    return self.bus.read_byte(self.addr)

  # Read
  def read_data(self, cmd):
    return self.bus.read_byte_data(self.addr, cmd)

  # Read a block of data
  def read_block_data(self, cmd):
    return self.bus.read_block_data(self.addr, cmd)

The scan application

This is a sequence of modules that were tested on their own. The first step exercises the LCD display and shows the IP adresses of LAN and WIFI, if available. Next the file barcodes.csv is fetched from the website. This file is created together with the mergeN.csv file(s) but contains much more products than showed on the website. This is to cover the extra products possibly on stock in the shop. See file: /home/pi/speldorado/prijs_scanner.py.

# barcode scanner application
# python 3 version 
#
from lcd_display import lcd
from time import sleep
from evdev import InputDevice, categorize, ecodes
from urllib.request import urlopen
import csv
import sys
import socket
import fcntl
import struct

dev = InputDevice('/dev/input/event0')

# URL of CSV file to download
url = "http://www.speldorado.com/barcodes.csv"
#url = "http://speldorado.vandenbussche.nl/barcodes.csv"

def get_ip_address(ifname):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    return socket.inet_ntoa(fcntl.ioctl(
        s.fileno(),
        0x8915,  # SIOCGIFADDR
        struct.pack('256s', bytes(ifname[:15],'utf-8'))
    )[20:24])

my_lcd = lcd()
my_lcd.display_string("---- SPELDORADO ----", 1)
my_lcd.display_string("SPEELGOED - ELDORADO", 2)

sleep(3)
ipaddr1 = get_ip_address('eth0')
my_lcd.display_string("LAN:  " + ipaddr1, 3)

#ipaddr2 = get_ip_address('wlan0')
#my_lcd.display_string("WLAN: " + ipaddr2, 4)
sleep(3)

## part 2: get CSV from remote website

file_name = '/tmp/'+url.split('/')[-1]
u = urlopen(url)
meta = u.info()
f = open(file_name, 'wb')

file_size = int(meta.get("Content-Length"))
my_lcd.display_string("Downloading:",1)
my_lcd.display_string("File: %s" % (file_name[5:]),2)
my_lcd.display_string("Size: %7d" % (file_size),3)

file_size_dl = 0
block_sz = 8192
while True:
    buffer = u.read(block_sz)
    if not buffer:
        break

    file_size_dl += len(buffer)
    f.write(buffer)
    status = "Done: %7d [%3.0f%%]" % (file_size_dl, file_size_dl * 100. / file_size)
    my_lcd.display_string(status,4)

f.close()

## part 3: Convert CSV file to dict

# get list of tuples from CSV file
with open(file_name) as f:
    myData=[tuple(line) for line in csv.reader(f)]

# convert to Dict
myDict = {}
for tup in myData:
    key = tup[0] # use first field (barcode) as key
    value = tup[1:4] # fields 2,3,4
    if key not in myDict:
        myDict[key] = value

my_lcd.clear()
my_lcd.display_string('Klaar om te scannen.',2)

# part 4 Handle HID

# Provided as an example taken from my own keyboard attached to a Centos 6 box:
scancodes = {
    # Scancode: ASCIICode
    0: None, 1: u'ESC', 2: u'1', 3: u'2', 4: u'3', 5: u'4', 6: u'5', 7: u'6', 8: u'7', 9: u'8',
    10: u'9', 11: u'0', 12: u'-', 13: u'=', 14: u'BKSP', 15: u'TAB', 16: u'q', 17: u'w', 18: u'e', 19: u'r',
    20: u't', 21: u'y', 22: u'u', 23: u'i', 24: u'o', 25: u'p', 26: u'[', 27: u']', 28: u'CRLF', 29: u'LCTRL',
    30: u'a', 31: u's', 32: u'd', 33: u'f', 34: u'g', 35: u'h', 36: u'j', 37: u'k', 38: u'l', 39: u';',
    40: u'"', 41: u'`', 42: u'LSHFT', 43: u'\\', 44: u'z', 45: u'x', 46: u'c', 47: u'v', 48: u'b', 49: u'n',
    50: u'm', 51: u',', 52: u'.', 53: u'/', 54: u'RSHFT', 56: u'LALT', 100: u'RALT'
}

capscodes = {
    0: None, 1: u'ESC', 2: u'!', 3: u'@', 4: u'#', 5: u'$', 6: u'%', 7: u'^', 8: u'&', 9: u'*',
    10: u'(', 11: u')', 12: u'_', 13: u'+', 14: u'BKSP', 15: u'TAB', 16: u'Q', 17: u'W', 18: u'E', 19: u'R',
    20: u'T', 21: u'Y', 22: u'U', 23: u'I', 24: u'O', 25: u'P', 26: u'{', 27: u'}', 28: u'CRLF', 29: u'LCTRL',
    30: u'A', 31: u'S', 32: u'D', 33: u'F', 34: u'G', 35: u'H', 36: u'J', 37: u'K', 38: u'L', 39: u':',
    40: u'\'', 41: u'~', 42: u'LSHFT', 43: u'|', 44: u'Z', 45: u'X', 46: u'C', 47: u'V', 48: u'B', 49: u'N',
    50: u'M', 51: u'<', 52: u'>', 53: u'?', 54: u'RSHFT', 56: u'LALT', 100: u'RALT'
}

#setup vars
scan = ''
caps = False

#grab that shit
dev.grab()

#loop
for event in dev.read_loop():
    if event.type == ecodes.EV_KEY:
        data = categorize(event)    # Save the event temporarily to introspect it
        if data.scancode == 42:     # LSHIFT
            if data.keystate == 1:  #down
                caps = True
            if data.keystate == 0:  #up
                caps = False
        if data.keystate == 1:      # down events only
            if caps:
                key_lookup = u'{}'.format(capscodes.get(data.scancode)) or u'UNKNOWN:[{}]'.format(data.scancode)  # Lookup or return UNKNOWN:XX
            else:
                key_lookup = u'{}'.format(scancodes.get(data.scancode)) or u'UNKNOWN:[{}]'.format(data.scancode)  # Lookup or return UNKNOWN:XX
            if (data.scancode != 42) and (data.scancode != 28):
                scan += key_lookup   # add char to string
            if(data.scancode == 28): # if CRLF key (=enter), output string
                # lookup key and value in dict
                try:
                   tup = myDict[scan]
                except KeyError:
                   tup = ['Onbekende barcode!!!Vraag bij de kassa.','','']
                # prepare the 4 lines for display
                line1 = tup[0][:20] # first 20 characters
                line2 = tup[0][20:40] # second 20 characters
                line3 = tup[1] # normal price
                line4 = tup[2] # sale price

                my_lcd.clear()
                my_lcd.display_string(line1,1)
                my_lcd.display_string(line2,2)
                if line3 != '':
                   my_lcd.display_string("Adv.pr.:%7s Euro" % line3,3)
                if line4 != line3:
                   my_lcd.display_string("Nu voor:%7s Euro" % line4,4)

                # reset scan string to empty
                sleep(5)
                scan = ''
                my_lcd.clear()
                my_lcd.display_string('Klaar om te scannen.',2)
                #sleep(1)
                #my_lcd.backlight_off()
            # no CRLF
        # not a down event
    # not an EV_KEY event
# end for loop

Python3

There are some changes in the script needed to run under Python3.

root@raspberrypi00:/home/pi/speldorado# diff scanner.py scanner3.py
3c3
< from subprocess32 import Popen, PIPE
---
> from subprocess import Popen, PIPE
5c5
< import urllib2
---
> import urllib.request
40c40
< u = urllib2.urlopen(url)
---
> u = urllib.request.urlopen(url)

References

See also the site of Ge Janssen for a very good description on the steps needed to get i2c LCDs to work.

The latest raspi-gpio: git clone https://github.com/paulbarber/raspi-gpio.git.