Price Scanner
Contents
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
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.