#include <Wire.h>
#include <ErriezSerialTerminal.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
// Newline character '\r' or '\n'
char newlineChar = '\n';
char delimiterChar = ' ';
SerialTerminal term(newlineChar, delimiterChar);
// Code ref table
/*
E - Error
I - Info
N - Not
& - And
@ - At
INT - Initialization
UNK - Unknown
ADR - Address
DVC - Device
BUS - Bus
SCN - Scanning
FND - Found
RCV - Received
DTA - Data
PRC - Processing
TRG - Target
VAL - Value
SST - Subset
AOR - And or
NSP - Not Specified
WHL - Withholds
INI - Incorrect Initializer
ex: E(RCV DTA: TRG & VAL WHL INI & SST VAL NSP)
Means: Error(Received Data: Target and Value withholds Incorrect Initializer and Subset Value Not Specified)
CI! - Compiler info is about to print.
C - Compiler type
V - Compiler version
F - Compiled file
D - Compile date
T - Compile time
P - Compiler Platform
A - Compiler Architecture
CS - C Standard
CHCI - Check code input
CHCC - Check is code correct
CPAS - Code is correct
CFAL - Code is incorrect
MNUP - Menu Printer
*/
// Define the LCD address, width, and height
#define LCD_ADDRESS 0x27
#define LCD_COLUMNS 20
#define LCD_ROWS 4
bool hasLCD = false;
// Types.
typedef void (*CallbackFunction)();
typedef struct {
const char* menuItem;
CallbackFunction callback;
} menuItem;
// Functions.
void enableScooter();
void openTrunk();
void menuOption3();
// Helper functions.
void printMenu(menuItem menuItems[], int itemCount, int cItem);
void i2cScanBus();
bool i2cAdrExists(byte adr);
// Debug functions.
void unknownCommand(const char* command);
void cmdHelp();
void cmdManual();
void cmdEnterDebug();
void cmdFMem();
void kpadTest();
void srlMsgTest();
// Pin definitions
#define DIRSW_CLK 11 // Directional switch - Movement clock
#define DIRSW_DIR 12 // Directional switch - Direction of movement
#define DIRSW_BTN A0 // Directional switch - Pushed down
#define BACK_BTN 13 // Back button
#define SPKR_PIN A3
// Relays
#define RELAY_UTIL_POWER 10 // Misc power - Blinkers, Brake lights, etc.
#define RELAY_TRUNK_OPEN 9 // Opens the trunk.
#define RELAY_ENGINE_KILL 8 // Engine run relay.
// Status's
#define DIRSW_DIR_BCK LOW
#define DIRSW_DIR_FWD HIGH
#define DIRSW_BTN_DWN LOW
#define BACK_BTN_DOWN HIGH
// Set up the LCD
LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLUMNS, LCD_ROWS);
// Main menu
menuItem mainMenu[] = {
{"Enable Scooter", enableScooter},
{"Open Trunk", openTrunk},
{"Power Down", menuOption3},
{"Another option ", menuOption3},
{"Settings", menuOption3},
{NULL, NULL} // Sentinel value to mark the end of the menu
};
// Debug menu
menuItem debugMenu[] = {
{"Test Relays", NULL},
{"Test KPad", kpadTest},
{"Hard Reset", NULL},
{"System Info", NULL},
{"Get SRL Msg", srlMsgTest},
{NULL, NULL} // Sentinel value to mark the end of the menu
};
// Define the keypad layout
const byte ROWS = 4; // Four rows
const byte COLS = 4; // Four columns
const char keys[ROWS][COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
// Define the row and column pins
byte rowPins[ROWS] = {7, 6, 5, 4}; // Connect to the row pins of the keypad
byte colPins[COLS] = {3, 2, A2, A1}; // Connect to the column pins of the keypad
// Create the Keypad object
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// The correct code
const String correctCode = "BADCAB";
String enteredCode = "";
int failCount = 0;
const int maxFails = 3;
const byte zero[] = {
B00000,
B00000,
B00000,
B00000,
B00000,
B00000,
B00000,
B00000
};
const byte one[] = {
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000
};
const byte two[] = {
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000
};
const byte three[] = {
B11100,
B11100,
B11100,
B11100,
B11100,
B11100,
B11100,
B11100
};
const byte four[] = {
B11110,
B11110,
B11110,
B11110,
B11110,
B11110,
B11110,
B11110
};
const byte five[] = {
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111
};
void lampTest() {
// LCD Test
for(int i=0; i<5+1; i++) {
for(int y=0; y<LCD_ROWS; y++) {
for(int x=0; x<LCD_COLUMNS; x++) {
lcd.setCursor(x,y);
lcd.write(i);
}
}
delay(100);
}
delay(1000);
lcd.clear();
}
void setup() {
// Debug serial.
Serial.begin(115200);
// Wire clock.
Wire.setClock(100000);
// Setup inputs
pinMode(DIRSW_CLK, INPUT);
pinMode(DIRSW_DIR, INPUT);
pinMode(DIRSW_BTN, INPUT_PULLUP);
pinMode(BACK_BTN, INPUT);
// Setup outputs.
pinMode(SPKR_PIN, OUTPUT);
pinMode(RELAY_UTIL_POWER, OUTPUT);
pinMode(RELAY_TRUNK_OPEN, OUTPUT);
pinMode(RELAY_ENGINE_KILL, OUTPUT);
// Terminal handler
term.setDefaultHandler(unknownCommand);
// Terminal commands.
term.addCommand("?", cmdHelp);
term.addCommand("M", cmdManual);
term.addCommand("D", cmdEnterDebug);
term.addCommand("F", cmdFMem);
// Power tone.
tone(SPKR_PIN, 100, 80);
delay(80);
tone(SPKR_PIN, 400, 80);
delay(80);
tone(SPKR_PIN, 800, 80);
// Scan full I2C Bus.
i2cScanBus();
// Check for required I2C Devices
hasLCD = i2cAdrExists(LCD_ADDRESS);
if(!hasLCD) {
// Serial error.
Serial.println(F("E(INT: \"Display\" N FND)"));
// Error tone
tone(SPKR_PIN, 300, 500);
delay(1000);
tone(SPKR_PIN, 300, 500);
delay(1000);
}
// Initialize the LCD
lcd.begin(LCD_COLUMNS, LCD_ROWS);
lcd.createChar(0, zero);
lcd.createChar(1, one);
lcd.createChar(2, two);
lcd.createChar(3, three);
lcd.createChar(4, four);
lcd.createChar(5, five);
lcd.backlight();
lampTest();
// Make a detailed info.
char* infoStr = getCompileInfo();
// Print info
lcd.setCursor(0,0);
lcd.print(F("Digital Key System"));
// More info
lcd.setCursor(0,1);
lcd.print(F("(C)2024 Dakotath"));
lcd.setCursor(0,3);
scrollMessage(3, "For Yamaha Zuma Scooter", 100, 20);
scrollMessage(3, infoStr, 50, 20);
free(infoStr);
// Clear.
lcd.clear();
lcd.print(F("Enter security code:"));
}
// Main unlocked loop.
void loopUnlocked() {
// Menu.
int menuItemCount = sizeof(mainMenu) / sizeof(mainMenu[0]);
int menuIndex = 0;
printMenu(mainMenu, menuItemCount, menuIndex);
while(true) {
// Term Handler.
term.readSerial();
// Directional switch moved.
if(digitalRead(DIRSW_CLK) == LOW) {
tone(SPKR_PIN, 500, 20);
if(digitalRead(DIRSW_DIR) == DIRSW_DIR_BCK) {
menuIndex--;
} else {
menuIndex++;
}
// Display the menu
printMenu(mainMenu, menuItemCount, menuIndex);
}
// Selected menu entry
if(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
// execute function
mainMenu[menuIndex].callback();
}
}
}
void loop() {
// Call the function to check the code and handle the result
bool isCodeCorrect = checkCode();
if (isCodeCorrect) {
displayMessage(F("Yes Sir!"));
delay(2000);
loopUnlocked();
reset();
} else if (failCount >= maxFails) {
displayMessage(F("Max Attempts Reached"));
delay(2000);
reset();
}
// Read from terminal.
term.readSerial();
}
void updateProgressBar(unsigned long count, unsigned long totalCount, int lineToPrintOn) {
double factor = totalCount/100.0;
int percent = (count+1)/factor;
int number = percent/5;
int remainder = percent%5;
if(number > 0)
{
for(int j = 0; j < number; j++)
{
lcd.setCursor(j,lineToPrintOn);
lcd.write(5);
}
}
lcd.setCursor(number,lineToPrintOn);
lcd.write(remainder);
if(number < LCD_COLUMNS)
{
for(int j = number+1; j <= LCD_COLUMNS; j++)
{
lcd.setCursor(j,lineToPrintOn);
lcd.write(0);
}
}
}
// Function to get the free memory available
int freeMemory() {
extern int __heap_start;
extern int *__brkval;
int v;
return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval);
}
// Function to return compile-time information as a single line string
char* getCompileInfo(void) {
// Define the maximum length of the output string
const int maxLength = 120;
char* info = malloc(maxLength);
if (info == NULL) {
return NULL; // Allocation failed
}
// Compiler Information
const char* compiler = "unknown";
#ifdef __GNUC__
compiler = "GCC";
#elif _MSC_VER
compiler = "MSVC";
#endif
// Define a macro for the architecture
const char* architecture = "?";
#if defined(__x86_64__) || defined(_M_X64)
architecture = "x86_64";
#elif defined(__i386__) || defined(_M_IX86)
architecture = "x86";
#elif defined(__arm__) || defined(_M_ARM)
architecture = "ARM";
#elif defined(__avr32__) || defined(_M_AVR)
architecture = "AVR";
#elif defined(__aarch64__)
architecture = "ARM64";
#endif
// Define the C Standard
const char* cStandard = "?";
#if __STDC_VERSION__ >= 201112L
cStandard = "C11|>";
#elif __STDC_VERSION__ >= 199901L
cStandard = "C99";
#else
cStandard = "C89/C90";
#endif
// Determine platform
const char* platform = "?";
#ifdef _WIN32
platform = "W32";
#elif _WIN64
platform = "W64";
#elif __unix__
platform = "UNX";
#elif __APPLE__
platform = "MOS";
#elif __linux__
platform = "LNX";
#endif
// Format the information into a single string
snprintf(info, maxLength,
"C: %s, V: %s, F: %s, D: %s, T: %s, P: %s, A: %s, CS: %s, FM: %dB",
compiler,
__VERSION__,
__FILE__,
__DATE__,
__TIME__,
platform,
architecture,
cStandard,
freeMemory());
// Write info to serial.
Serial.println("CI!");
Serial.println(info);
return info;
}
// Sir, what would you like to eat?
void printMenu(menuItem menuItems[], int itemCount, int cItem) {
lcd.clear(); // Clear the display at the beginning
int maxLines = LCD_ROWS; // Number of lines on the LCD
int startLine = (cItem < maxLines) ? 0 : (cItem - maxLines + 1); // Determine start line for scrolling
// Ensure startLine does not exceed available items
if (startLine + maxLines > itemCount) {
startLine = itemCount - maxLines;
}
for (int i = startLine; i < startLine + maxLines && i < itemCount; i++) {
bool isSelected = (cItem == i);
// Format the menu entry with selection marker
char menuEntry[17]; // Use a buffer size that fits within the LCD width
snprintf(menuEntry, sizeof(menuEntry), "%c%s", isSelected ? '>' : ' ', menuItems[i].menuItem);
// Set line on LCD
lcd.setCursor(0, i - startLine); // Adjust the line index for scrolling
lcd.print(menuEntry);
// Debug
char* dbgInf = malloc(40);
sprintf(dbgInf, "MNUP: %s", menuEntry);
Serial.println(dbgInf);
free(dbgInf);
}
Serial.println();
}
// Function to display a message on the LCD and clear the screen
void displayMessage(const String &message) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(message);
}
bool checkCode() {
char key = keypad.getKey();
if (key) {
// Blip.
tone(SPKR_PIN, 620, 50);
// Display the pressed key on the LCD
lcd.setCursor(0, 1);
lcd.print(F("Code: "));
lcd.print(enteredCode);
lcd.print(key);
// Append the key to the entered code
enteredCode += key;
Serial.print(F("CHCI: "));
Serial.println(enteredCode);
// Check if the entered code matches the correct code
if (enteredCode.length() >= correctCode.length()) {
if (enteredCode.endsWith(correctCode)) {
Serial.println();
Serial.println(F("CHCC: CPAS"));
// Ok tone
delay(300);
tone(SPKR_PIN, 700, 100);
delay(100);
tone(SPKR_PIN, 620, 50);
delay(50);
return true; // Code is correct
} else if (enteredCode.length() > correctCode.length()) {
// Code is incorrect
failCount++;
enteredCode = ""; // Clear entered code for next attempt
displayMessage(F("Code Incorrect"));
Serial.println(F("CHCC: CFAL"));
delay(2000); // Show the message for 2 seconds
resetSoft();
return false;
}
}
}
return false; // Code is incorrect or not yet complete
}
// Function to scroll text on a specified line
void scrollMessage(int row, String message, int delayTime, int totalColumns) {
for (int i=0; i < totalColumns; i++) {
message = " " + message;
}
message = message + " ";
for (int position = 0; position < message.length(); position++) {
updateProgressBar(position, message.length(), 2);
lcd.setCursor(0, row);
lcd.print(message.substring(position, position + totalColumns));
term.readSerial();
delay(delayTime);
}
}
// Function to reset the code entry process
void reset() {
enteredCode = "";
failCount = 0;
displayMessage(F("Enter Code:"));
}
void resetSoft() {
enteredCode = "";
displayMessage(F("Enter Code:"));
}
// Real shit.
void enableScooter() {
lcd.clear();
lcd.print(F("Enabling Scooter"));
digitalWrite(RELAY_UTIL_POWER, HIGH);
digitalWrite(RELAY_ENGINE_KILL, HIGH);
delay(1000);
loopUnlocked();
}
void openTrunk() {
lcd.clear();
lcd.print(F("Opening Trunk"));
digitalWrite(RELAY_TRUNK_OPEN, HIGH);
delay(200);
digitalWrite(RELAY_TRUNK_OPEN, LOW);
delay(1000);
loopUnlocked();
}
void menuOption3() {
//printf("Menu Option 2 selected.\n");
}
bool i2cAdrExists(byte adr) {
byte error;
Wire.beginTransmission(adr);
error = Wire.endTransmission();
if (error == 0)
{
return true;
}
else if (error==4)
{
Serial.print(F("E(I2C: UNK E @ ADR 0x"));
if (adr<16) {
Serial.print("0");
}
Serial.print(adr,HEX);
Serial.println(")");
return false;
}
return false;
}
// Scan the entire I2C Bus.
void i2cScanBus() {
byte error, address;
int nDevices;
Serial.println("I(I2C: SCN BUS)");
nDevices = 0;
for(address = 1; address < 128; address++ )
{
// The i2c_scanner uses the return value of
// the Write.endTransmisstion to see if
// a device did acknowledge to the address.
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0)
{
Serial.print(F("I(I2C: DVC FND @ ADR 0x"));
if (address<16) {
Serial.print("0");
}
Serial.print(address,HEX);
Serial.println(")");
nDevices++;
}
else if (error==4)
{
Serial.print(F("E(I2C: UNK E @ ADR 0x"));
if (address<16) {
Serial.print("0");
}
Serial.print(address,HEX);
Serial.println(")");
}
}
if (nDevices == 0) {
Serial.println("E(I2C: 0 DVC FND)");
} else {
Serial.print("I(I2c: ");
Serial.print(nDevices);
Serial.println(" DVC FND)");
}
}
// Terminal.
void unknownCommand(const char *command)
{
// Print unknown command
Serial.print(F("Unknown command: "));
Serial.println(command);
}
void cmdHelp() {
Serial.println(F("YW50 Digital Key System:"));
Serial.println(F("\t? - Help"));
}
// Manual code execution.
void cmdManual() {
// Argument
char* arg;
// Device to target
char *tDevice = NULL;
int dState = 0;
// Get target device.
arg = term.getNext();
if (arg != NULL) {
tDevice = arg;
} else {
Serial.println(F("E: TRG NSP"));
return;
}
// Get State to set.
arg = term.getNext();
if (arg != NULL) {
// Is int?
if(atoi(arg)) {
dState = atoi(arg);
} else {
Serial.println(F("E(VAL INI)"));
return;
}
} else {
Serial.println(F("E(VAL NSP)"));
return;
}
// Process info.
char* dbgLne = malloc(100);
sprintf(dbgLne, "TRG: %s, VAL: %d", tDevice, dState);
Serial.println(dbgLne);
free(dbgLne);
/*
Devices:
DBLK - Display Backlight
SPIN - Sets a pin value
RPIN - Reads a pin value
*/
// Switch
if(strcmp(tDevice, "DBLK") == 0) { // LCD Backlight
switch(dState) {
case 1:
lcd.noBacklight();
break;
case 2:
lcd.backlight();
break;
default:
Serial.println(F("E(DTA PRC: VAL INI)"));
}
} else if(strcmp(tDevice, "SPIN") == 0) { // Set Pin Values
// We need another argument for the pin value
int pnState = LOW;
arg = term.getNext();
if (arg != NULL) {
// Is int?
if(atoi(arg)) {
pnState = atoi(arg);
} else {
Serial.println(F("E(DTA PRC: SST VAL INI)"));
return;
}
} else {
Serial.println(F("E(DTA PRC: SST VAL NSP)"));
return;
}
// Set it.
digitalWrite(dState-1, pnState-1);
} else if(strcmp(tDevice, "RPIN") == 0) { // Read a pin value
// Pin value.
int pVal = digitalRead(dState);
Serial.print(F("VAL: "));
Serial.println(pVal);
} else {
Serial.println(F("E(DTA PRC: TRG INI)"));
}
}
// Show debug menu
void cmdEnterDebug() {
// Debug Menu.
int menuItemCount = sizeof(debugMenu) / sizeof(debugMenu[0]);
int menuIndex = 0;
printMenu(debugMenu, menuItemCount, menuIndex);
// Alert tone
tone(SPKR_PIN, 800, 40);
delay(100);
tone(SPKR_PIN, 800, 40);
while(true) {
// Term Handler.
term.readSerial();
// Directional switch moved.
if(digitalRead(DIRSW_CLK) == LOW) {
tone(SPKR_PIN, 700, 20);
if(digitalRead(DIRSW_DIR) == DIRSW_DIR_BCK) {
menuIndex--;
} else {
menuIndex++;
}
// Display the menu
printMenu(debugMenu, menuItemCount, menuIndex);
}
// Selected menu entry
if(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
// Wait for release.
while(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
// Nothing
}
// Blip.
tone(SPKR_PIN, 900, 30);
// execute function
debugMenu[menuIndex].callback();
// Display the menu again.
printMenu(debugMenu, menuItemCount, menuIndex);
}
}
}
// Keypad test.
void kpadTest() {
lcd.clear();
lcd.print(F("Press Keys:"));
lcd.setCursor(0, 3);
lcd.print(F("'OK' to go back"));
lcd.setCursor(0, 1);
// Loop
while(true) {
char key = keypad.getKey();
if (key) {
// Display the pressed key on the LCD
lcd.print(key);
// Blip
tone(SPKR_PIN, 750, 20);
}
// Go back
if(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
// Wait for release.
while(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
// Nothing
}
return;
}
}
}
// Serial messages.
void srlMsgTest() {
lcd.clear();
// Loop.
while(true) {
if(Serial.available() > 1) {
lcd.write(Serial.read());
}
// Go back
if(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
// Wait for release.
while(digitalRead(DIRSW_BTN) == DIRSW_BTN_DWN) {
// Nothing
}
return;
}
}
}
void cmdFMem() {
Serial.println(F("Free Memory:"));
Serial.println(freeMemory());
}