ArduinoTITODeluxeR4Wifi SourseCode
ArduinoTITODeluxeR4Wifi SourseCode
Portions of the Arduino SAS protocol implementation by Ian Walker - Thank you!
Additional testing and troubleshooting by NLG member Eddiiie - Thank you!
9-Bit code by j.DeLutis and vashadow - Thank you!
Hardware requirements:
Arduino Uno R4 WiFi; Serial Port
Note: Remote access has been made compatible with BETTORSlots TITO apps for
IOS/Android; BETTORSlots is
not affiliated with this project and does not support or endorse using their apps
for this purpose;
This project does not use BETTORSlots code.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
ANY EXPRESS
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
//
-----------------------------------------------------------------------------------
-------------------------
// Required Libraries
//
-----------------------------------------------------------------------------------
-------------------------
#include "WiFiS3.h"
#include "ArduinoGraphics.h"
#include "Arduino_LED_Matrix.h"
#include <Arduino.h>
//
-----------------------------------------------------------------------------------
-------------------------
// Core Variables
//
-----------------------------------------------------------------------------------
-------------------------
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // UPDATE BEFORE COMPILING IF
YOU HAVE MULTIPLE MACHINES - Change to unique address for each board
IPAddress ip(192, 168, 1, 100); // Your default IP address - change if your network
addressing is different or if you have multiple machines
String versionString = "3.0.20241013DR4";
//
-----------------------------------------------------------------------------------
-------------------------
// SAS Protocol Variables
//
-----------------------------------------------------------------------------------
-------------------------
//
-----------------------------------------------------------------------------------
-------------------------
// Setup instances
//
-----------------------------------------------------------------------------------
-------------------------
WiFiServer server(80);
ArduinoLEDMatrix matrix;
//
-----------------------------------------------------------------------------------
-------------------------
// Setup - called once during init
//
-----------------------------------------------------------------------------------
-------------------------
void setup()
{
// Serial Logging
Serial.begin(9600);
matrix.begin();
matrixPrint("ATD");
// Initialize WiFi
initWiFi();
Serial.println(F("Initialization complete"));
matrixPrint(" OK");
}
//
-----------------------------------------------------------------------------------
-------------------------
// Main processing loop
//
-----------------------------------------------------------------------------------
-------------------------
void loop()
{
// Flash the LED
digitalWrite(LED_BUILTIN, HIGH);
// Check web
htmlPoll();
// Check game
generalPoll();
}
//
-----------------------------------------------------------------------------------
-------------------------
// Network Functions
//
-----------------------------------------------------------------------------------
-------------------------
if (!useDHCP) WiFi.config(ip);
status = WiFi.begin(ssid, pass);
// Connection Information
Serial.print(F("SSID: "));
Serial.println(WiFi.SSID());
IPAddress ip = WiFi.localIP();
Serial.print(F("IP Address: "));
Serial.println(ip);
//
-----------------------------------------------------------------------------------
-------------------------
// Game Functions
//
-----------------------------------------------------------------------------------
-------------------------
byte ac[4];
credits.trim();
for (int i = 0; i < 8 - credits.length(); i++) paddedValue += "0";
paddedValue += credits;
ac[0] = dec2bcd(paddedValue.substring(0, 2).toInt());
ac[1] = dec2bcd(paddedValue.substring(2, 4).toInt());
ac[2] = dec2bcd(paddedValue.substring(4, 6).toInt());
ac[3] = dec2bcd(paddedValue.substring(6, 8).toInt());
//
-----------------------------------------------------------------------------------
-------------------------
// Misc Functions
//
-----------------------------------------------------------------------------------
-------------------------
// Matrix display text message
if (keyValueSeparator != NULL) {
char* currentKey = pair;
size_t keyLength = keyValueSeparator - pair;
return String(value);
}
}
// Used by urlDecode
decodedString += c;
}
yield();
}
return decodedString;
}
//
-----------------------------------------------------------------------------------
-------------------------
// HTML Server
//
-----------------------------------------------------------------------------------
-------------------------
void htmlPoll()
{
WiFiClient client = server.available();
if (!client) return;
// Parse querystring
if (strstr(stringData,"ds=")) // Game Statistics
{
client.print(F("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection:
close\r\n\r\n<html>\r\n"));
client.print(F("<head></head><body><h2>GAME STATISTICS</h2><br>"));
client.print(F("Credits<br>"));
client.print(pollMeters(mCredits));
client.print(F("<br>Total In<br>"));
client.print(pollMeters(mCoinIn));
client.print(F("<br>Total Won<br>"));
client.print(pollMeters(mTotWon));
client.print(F("<br>Total Games<br>"));
client.print(pollMeters(mTotGames));
client.print(F("<br>Games Won<br>"));
client.print(pollMeters(mgamesWon));
client.print(F("<br>Games Lost<br>"));
client.print(pollMeters(mgamesLost));
client.print(F("<br>"));
client.print(F("</body>"));
client.print(F("</html>\r\n\r\n"));
client.stop();
return;
}
client.stop();
delay(1000);
return;
}
}
//
-----------------------------------------------------------------------------------
-------------------------
// SAS Protocol
// NOTE: This does not implement the full protocol - just enough for TITO, Meters,
some controls and
// System Bonusing
//
-----------------------------------------------------------------------------------
-------------------------
// The General poll must be running in order to send data to game and receive
responses
void generalPoll()
{
byte eventCode = 0;
Serial1.write((uint8_t *)gPoll1, sizeof(gPoll1));
delay(20);
Serial1.write((uint8_t *)gPoll2, sizeof(gPoll2));
delay(10); // Wait for data on the serial bus
if (Serial1.available() > 0) {
eventCode = Serial1.read();
if (sasOnline==false) sasOnline=true;
}
if (eventCode != 0x1F && eventCode != 0x00 && eventCode != 0x01 && eventCode !=
0x80 && eventCode != 0x81 && eventCode != 0x7C) {
Serial.print(F("SAS Event Received: ")); Serial.print(eventCode, HEX);
Serial.print(F(" "));
switch (eventCode) {
case 0x11:
Serial.println(F("Game door opened"));
break;
case 0x12:
Serial.println(F("Game door closed"));
break;
case 0x17:
Serial.println(F("AC power was applied to gaming machine"));
break;
case 0x18:
Serial.println(F("AC power was lost from gaming machine"));
break;
case 0x19:
Serial.println(F("Cashbox door was opened"));
break;
case 0x1A:
Serial.println(F("Cashbox door was closed"));
break;
case 0x66:
Serial.println(F("Cash out button pressed"));
break;
case 0x51:
Serial.println(F("Handpay is pending"));
getHandpayInfo();
break;
case 0x52:
Serial.println(F("Handpay was reset"));
break;
case 0x2B:
Serial.println(F("Bill rejected"));
break;
case 0x7E:
Serial.println(F("Game started"));
break;
case 0x7F:
Serial.println(F("Game ended"));
break;
case 0x71:
case 0x72:
if (changeToCredits) {
addCredits(changeCredits); // To enable 'Change button' credits
}
break;
case 0x57:
SystemValidation();
break;
case 0x3D:
CashOutState();
break;
case 0x67:
RedeemTicket();
break;
case 0x68:
ConfirmRedeem();
break;
default:
break;
}
matrixPrint(" OK");
Serial.println(F(""));
}
}
// Data from game may be delayed due to other events or the state of the board
Serial1.readBytes(responseBytes, sizeof(responseBytes));
ret[0] = {0x01};
ret[1] = waitfor;
memcpy(ret + 2, responseBytes, sizeof(responseBytes));
return;
}
return true;
}
SendTypeR(meter, 2); // meter size will always be 2; sizeof was returning 4 due
to the 32 bit pointers
waitForResponse(meter[1], meterData, sizeof(meterData));
String sMeter;
for (int i = 2; i < 6; i++) {
sMeter.concat(String(bcd2dec(meterData[i]) < 10 ? "0" : ""));
sMeter.concat(String(bcd2dec(meterData[i])));
}
return sMeter.toInt();
}
void getHandpayInfo()
{
Serial.println(F("Getting handpay information"));
SendTypeR(HPI, sizeof(HPI));
waitForResponse(HPI[1], HPS, sizeof(HPS));
}
void SystemValidation()
{
// Retry up to 2 times
Serial.println(F("Getting cashout information"));
if (!sasError)
{
SVN [0] = SASAdr; // Address
SVN [1] = 0x58; // Receive Validation Number
SVN [2] = 0x01; // Validation ID
SVN [3] = COT [2]; // Cashout Type
SVN [4] = 0x00; // None
SVN [5] = 0x00; // None
SVN [6] = COT [3]; // Cashout Value Byte5 (MSB)
SVN [7] = COT [4]; // Cashout Value Byte4
SVN [8] = COT [5]; // Cashout Value Byte3
SVN [9] = COT [6] ; // Cashout Value Byte2
SVN [10] = COT [7]; // Cashout Value Byte1 (LSB)
CalculateCRC(COT, 8);
if (CRCH != COT[8] && CRCL != COT[9])
{
// Bad Validation Data
Serial.println(F("Unable to validate - CRC does not match"));
SVN [2] = 0x00;
SendTypeS(SVN, sizeof(SVN));
waitForResponse(SVN[1], COS, sizeof(COS));
Serial.println(F("Cashout aborted"));
}
else
{
Serial.println(F("Validating cashout request"));
SendTypeS(SVN, sizeof(SVN));
waitForResponse(SVN[1], COS, sizeof(COS));
Serial.println(F("Printing cashout ticket"));
}
}
}
ticketData[0] = SASAdr;
ticketData[1] = 0x7D;
ticketData[2] = bSize;
ticketData[3] = 0x00;
ticketData[4] = 0x00;
ticketData[5] = 0x00;
ticketData[6] = loc.length();
SendTypeS(ticketData, sizeof(ticketData));
waitForResponse(ticketData[1], TDR, sizeof(TDR));
Serial.println(F("Updated Cashout Ticket Data"));
return true;
}
void CashOutState()
{
SendTypeS(EVInfo, sizeof(EVInfo));
waitForResponse(EVInfo[1], TPS, sizeof(TPS));
Serial.println(F("Cashout ticket has been printed"));
}
SendTypeS(TIM, sizeof(TIM));
return waitForACK(SASAdr,"DateTime set by host");
}
bool LegacyBonus (byte SASAdr, byte Amount1, byte Amount2, byte Amount3, byte
Amount4, byte Type)
{
LBS [0] = SASAdr;
LBS [1] = 0x8A;
LBS [2] = Amount1;
LBS [3] = Amount2;
LBS [4] = Amount3;
LBS [5] = Amount4;
LBS [6] = Type;
SendTypeS(LBS, sizeof(LBS));
return waitForACK(SASAdr, "");
}
void RedeemTicket()
{
// Retry up to 2 times
Serial.println(F("Ticket inserted"));
if (!sasError)
{
Serial.println(F("Received ticket data"));
TRS [0] = 0x01; // Address
TRS [1] = 0x71; // Command
TRS [2] = 0x10; // Number of Bytes
TRS [3] = 0x00; // Transfer Code
TRS [4] = TEQ [14]; // Ticket Amount BCD1 LSB
TRS [5] = TEQ [15]; // Ticket Amount BCD2
TRS [6] = TEQ [16]; // Ticket Amount BCD3
TRS [7] = TEQ [17]; // Ticket Amount BCD4
TRS [8] = TEQ [18]; // Ticket Amount BCD5 MSB
TRS [9] = 0x00; // Parsing Code
TRS [10] = TEQ [10]; // Validation BCD1
TRS [11] = TEQ [11]; // Validation BCD2
TRS [12] = TEQ [12]; // Validation BCD3
TRS [13] = TEQ [13]; // Validation BCD4
TRS [14] = TEQ [14]; // Validation BCD5
TRS [15] = TEQ [15]; // Validation BCD6
TRS [16] = TEQ [16]; // Validation BCD7
TRS [17] = TEQ [17]; // Validation BCD8
TRS [18] = TEQ [18]; // Validation BCD9
Serial.println(F("Authorizing ticket"));
SendTypeS(TRS, sizeof(TRS));
waitForResponse(TRS[1], TEQ, sizeof(TEQ));
void ConfirmRedeem()
{
SendTypeS(transComplete, sizeof(transComplete));
waitForResponse(transComplete[1], TEQ, sizeof(TEQ));
uint16_t d[len];
for (int i = 0; i < len; i++) {
d[i] = temp[i]; // Move data to a 16 bit array
if(i == 0) {
d[i] = d[i] | 0xFF00; // For the address byte turn on the wake bit
} else {
d[i] = d[i] | 0xFC00; // For all the data turn off the wake bit
}
}
Serial1.write((uint8_t * )d, sizeof(d));
}
long crc;
long q;
byte c;
crc = 0;
for (int i = 0; i < len; i++)
{
c = val[i];
q = (crc ^ c) & 0x0f;
crc = (crc >> 4) ^ (q * 0x1081);
q = (crc ^ (c >> 4)) & 0xf;
crc = (crc >> 4) ^ (q * 0x1081);
}
CRCH = crc & 0xff;
CRCL = crc >> 8;
}