SAS¶
Slot Accounting System. The big one. Real SAS runs on a 9-bit serial line where the ninth bit marks the address byte. A PTY or a TCP socket has eight bits and no opinion about it, so the electrical part isn't emulated. What is emulated: correctly sized replies, BCD meters at the offsets the readers slice, and a CRC that saspy agrees with. The readers count bytes and cut at fixed offsets, so getting the sizes right is 90% of the job. The other 10% is also getting the sizes right.
Requests¶
No ninth bit, so the readers fake the wake-up with a marker byte. All of these work:
82 01 0F marker, address, command
80 01 0F other marker, address, command
01 0F address, command
0F just the command
The machine address defaults to 0x01. Change it with --sas-addr.
One request per read, no buffering. SAS hosts send a request and wait for the answer, so that's what they get.
Replies¶
The total length is fixed per command, and it's the number the reader passes to read(). One byte off and the
reader sits there until it times out.
CRC¶
CRC-16/KERMIT: polynomial 0x1021 bit-reversed (0x8408), initial value 0, low byte first on the wire. The
check value for "123456789" is 0x2189, sent as 89 21. Same as saspy, which is the point.
BCD¶
Most meters go out as BCD, two decimal digits per byte. 1234 in 4 bytes is 00 00 12 34. A meter too big for
its field keeps only the low digits, same as a real meter rolling over. Negative becomes 0.
A real exchange¶
With play = 1000, win = 2000, plays = 77:
28 bytes: address, command, six 4-byte BCD meters (cancelled credits, coin in, coin out, drop, jackpot, games
played), CRC. play stood in for coin in and win for coin out, because nobody set those.
Commands¶
Acks and polls¶
- General poll,
0x80or0x80 + addr: 1 byte,00, "no events". The most honest thing a machine says. - Ack-only,
0x01to0x0B,0x0E,0x2E,0x7F,0x8A,0x8B,0x8C,0x94,0xAA: 1 byte, the machine address. Shutdown, startup, sound, bill acceptor on/off, maintenance mode and friends. The host barks, the machine nods.
Meters¶
Meter names in parentheses are tried in order. The first one you actually set wins.
| Command | Length | Contents |
|---|---|---|
0x0F |
28 | cancelled_credits, (play, coin_in), (win, coin_out), drop, jackpot, plays |
0x10 to 0x4A |
8 | One 4-byte BCD meter. See the table below. |
0x18 |
8 | games_last_power_up, games_since_door_closure, 2 BCD bytes each |
0x19 |
24 | play, win, (coin_in, play), jackpot, plays |
0x1C |
36 | play, win, drop, jackpot, plays, won, slot_door_opened, power_reset |
0x1E |
28 | bill_1, bill_5, bill_10, bill_20, bill_50, bill_100 |
0x2F |
57 | Ten (code, 4-byte BCD) blocks, always the same ten. See below. |
0x52 |
22 | Game number, then play, win, jackpot, plays. Every game reports the machine-wide meters. |
0x9A |
18 | legacy_deductible, legacy_non_deductible, legacy_wager_match |
Single meter polls:
| Command | Meter | Command | Meter |
|---|---|---|---|
0x10 |
cancelled_credits |
0x2D |
hand_paid_cancelled_credit |
0x11 |
(play, coin_in) |
0x31 to 0x45 |
bill_*, one per denomination (below) |
0x12 |
(win, coin_out) |
0x46 |
bill_credit_total |
0x13 |
drop |
0x47 |
coin_external |
0x14 |
jackpot |
0x49 |
bills_in_stacker |
0x15 |
plays |
0x4A |
bills_stacker_credit_total |
0x16 |
won |
0x20 |
(bill_value_total, bill_in) |
0x17 |
lost |
0x2A |
(true_coin_in, coin_in, play) |
0x1A |
credits |
0x2B |
(true_coin_out, coin_out, win) |
The bill polls skip 0x3D, which is the cashout ticket. In order: 0x31 bill_1, 0x32 bill_2, 0x33
bill_5, 0x34 bill_10, 0x35 bill_20, 0x36 bill_50, 0x37 bill_100, 0x38 bill_500, 0x39
bill_1000, 0x3A bill_200, 0x3B bill_25, 0x3C bill_2000, 0x3E bill_2500, 0x3F bill_5000,
0x40 bill_10000, 0x41 bill_20000, 0x42 bill_25000, 0x43 bill_50000, 0x44 bill_100000, 0x45
bill_250. The order is SAS's idea, not mine.
| | | 0x2C | (hopper_level, coin_hopper) |
0x2F ignores which meters the request asks for and always sends these ten, because they're the ten the reader
always asks for. If the reader ever changes its mind, the simulator will be the last to know.
| Code | Meter | Code | Meter |
|---|---|---|---|
| 0 | play |
9 | (coin_out, win) |
| 1 | win |
10 | coin_hopper |
| 5 | plays |
11 | bill_in |
| 6 | won |
88 | bill_hopper |
| 8 | (coin_in, play) |
110 | bill_out |
Machine info¶
| Command | Length | Contents |
|---|---|---|
0x1F |
24 | Machine id and info. Hardcoded, because the machine is also fake. See below. |
0x21 |
6 | rom_signature, 2 bytes big endian. No math. |
0x51 |
6 | total_games, at least 1. An empty cabinet is just furniture. |
0x53 |
26 | Game N configuration. The 0x1F identity with the requested game number in front. |
0x54 |
20 | SAS version 6.03, serial SIM000000001 |
0x55 |
6 | selected_game, defaults to 1 |
0x56 |
8 | Enabled games: exactly one, the selected one |
0x6E |
10 | Authentication info: one component, nothing to authenticate. Trust me. |
0x7E |
11 | Local date and time, BCD MMDDYYYYHHMMSS. The only thing in here that isn't made up. |
0xA0 |
12 | Enabled features for game N, from features_1 to features_3 |
The 0x1F identity: game id GM, additional id 001, denomination code 01, max bet 100, no progressive
group, paytable P00001, base payback 9500. That's 95.00%. The house thanks you.
Tickets, validation, handpay¶
0x1B(24 bytes), handpay:progressive_group,progressive_level,handpay_amount,handpay_partial_amount,handpay_reset_id.0x3D(13): cashout ticket. (cashout_ticket_number,ticket_number), then (cashout_amount_cents,pending_cashout_amount,credits).0x48(10): last accepted bill.bill_country_code,bill_last_denomination,bill_last_count.0x4C(11):validation_machine_id,validation_sequence. The request is ignored.0x4D(35): one enhanced validation record, every field from avalidation_*meter.0x4F(11): hopper.hopper_status,hopper_percent, (hopper_level,coin_hopper).0x50(14): echoes the validation type, thenvalidation_total,validation_cumulative_amount.0x57(10): pending cashout.pending_cashout_type, (pending_cashout_amount,credits).0x58(5): status00, accepted. The number goes straight in the trash.0x70(20): ticket validation data.ticket_status, (ticket_amount,pending_cashout_amount),ticket_parsing_code,ticket_validation_number.0x71(20): redeem ticket. Status00, it worked. No credits move. It's a demo.0x7B(15): extended validation status. Asset number,cashable_ticket_exp,restricted_ticket_exp.
AFT¶
AFT (Advanced Funds Transfer) is the one corner of the simulator that changes state: transfers move the
credits meter, so a test can push money in and watch it land. Then take it out. The machine always wins,
even the fake one.
0x72 transfer funds. The request carries the fields in this order, after the command byte:
| Request byte | Field |
|---|---|
| 1 | length |
| 2 | transfer code |
| 3 | transaction index |
| 4 | transfer type |
| 5 to 9 | cashable amount, BCD |
| 10 to 14 | restricted amount, BCD |
| 15 to 19 | non-restricted amount, BCD |
| 20 | transfer flags |
| 21 to 24 | asset number |
| 25 to 44 | registration key |
| 45 | transaction ID length |
| 46 and up | transaction ID |
The transfer code says what to do with the request:
0x00(full) or0x01(partial): move the money.0xFF: don't move anything, report the last transfer. That's the status poll.- anything else (like
0x80, cancel): move nothing, report the last transfer.
The transfer type says which way the money goes:
0x00,0x10,0x11(in-house, bonus coin out, bonus jackpot): the cashable amount gets added tocredits.0x80,0x90(in-house to host, win to host): it gets taken out, floored at zero. The machine doesn't do loans.
The reply is 82 bytes and always says the transfer succeeded, which is more than most banks will do for you. It reports the last real transfer: its type at payload byte 4, the three amounts, the asset number, the transaction ID and the current date and time. A status poll right after a transfer reads the amount that just moved, which is exactly where the jupiter-os reader looks for it.
The transaction ID gets remembered from the request (its length byte is looked for at offset 60, then 45). Before the first transfer it's fifteen spaces and an exclamation mark. Somebody's idea of a placeholder.
0x73 register. Code 0x80 unregisters, anything else registers. No paperwork. 34 bytes.
0x74 lock and status. Lock code 0x80 reports 0xFF (not locked), anything else reports 0x00
(locked). The current credits go in the cashable slot, the other balances are zero. 40 bytes.
Asset number¶
4 bytes, little endian binary. Not BCD, for once. It comes from the asset_number meter, or 123456789 if you
never set one (15 CD 5B 07 on the wire). The readers build it the same way: asset 1 is 01 00 00 00.
Anything else¶
0xFF followed by zeros, padded to the length the reader expects for that command, or 8 bytes if the simulator
has never heard of it. Not a real answer, but it's the right size, and with these readers that's most of the
war.
Idle line¶
On a PTY, until the host sends anything, the simulator announces its address byte on the idle line, at most every 0.5 seconds. The first real request shuts that off for good. pyserial flushes the input buffer when it opens the port, so those bytes don't end up in front of your first reply.