Skip to content

Instantly share code, notes, and snippets.

@Kopachris
Last active August 18, 2016 08:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kopachris/b8bb1de2cada4fdde88666e018167926 to your computer and use it in GitHub Desktop.
Save Kopachris/b8bb1de2cada4fdde88666e018167926 to your computer and use it in GitHub Desktop.
Reverse-engineering statistics commands for JCM bill validators

Introduction

I work as a slot machine technician at a casino. As part of our usual maintenance routines, we have a spreadsheet to keep track of bill validator acceptance/rejection rates for bills and barcode tickets. We run a program called Accload (part of JCM's tool suite, which can be found at http://slot-tech.com/interesting_stuff/jcm/JCM%20Apps/UBAToolSuiteStandardEditionVer106.zip), plug a USB cable into the front of the bill validator, and Accload displays the relevant data, which we then insert into the spreadsheet manually along with the bill validator's serial number and model type.

I decided I wanted to reverse engineer the communication between Accload and the bill validator so that I could automate the entry of these data. (And because I thought it would be fun.) For this project, I used two bill validators made by JCM: alternately a UBA-10 and an iVizion. Service manuals for both can be found at http://users.kopachr.is/~chris/files/jcm/

When the JCM tool suite is installed, both bill validators show up as USB COM ports in windows. To begin with, I logged serial communication using HHD's free version of their serial port monitor while using JCM's tool suite with those bill validators. I then attempted to mimic this communication using pyserial, but it seemed that the generic USB serial driver pyserial tried to use conflicted with the driver installed as part of the JCM toolsuite, and I wasn't able to open the COM port. On Linux (Fedora Workstation 24), the UBA showed up as /dev/ttyACM0 and I was able to open the serial port and mimic the logged communication using default port settings (9600 baud, 8 bits, no parity, 1 stop bit). The iVizion did not show up at first, but did show up as /dev/ttyUSB0 after echoing the vendor and product ID to the generic usb-serial driver's new_id endpoint: echo 2475 0105 > /sys/bus/usb-serial/drivers/generic/new_id

Basic protocol

When the JCM tool suite (before opening Accload) is connected to a bill validator, it polls for its status, boot ROM version, flash ROM status, serial number, firmware version, and firmware CRC. These messages appear to consist of 0xAA as the header for a request, followed by the total length byte, and then a byte representing which item is being requested. The response consists of 0xBB for the header, followed by the total length byte, the same byte indicating which item was requested, and then the data that was requested. These are example requests seen with the UBA. The basic protocol appears to be identical between the UBA and the iVizion.

  • 0xAA 0x03 0x00 - 0xBB 0x04 0x00 0x00
    • Appears to be a status request. 0x00 indicates status okay.
  • 0xAA 0x03 0x01 - 0xBB 0x05 0x01 0x07 0x00
    • Unknown
  • 0xAA 0x03 0x03 - 0xBB 0x05 0x03 0x02 0x01
    • Unknown. Possibly indicates command to initiate memory transfer? See below.
  • 0xAA 0x03 0x04 - 0xBB 0x08 0x04 "B03"
    • Boot ROM version as null-terminated string
  • 0xAA 0x03 0x05 - 0xBB 0x04 0x05 0x03
    • Unknown. Possibly flash ROM status because the rest of the commands seem to go in order that they're displayed in JCM tool suite?
  • 0xAA 0x03 0x06 - 0xBB 0x0f 0x06 "12345678901"
    • Serial number
  • 0xAA 0x03 0x07 - 0xBB 0x28 0x07 "U(USA)-10-SS ID003-03V208-31 18NOV11"
    • Version string (same as returned by the 0x88 request in the ID-003 protocol)
  • 0xAA 0x03 0x08 - 0xBB 0x05 0x08 0x04 0x1C
    • Firmware 16-bit CRC in little-endian format
  • 0xAA 0x03 0x09 - 0xBB 0x04 0x09 0x03
    • Unknown
  • 0xAA 0x04 0x02 0x01 - 0xBB 0x04 0x02 0x01
    • Ready for memory dump? This command appears to be required before sending commands requesting memory pages. The response to 0x03 is 0x02 0x01, perhaps this is variable among different models/markets.

The iVizion and the UBA use different commands to fetch statistics data. In both, however, it appears the meaning of the commands is less "give me statistics data" and more "dump a certain page of your nonvolatile memory." For the UBA, the command has a 0x10 header byte, followed by a length following byte (as opposed to the total length used in the basic protocol), then the bytes 0xD1 0x12, a byte indicating the page of memory to retrieve, and a simple checksum byte. The bill validator will respond similarly, attaching the data and the checksum at the end. For example, sending 0x10 0x04 0xD1 0x12 0x01 0xF8 will cause the bill validator to respond with 0x10 0x08 0xD1 0x12 0x01 0x00 0x02 0x00 0x01 0xFF.

For the iVizion, the command has a two-byte header 0x02 0x00 followed by a total length byte, then 0x71 0xD1 0x12 and the page to retrieve. The iVizion does not use a checksum. Example: sending 0x02 0x00 0x07 0x71 0xD1 0x12 0x01 will yield 0x02 0x00 0x0C 0x71 0xD1 0x12 0x01 0x03 0x00 0x02 0x00 0x32.

For both the UBA and the iVizion, pages start at 0x01, incrementing until the bill validator doesn't return any more data. There should be less than 256 pages in any case, with each page less than 256 bytes long.

Figuring out response format

I suggest that the "get statistics" commands are actually "dump memory" commands because it was immediately apparent that the data retrieved is not divided into sections logically, but by length instead. Some pages do appear to be self-contained sections (for example, iVizion page 0x06 only contains a record of the last four digits of the last ten barcodes read by the bill validator), but others run together (iVizion page 0x35 contains the last few bytes of one previous statistics record and the beginning of another). Furthermore, several pages at the end only contain null bytes. Therefore, to make the data easier to work with, I suggest concatenating all pages together and treating them as one memory-mapped file. As each item is a specific length and always appears in the same position relative to other items, particular items can be retrieved simply by pointing to the right address and reading.

After reading from both a UBA and an iVizion, I had Accload save the data to a file so that I could continue to analyze it without a bill validator connected. I opened the saved files in a hex editor and found that the layout was almost exactly the same as the serial data that I logged following the page number. When I took a closer look, the only differences appeared to be that the file was saved in little-endian format, while the data sent by the bill validator was all big-endian, and the saved file for the iVizion had some null bytes removed (73 bytes near the beginning).

To determine the memory structure, all I had to do was look for particular numbers, change them in the file, and reload the file in Accload to see if it was what I thought it was. After identifying the locations of certain key items, I checked another bill validator to confirm the addresses were the same.

Memory structure

For both UBA and iVizion, it appears that most or all integers are 16 bits, unsigned, with big endianness. Treating all the data received from the bill validator as one memory-mapped file, these are some example offsets where useful information can be found:

UBA

  • Offset 0x0084 - Currency assign table. Each denomination is essentially a two-byte float: one byte (signed?) for the exponent and one byte for the significand. Each denomination is followed by four null bytes or one null byte and three ASCII spaces (0x20), e.g. 0x01 0x05 0x00 0x20 0x20 0x20 in the US software is a $5 bill. The UBA supports 20 denominations. For unused denominations, the exponent will be 0x01 and the significand will be 0x00.
  • Offset 0x00FC - Total inserted notes. (Immediately follows currency assign table.)
  • Offset 0x00FE - Beginning of note acceptance data. Note acceptance data is broken down by direction the note was inserted (four directions) and by denomination. Certain markets may have multiple records for each denomination as different print series of notes are treated as separate denominations. The UBA can have up to 20 denominations assigned. Unused denominations are still present in the data as null bytes so as not to affect the positions of other items. In the latest US version of the UBA's firmware, there are 16 denominations used: $1 bills and three series each of $5, $10, $20, $50, and $100 bills. The UBA does not accept $2 bills. Each full record is 34 bytes and consists of the number of accepted notes of that denomination followed by the number of rejected notes for each of 16 reject reasons.
  • Offset 0x0BE2 - Start date for statistics in ddmmyyyy format, followed by M/C# without separator. M/C# is 20 characters, right-filled with spaces.
  • Offset 0x0C08 - Tickets accepted followed by tickets rejected for each of 32 reject reasons (16 with index mark, i.e. upside-down, and 16 without).

iVizion

  • Offset 0x0065 - Currency assign table. Like UBA, except following the first null byte is a byte indicating the banknote series, e.g. 0x01 0x0A 0x00 0x60 0x00 0x00 for the US software is $10, series 1996 (10 * 10^1, series 96). Unused denominations still use a null byte for the series, but denominations of unknown series use 0xFF.
  • Offset 0x01DB - Last four digits of the last ten barcodes read. ASCII, no separator.
  • Offset 0x0203 - Beginning of note acceptance data. Like UBA, except there are 50 denominations, plus tickets for each direction. Full record still 34 bytes (accepted plus rejects for 16 reject reasons). For tickets, the rejection record is elsewhere, and the remaining 32 bytes in this section are null.
  • Offset 0x1635 - Tickets rejected for each of 16 reject reasons. Not separated by direction, all in one 32-byte record.
  • Offset 0x1E23 - Start date for statistics in ddmmyyyy format followed by M/C# without a separator. Like UBA.

Example code

If you're using an iVizion bill validator, you'll need to bind the iVizion's vendor ID and product ID to the generic usb-serial driver before plugging it in. This can be done, for example, by executing echo 2475 0105 > /sys/bus/usb-serial/drivers/generic/new_id as root. You'll need to execute that command after every reboot if you choose to do it that way.

After plugging in the iVizion, it will show up as /dev/ttyUSB0 (or /dev/ttyUSB1 or 2 or 3 or whatever if you have multiple USB serial ports - the example script tries to use /dev/ttyUSB0). After plugging in a UBA, it should show up as /dev/ttyACM0.

Plug in the bill validator, using the front USB port on the bill validator. Run the example script and type in either "UBA" or "iVizion" according to which model you're using. The script will retrieve and print the acceptance/rejection rate for notes and tickets and save the raw data as bv.dat. Note that the saved file will not be compatible with Accload for either the UBA or the iVizion, as it will be saved in big-endian format just as is received from the bill validator.

The contents of this entire gist, plus the serial logs and Accload files I used for testing can be found at http://users.kopachr.is/~chris/files/jcm/stats.zip

Addendum: WBA

The WBA is kind of a special case. It doesn't have a front USB port, so all communications must go through the rear harness. The communication is TxD on pin 4 and RxD on pin 6. The WBA uses TTL-level opto-isolated serial. After shifting the levels (e.g. through a MAX232), any USB-serial adapter should work.

The WBA does not support the basic "AA/BB" protocol described above, but otherwise supports memory dumps exactly like the UBA, with the exception that the WBA uses even parity in its communications and requires a longer read timeout because of slower processing speed. Because the WBA has less memory than the UBA, the addresses of the items mentioned above are a bit different.

  • Offset 0x0084 - Currency assign table, exactly like UBA except the WBA only supports 16 denominations.
  • Offset 0x00E4 - Total notes, immediately following currency assign table just like UBA.
  • Offset 0x00E6 - Beginning of note acceptance data. Like UBA, except only 16 denominations.
  • Offset 0x09AA - Begin date and M/C#, like UBA. On every WBA I tested so far, the start date was ????????.
  • Offset 0x09D0 - Accepted tickets followed by rejected tickets for 32 reject reasons (16 each upside-down and right side up).
#!/usr/bin/env python3
"""
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2016 Christopher Koch <kopachris@gmail.com>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
"""
import serial
import struct
from time import sleep
from serial.serialutil import SerialException
models = {'UBA': '/dev/ttyACM0',
'iVizion': '/dev/ttyUSB0',
'WBA': '/dev/ttyUSB0',
# remember to `echo 2475 0105 > /sys/bus/usb-serial/drivers/generic/new_id`
# before plugging in iVizion for the first time after booting
# or otherwise bind that VID/PID to the generic usb-serial driver
}
def get_ushort(s, e='big'):
s = bytes(s)
if len(s) != 2:
raise ValueError("Must be bytes object of length 2")
if e == 'big':
return struct.unpack('>H', s)[0]
elif e == 'little':
return struct.unpack('<H', s)[0]
else:
raise ValueErorr("Endianness must be 'big' or 'little'")
def main():
bv_model = input("UBA, WBA, or iVizion? ")
# WBA DOES NOT WORK RIGHT NOW, USES DIFFERENT
# ADDRESSES FROM UBA
if bv_model == 'WBA':
rx_timeout = 1.0
parity = serial.PARITY_EVEN
elif bv_model in ('UBA', 'iVizion'):
rx_timeout = 0.02
parity = serial.PARITY_NONE
else:
raise ValueError("Unknown model")
while True:
try:
bv = serial.Serial(models[bv_model], timeout=rx_timeout, parity=parity)
break
except SerialException:
# Device or resource busy, wait a bit before trying again
sleep(0.1)
if bv_model != 'WBA':
responses = []
for x in [0, 1, 3, 4, 5, 6, 7, 8, 9]:
# get basic info
bv.write(b'\xaa\x03' + bytes([x]))
resp = bv.read(255)
responses.append(resp[3:])
print("Boot ROM version: ", responses[3][:-1].decode(errors='replace'))
print("Serial number: ", responses[5].decode(errors='replace'))
# with UBA sometimes get a UnicodeDecodeError on serial number (0xBB)
# and it attaches the serial to the firmware version instead? weird
print("Firmware version: ", responses[6].decode(errors='replace'))
print("Firmware CRC: ", responses[7][::-1].hex())
# initialize memory dump
bv.write(b'\xaa\x04\x02\x01')
response = bv.read(255)
if bv_model in ('UBA', 'WBA'):
dump_cmd = b'\x10\x04\xd1\x12'
checksum = True
elif bv_model == 'iVizion':
dump_cmd = b'\x02\x00\x07\x71\xd1\x12'
checksum = False
response = 1
memory = b''
page = 1
while response != b'' and page < 256:
cmd = dump_cmd + bytes([page])
i = len(cmd)
if checksum:
# UBA
cmd += bytes([sum(cmd) % 256])
bv.write(cmd)
if checksum:
# UBA, remove trailing checksum
# you could actually check it if you want
full_resp = bv.read(255)
response = full_resp[i:-1]
else:
# iVizion, no trailing checksum
response = bv.read(255)[i:]
memory += response
page += 1
# for debugging
with open('bv.dat', 'wb') as dat:
dat.write(memory)
rej_notes = 0
rej_tkts = 0
acc_notes = 0
acc_tkts = 0
if bv_model in ('UBA', 'WBA'):
# TODO wba addresses are different from uba
try:
addr = 0x00fe
for direc in range(4):
# 4 directions: FA FB BA BB
for denom in range(20):
# 20 denominations
acc_notes += get_ushort(memory[addr:addr+2])
addr += 2
for reason in range(16):
# 16 reject codes
rej_notes += get_ushort(memory[addr:addr+2])
addr += 2
# tickets not separated by direction
addr = 0x0c08
acc_tkts = get_ushort(memory[addr:addr+2])
addr += 2
for reason in range(16):
rej_tkts += get_ushort(memory[addr:addr+2])
addr += 2
except Exception as e:
print('Address: ', hex(addr))
print('acc_notes: ', acc_notes)
print('rej_notes: ', rej_notes)
print('acc_tkts: ', acc_tkts)
print('rej_tkts: ', rej_tkts)
raise e
elif bv_model == 'iVizion':
addr = 0x0203
for direc in range(4):
for denom in range(50):
acc_notes += get_ushort(memory[addr:addr+2])
addr += 2
for reason in range(16):
rej_notes += get_ushort(memory[addr:addr+2])
addr += 2
# tickets are counted for each direction
acc_tkts += get_ushort(memory[addr:addr+2])
addr += 2
for reason in range(16):
rej_tkts += get_ushort(memory[addr:addr+2])
addr += 2
print("Notes accepted: ", acc_notes)
print("Notes rejected: ", rej_notes)
print("Tickets accepted: ", acc_tkts)
print("Tickets rejected: ", rej_tkts)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment