// VendingMachine.ino (کامل با معماری جدید کنترل موتور)
// شامل ۴۰ درایور مستقل با کنترل Enable از طریق شیفت رجیستر
#include <Wire.h>
#include <SPI.h>
#include <FS.h>
#include <SPIFFS.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Keypad.h>
#include "HX711_ADC.h"
#include <Preferences.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include "mbedtls/md.h"
#include <WiFiManager.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
// ------------------ CONFIG / PINS ------------------
// I2C (OLED)
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
// OLED
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_I2C_ADDRESS 0x3C
// Keypad (4x4)
const byte KEYPAD_ROWS = 4;
const byte KEYPAD_COLS = 4;
byte KEYPAD_ROW_PINS[KEYPAD_ROWS] = {19, 18, 17, 16}; // R1..R4
byte KEYPAD_COL_PINS[KEYPAD_COLS] = {34, 35, 32, 33}; // C1..C4
char KEYPAD_KEYS[KEYPAD_ROWS][KEYPAD_COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// کنترل درایورها با شیفت رجیستر
#define SHIFT_REG_DATA_PIN 2 // DS Pin (Serial Data) -> D2
#define SHIFT_REG_CLOCK_PIN 15 // SHCP Pin (Shift Clock) -> D15
#define SHIFT_REG_LATCH_PIN 12 // STCP Pin (Latch) -> D12
#define NUM_DRIVERS 40
#define NUM_SHIFT_REGISTERS 5 // 5 chips * 8 outputs/chip = 40 درایور
// پینهای مشترک برای تمام درایورها
#define COMMON_STEP_PIN 23 // پین STEP مشترک
#define COMMON_DIR_PIN 25 // پین DIR مشترک
// سرعتهای مختلف (میکروثانیه برای delay)
#define SPEED_LEVEL_1 1000 // سرعت آهسته
#define SPEED_LEVEL_2 500 // سرعت متوسط
#define SPEED_LEVEL_3 250 // سرعت سریع
// HX711 (Loadcell)
#define HX711_DOUT_PIN 4
#define HX711_SCK_PIN 5
#define LOADCELL_THRESHOLD 30.0f
#define LOADCELL_CALIBRATION_FACTOR 415.0f
#define LOADCELL_STABILIZING_TIME 2000
#define LOADCELL_TARE_TIMEOUT 5000
#define LOADCELL_READ_DELAY 100
// App logic - مقادیر پیشفرض
#define DEFAULT_ADMIN_CODE "9999"
#define MAX_PRODUCTS 40
#define MAX_HISTORY 40
#define PRODUCT_CODE_LENGTH 4
#define PRODUCT_NAME_LENGTH 20
#define MOTOR_STEPS_PER_REV 800
#define DISPENSE_TIMEOUT_MS 7000
#define DEBOUNCE_TIME_MS 200
#define INACTIVITY_TIMEOUT_MS 30000
// WiFi & Network - مقادیر پیشفرض
#define WIFI_TIMEOUT 10000
#define NTP_UPDATE_INTERVAL 3600000
#define DEFAULT_SERVER_URL "https://yourserver.com/api/transaction"
// JSON doc sizes
const size_t DOC_PRODUCTS_SIZE = 16384;
const size_t DOC_HISTORY_SIZE = 8192;
const size_t DOC_SMALL = 1024;
// گواهی Root CA برای ارتباط امن HTTPS
const char* root_ca_cert = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n" \
"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMRkwFwYDVQQDDBBBmYXpv\n" \
"biBSb290IENBIDEwHhcNMTUwNTI2MDAwMDAwWhcNMzgwMTE3MDAwMDAwWjA5MQsw\n" \
"CQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMRkwFwYDVQQDDBBBbWF6b24gUm9v\n" \
"dCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAJ4gHHKeNXjca\n" \
"9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM9O\n" \
"6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIF\n" \
"AGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VO\n" \
"ujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93\n" \
"FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\n" \
"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n" \
"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n" \
"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n" \
"U5PMCCjjmCXPI6T53iHTfIuJruydjsw2hUwsOBYk2o2/nWF09e7/3GXK+F8CLFM9\n" \
"wYfNqkPPQnGhh4M2+U4XiQ1FJrJgbN8TdwX1EEjSU5cFy5y4oD2PJgE0mG6VhJ8O\n" \
"HSfH6WeyGm4tUqk4J3J5tH6KQMRs2YnTMKuPOLfK1XrN2MGI2mKn0NqVfCv9pHs4\n" \
"vAg8o4FJhUk9dTotLZ5To0EduADL6Mko1UJy3R18N65aM2U5p+qVFa5zzgwHkEey\n" \
"aCq7W2pQ7Q1Hc3/y4/VBpaZWEg=\n" \
"-----END CERTIFICATE-----\n";
// ------------------ DATA STRUCTS ------------------
struct Product {
bool isValid = false;
char code[PRODUCT_CODE_LENGTH + 1] = "";
char name[PRODUCT_NAME_LENGTH + 1] = "";
uint32_t price = 0;
uint8_t driverIndex = 0; // 1-40
uint8_t speedLevel = 2; // 1, 2, or 3
uint16_t stock = 0;
};
struct Transaction {
char productCode[PRODUCT_CODE_LENGTH + 1] = "";
char productName[PRODUCT_NAME_LENGTH + 1] = "";
uint32_t price = 0;
char datetime[20] = ""; // "YYYY-MM-DD HH:MM:SS"
bool success = false;
};
// ساختار برای ذخیره تنظیمات
struct Config {
String serverURL = DEFAULT_SERVER_URL;
String adminCode = DEFAULT_ADMIN_CODE;
int shiftRegDataPin = SHIFT_REG_DATA_PIN;
int shiftRegClockPin = SHIFT_REG_CLOCK_PIN;
int shiftRegLatchPin = SHIFT_REG_LATCH_PIN;
};
// صف برای تراکنشها
QueueHandle_t transactionQueue;
// ------------------ FORWARD DECL ------------------
class DisplayManager;
class KeypadManager;
class Loadcell;
class Database;
class MotorControl;
class NetworkManager;
class UserFlow;
class TextInputHelper;
class AdminMenu;
// ------------------ TEXT INPUT HELPER ------------------
class TextInputHelper {
public:
TextInputHelper() {
keymap[0] = " 0";
keymap[1] = ".:/\\_?!@1";
keymap[2] = "abc2ABC";
keymap[3] = "def3DEF";
keymap[4] = "ghi4GHI";
keymap[5] = "jkl5JKL";
keymap[6] = "mno6MNO";
keymap[7] = "pqrs7PQRS";
keymap[8] = "tuv8TUV";
keymap[9] = "wxyz9WXYZ";
}
void start(String initial_value = "") {
buffer = initial_value;
lastKey = '\0';
pressCount = 0;
lastPressTime = 0;
}
void handleKey(char key) {
if (key == '#') {
confirmChar();
return;
}
if (key == '*') {
confirmChar();
if (buffer.length() > 0) {
buffer.remove(buffer.length() - 1);
}
return;
}
int keyIndex = key - '0';
if (keyIndex < 0 || keyIndex > 9) return;
if (lastKey != key) {
confirmChar();
pressCount = 0;
lastKey = key;
buffer += keymap[keyIndex][pressCount];
} else {
pressCount++;
if (pressCount >= strlen(keymap[keyIndex])) {
pressCount = 0;
}
buffer.setCharAt(buffer.length() - 1, keymap[keyIndex][pressCount]);
}
lastPressTime = millis();
}
void checkTimeout() {
if (lastKey != '\0' && millis() - lastPressTime > 1500) {
confirmChar();
}
}
String getDisplayBuffer() {
if (lastKey != '\0') {
return buffer + "_";
}
return buffer;
}
String getFinalString() {
confirmChar();
return buffer;
}
private:
void confirmChar() {
lastKey = '\0';
pressCount = 0;
}
String buffer;
char lastKey = '\0';
int pressCount = 0;
unsigned long lastPressTime = 0;
const char* keymap[10];
};
// ------------------ DISPLAY MANAGER ------------------
class DisplayManager {
public:
DisplayManager() : display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1) {}
bool init();
void clear();
void showMessage(const String& line1, const String& line2 = "", int delay_ms = 0, uint8_t size1 = 2, uint8_t size2 = 1);
void showWelcomeMessage(const String& status, int delay_ms);
void showScrollMessage(const String& message, uint8_t textSize = 1);
private:
Adafruit_SSD1306 display;
uint16_t scrollOffset = 0;
unsigned long lastScrollTime = 0;
};
bool DisplayManager::init() {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
return false;
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.display();
return true;
}
void DisplayManager::clear() {
display.clearDisplay();
display.display();
scrollOffset = 0;
}
void DisplayManager::showMessage(const String& line1, const String& line2, int delay_ms, uint8_t size1, uint8_t size2) {
display.clearDisplay();
display.setTextSize(size1);
display.setCursor(0, 10);
display.println(line1);
if (line2 != "") {
display.setTextSize(size2);
display.setCursor(0, 40);
display.println(line2);
}
display.display();
scrollOffset = 0;
if (delay_ms > 0) {
delay(delay_ms);
}
}
void DisplayManager::showWelcomeMessage(const String& status, int delay_ms) {
display.clearDisplay();
display.setTextSize(2);
display.setCursor(10, 10);
display.println("Vending");
display.setCursor(10, 30);
display.println("Machine");
display.setTextSize(1);
display.setCursor(0, 55);
display.print(status);
display.display();
scrollOffset = 0;
if (delay_ms > 0) {
delay(delay_ms);
}
}
void DisplayManager::showScrollMessage(const String& message, uint8_t textSize) {
if (millis() - lastScrollTime > 100) {
display.clearDisplay();
display.setTextSize(textSize);
display.setCursor(0, 20);
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(message, 0, 0, &x1, &y1, &w, &h);
if (w > SCREEN_WIDTH) {
display.setCursor(-scrollOffset, 20);
display.println(message);
scrollOffset += 2;
if (scrollOffset > w + 10) {
scrollOffset = 0;
}
} else {
display.setCursor((SCREEN_WIDTH - w) / 2, 20);
display.println(message);
}
display.display();
lastScrollTime = millis();
}
}
// ------------------ KEYPAD MANAGER ------------------
class KeypadManager {
public:
void init();
char getKey();
private:
Keypad* customKeypad = nullptr;
unsigned long lastKeyTime = 0;
};
void KeypadManager::init() {
customKeypad = new Keypad(makeKeymap((char*)KEYPAD_KEYS), KEYPAD_ROW_PINS, KEYPAD_COL_PINS, KEYPAD_ROWS, KEYPAD_COLS);
customKeypad->setDebounceTime(DEBOUNCE_TIME_MS);
customKeypad->setHoldTime(1000);
}
char KeypadManager::getKey() {
if (!customKeypad) return 0;
char key = customKeypad->getKey();
if (key) {
lastKeyTime = millis();
}
return key;
}
// ------------------ LOADCELL (HX711) ------------------
// کد زیر را به طور کامل جایگزین کلاس Loadcell فعلی خود کنید
class Loadcell {
public:
// ۱. سازنده را برای دریافت پینها اصلاح میکنیم
Loadcell(uint8_t dout_pin, uint8_t sck_pin) : hx711(dout_pin, sck_pin) {}
bool init() {
// ۲. تابع begin بدون پارامتر فراخوانی میشود
hx711.begin();
// ۳. این کتابخانه تابع کالیبراسیون ندارد، پس این بخش را حذف یا کامنت میکنیم
// hx711.set_scale(LOADCELL_CALIBRATION_FACTOR); // این خط حذف میشود
return tare();
}
// بقیه توابع کلاس بدون تغییر باقی میمانند
float getStableValue(int samples = 5, int delayMs = 50) {
float total = 0;
for (int i = 0; i < samples; i++) {
total += hx711.getData();
delay(delayMs);
}
return total / samples;
}
bool tare() {
Serial.println("Taring load cell...");
unsigned long startTime = millis();
hx711.tareNoDelay();
while (!hx711.getTareStatus()) {
if (millis() - startTime > LOADCELL_TARE_TIMEOUT) {
Serial.println("Tare timeout!");
return false;
}
delay(10);
}
isTared = true;
Serial.println("Tare complete.");
return true;
}
bool isReady() {
return hx711.update();
}
void powerDown() {
hx711.powerDown();
}
void powerUp() {
hx711.powerUp();
}
private:
HX711_ADC hx711;
bool isTared = false;
};
// class Loadcell {
// public:
// Loadcell(uint8_t dout_pin, uint8_t sck_pin) : hx711(dout_pin, sck_pin) {}
// bool init();
// float getStableValue(int samples = 5, int delayMs = 50);
// bool isReady();
// bool tare();
// void powerDown();
// void powerUp();
// private:
// HX711_ADC hx711;
// bool isTared = false;
// };
// bool Loadcell::init() {
// if (!hx711.begin(HX711_DOUT_PIN, HX711_SCK_PIN)) {
// Serial.println("HX711 initialization failed");
// return false;
// }
// hx711.setGain(128);
// hx711.set_scale(LOADCELL_CALIBRATION_FACTOR);
// return tare();
// }
// float Loadcell::getStableValue(int samples, int delayMs) {
// float total = 0;
// for (int i = 0; i < samples; i++) {
// total += hx711.getData();
// delay(delayMs);
// }
// return total / samples;
// }
// bool Loadcell::tare() {
// Serial.println("Taring load cell...");
// unsigned long startTime = millis();
// hx711.tareNoDelay();
// while (!hx711.getTareStatus()) {
// if (millis() - startTime > LOADCELL_TARE_TIMEOUT) {
// Serial.println("Tare timeout!");
// return false;
// }
// delay(10);
// }
// isTared = true;
// Serial.println("Tare complete.");
// return true;
// }
// bool Loadcell::isReady() {
// return hx711.update();
// }
// void Loadcell::powerDown() {
// hx711.powerDown();
// }
// void Loadcell::powerUp() {
// hx711.powerUp();
// }
// ------------------ NETWORK MANAGER ------------------
class VendingNetworkManager {
public:
VendingNetworkManager() : timeClient(ntpUDP) {}
void init();
bool isConnected();
bool syncTime();
bool sendTransaction(const Transaction& transaction);
String getDateTimeString();
void setServerURL(const char* url);
String getServerURL();
private:
WiFiUDP ntpUDP;
NTPClient timeClient;
String serverURL;
const char* ntpServer = "pool.ntp.org";
const int timeZone = 3.5 * 3600;
String sha256(const char *str) {
byte hash[32];
mbedtls_md_context_t ctx;
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
mbedtls_md_starts(&ctx);
mbedtls_md_update(&ctx, (const unsigned char *)str, strlen(str));
mbedtls_md_finish(&ctx, hash);
mbedtls_md_free(&ctx);
char hexResult[65];
for (int i = 0; i < 32; i++) {
sprintf(hexResult + i * 2, "%02x", hash[i]);
}
return String(hexResult);
}
};
void VendingNetworkManager::init() {
WiFiManager wifiManager;
if (!wifiManager.autoConnect("VendingMachineSetup")) {
Serial.println("Failed to connect and hit timeout");
delay(3000);
ESP.restart();
delay(5000);
}
Serial.println("Connected to WiFi!");
timeClient = NTPClient(ntpUDP, ntpServer, timeZone, NTP_UPDATE_INTERVAL);
timeClient.begin();
syncTime();
}
bool VendingNetworkManager::isConnected() {
return WiFi.status() == WL_CONNECTED;
}
bool VendingNetworkManager::syncTime() {
if (!isConnected()) return false;
if (timeClient.update()) {
Serial.println("Time synchronized: " + timeClient.getFormattedTime());
return true;
}
return false;
}
bool VendingNetworkManager::sendTransaction(const Transaction& transaction) {
if (!isConnected()) {
Serial.println("Cannot send transaction - WiFi not connected");
return false;
}
WiFiClientSecure *client = new WiFiClientSecure;
client->setCACert(root_ca_cert);
HTTPClient http;
http.begin(*client, serverURL);
http.addHeader("Content-Type", "application/json");
DynamicJsonDocument doc(1024);
doc["productCode"] = transaction.productCode;
doc["productName"] = transaction.productName;
doc["price"] = transaction.price;
doc["datetime"] = transaction.datetime;
doc["success"] = transaction.success;
String jsonString;
serializeJson(doc, jsonString);
int httpResponseCode = http.POST(jsonString);
bool success = (httpResponseCode == 200);
if (success) {
Serial.println("Transaction sent successfully");
} else {
Serial.printf("Failed to send transaction. Error: %s\n", http.errorToString(httpResponseCode).c_str());
}
http.end();
delete client;
return success;
}
String VendingNetworkManager::getDateTimeString() {
if (!isConnected()) {
return "0000-00-00 00:00:00";
}
timeClient.update();
String formattedTime = timeClient.getFormattedTime();
time_t epochTime = timeClient.getEpochTime();
struct tm *ptm = gmtime((time_t *)&epochTime);
char dateString[20];
sprintf(dateString, "%04d-%02d-%02d %s",
ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday,
formattedTime.c_str());
return String(dateString);
}
void VendingNetworkManager::setServerURL(const char* url) {
serverURL = url;
}
String VendingNetworkManager::getServerURL() {
return serverURL;
}
// ------------------ DATABASE (SPIFFS + ArduinoJson) ------------------
class Database {
public:
bool init(DisplayManager* display);
bool loadProducts();
bool saveProducts();
bool loadHistory();
bool saveHistory();
Product* findProductByCode(const char* code);
bool addOrUpdateProduct(const Product& newProduct);
bool deleteProduct(const char* code);
int getProductCount();
Product* getProductByIndex(int index);
bool addTransaction(const Transaction& transaction);
int getHistoryCount();
Transaction* getHistory(int& count);
void clearHistory();
bool createBackup();
bool restoreBackup();
Product products[MAX_PRODUCTS];
private:
const char* products_path = "/products.json";
const char* history_path = "/history.json";
const char* backup_path = "/backup/products.bak";
Transaction history[MAX_HISTORY];
int historyCount = 0;
DisplayManager* _display;
bool validateProduct(const Product& product) {
if (strlen(product.code) != PRODUCT_CODE_LENGTH) return false;
if (strlen(product.name) == 0) return false;
if (product.price == 0) return false;
if (product.driverIndex < 1 || product.driverIndex > 40) return false;
if (product.speedLevel < 1 || product.speedLevel > 3) return false;
return true;
}
};
bool Database::init(DisplayManager* display) {
_display = display;
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Mount Failed");
return false;
}
if (!SPIFFS.exists("/backup")) {
SPIFFS.mkdir("/backup");
}
if (!SPIFFS.exists(products_path)) {
if (!saveProducts()) return false;
}
if (!SPIFFS.exists(history_path)) {
if (!saveHistory()) return false;
}
return loadProducts() && loadHistory();
}
bool Database::loadProducts() {
for(int i = 0; i < MAX_PRODUCTS; ++i) {
products[i].isValid = false;
}
File file = SPIFFS.open(products_path, FILE_READ);
if (!file) {
Serial.println("Failed to open products file for reading");
return false;
}
DynamicJsonDocument doc(DOC_PRODUCTS_SIZE);
if (deserializeJson(doc, file)) {
file.close();
return false;
}
file.close();
JsonArray array = doc.as<JsonArray>();
int i = 0;
for (JsonVariant v : array) {
if (i < MAX_PRODUCTS) {
products[i].isValid = true;
strlcpy(products[i].code, v["code"] | "", sizeof(products[i].code));
strlcpy(products[i].name, v["name"] | "", sizeof(products[i].name));
products[i].price = v["price"] | 0;
products[i].driverIndex = v["driverIndex"] | 0;
products[i].speedLevel = v["speedLevel"] | 2;
products[i].stock = v["stock"] | 0;
i++;
}
}
Serial.printf("Loaded %d products.\n", i);
return true;
}
bool Database::saveProducts() {
const char* temp_path = "/products.tmp";
File file = SPIFFS.open(temp_path, FILE_WRITE);
if (!file) {
Serial.println("Failed to open temp products file for writing");
_display->showMessage("ERROR", "Storage Write Fail", 2000);
return false;
}
DynamicJsonDocument doc(DOC_PRODUCTS_SIZE);
JsonArray array = doc.to<JsonArray>();
for (int i = 0; i < MAX_PRODUCTS; ++i) {
if (products[i].isValid) {
JsonObject obj = array.createNestedObject();
obj["code"] = products[i].code;
obj["name"] = products[i].name;
obj["price"] = products[i].price;
obj["driverIndex"] = products[i].driverIndex;
obj["speedLevel"] = products[i].speedLevel;
obj["stock"] = products[i].stock;
}
}
if (serializeJson(doc, file) == 0) {
Serial.println("Failed to write to temp products file");
_display->showMessage("ERROR", "Storage Full?", 2000);
file.close();
SPIFFS.remove(temp_path);
return false;
}
file.close();
if (SPIFFS.exists(products_path)) {
SPIFFS.remove(products_path);
}
if (!SPIFFS.rename(temp_path, products_path)) {
Serial.println("Failed to rename temp file");
_display->showMessage("ERROR", "Storage Rename Fail", 2000);
return false;
}
Serial.println("Products saved successfully and atomically.");
return true;
}
bool Database::saveHistory() {
const char* temp_path = "/history.tmp";
File file = SPIFFS.open(temp_path, FILE_WRITE);
if (!file) {
Serial.println("Failed to open temp history file for writing");
return false;
}
DynamicJsonDocument doc(DOC_HISTORY_SIZE);
JsonArray array = doc.to<JsonArray>();
for (int i = 0; i < historyCount; ++i) {
JsonObject obj = array.createNestedObject();
obj["productCode"] = history[i].productCode;
obj["productName"] = history[i].productName;
obj["price"] = history[i].price;
obj["datetime"] = history[i].datetime;
obj["success"] = history[i].success;
}
if (serializeJson(doc, file) == 0) {
Serial.println("Failed to write to temp history file");
file.close();
SPIFFS.remove(temp_path);
return false;
}
file.close();
if (SPIFFS.exists(history_path)) {
SPIFFS.remove(history_path);
}
if (!SPIFFS.rename(temp_path, history_path)) {
Serial.println("Failed to rename temp history file");
return false;
}
Serial.println("History saved successfully and atomically.");
return true;
}
bool Database::loadHistory() {
historyCount = 0;
File file = SPIFFS.open(history_path, FILE_READ);
if (!file) {
Serial.println("Failed to open history file for reading");
return false;
}
DynamicJsonDocument doc(DOC_HISTORY_SIZE);
if (deserializeJson(doc, file)) {
file.close();
return false;
}
file.close();
JsonArray array = doc.as<JsonArray>();
for (JsonVariant v : array) {
if (historyCount < MAX_HISTORY) {
strlcpy(history[historyCount].productCode, v["productCode"] | "", sizeof(history[historyCount].productCode));
strlcpy(history[historyCount].productName, v["productName"] | "", sizeof(history[historyCount].productName));
history[historyCount].price = v["price"] | 0;
strlcpy(history[historyCount].datetime, v["datetime"] | "", sizeof(history[historyCount].datetime));
history[historyCount].success = v["success"] | false;
historyCount++;
}
}
Serial.printf("Loaded %d history records.\n", historyCount);
return true;
}
bool Database::createBackup() {
File backupFile = SPIFFS.open(backup_path, FILE_WRITE);
if (!backupFile) {
Serial.println("Failed to create backup file");
return false;
}
File originalFile = SPIFFS.open(products_path, FILE_READ);
if (!originalFile) {
backupFile.close();
return false;
}
while (originalFile.available()) {
backupFile.write(originalFile.read());
}
backupFile.close();
originalFile.close();
Serial.println("Backup created successfully.");
return true;
}
bool Database::restoreBackup() {
if (!SPIFFS.exists(backup_path)) {
Serial.println("No backup file found");
return false;
}
File backupFile = SPIFFS.open(backup_path, FILE_READ);
if (!backupFile) {
Serial.println("Failed to open backup file");
return false;
}
File originalFile = SPIFFS.open(products_path, FILE_WRITE);
if (!originalFile) {
backupFile.close();
return false;
}
while (backupFile.available()) {
originalFile.write(backupFile.read());
}
backupFile.close();
originalFile.close();
Serial.println("Backup restored successfully.");
return true;
}
bool Database::addTransaction(const Transaction& transaction) {
if (historyCount >= MAX_HISTORY) {
for (int i = 0; i < MAX_HISTORY - 1; i++) {
history[i] = history[i + 1];
}
historyCount = MAX_HISTORY - 1;
}
history[historyCount] = transaction;
historyCount++;
return saveHistory();
}
int Database::getHistoryCount() {
return historyCount;
}
Transaction* Database::getHistory(int& count) {
count = historyCount;
return history;
}
void Database::clearHistory() {
historyCount = 0;
saveHistory();
}
Product* Database::findProductByCode(const char* code) {
for (int i = 0; i < MAX_PRODUCTS; ++i) {
if (products[i].isValid && strcmp(products[i].code, code) == 0) {
return &products[i];
}
}
return nullptr;
}
bool Database::addOrUpdateProduct(const Product& newProduct) {
if (!validateProduct(newProduct)) {
Serial.println("Invalid product data");
return false;
}
Product* existingProduct = findProductByCode(newProduct.code);
if (existingProduct) {
*existingProduct = newProduct;
} else {
bool added = false;
for (int i = 0; i < MAX_PRODUCTS; ++i) {
if (!products[i].isValid) {
products[i] = newProduct;
added = true;
break;
}
}
if (!added) {
Serial.println("No space for new product");
return false;
}
}
return saveProducts();
}
bool Database::deleteProduct(const char* code) {
Product* product = findProductByCode(code);
if (product) {
product->isValid = false;
return saveProducts();
}
return false;
}
int Database::getProductCount() {
int count = 0;
for (int i = 0; i < MAX_PRODUCTS; ++i) {
if (products[i].isValid) count++;
}
return count;
}
Product* Database::getProductByIndex(int index) {
int count = 0;
for (int i = 0; i < MAX_PRODUCTS; ++i) {
if (products[i].isValid) {
if (count == index) return &products[i];
count++;
}
}
return nullptr;
}
// ------------------ MOTOR CONTROL (معماری جدید) ------------------
class MotorControl {
public:
bool init(Loadcell* loadcell, KeypadManager* keypad);
bool dispenseProduct(uint8_t driverIndex, uint8_t speedLevel);
bool testMotor(uint8_t driverIndex, uint8_t speedLevel, uint16_t steps = 100);
void emergencyStop();
void setMotorSpeed(uint8_t speedLevel);
void calculatePowerRequirements();
private:
Loadcell* _loadcell = nullptr;
KeypadManager* _keypad = nullptr;
bool isMotorBusy = false;
uint8_t currentSpeed = SPEED_LEVEL_2;
// بافر برای وضعیت Enable درایورها
byte enableStates[NUM_SHIFT_REGISTERS] = {0};
void updateShiftRegisters();
void setDriverEnable(uint8_t driverIndex, bool enable);
void disableAllDrivers();
uint16_t getStepDelay();
};
bool MotorControl::init(Loadcell* loadcell, KeypadManager* keypad) {
_loadcell = loadcell;
_keypad = keypad;
// راهاندازی پینهای شیفت رجیستر
pinMode(SHIFT_REG_DATA_PIN, OUTPUT);
pinMode(SHIFT_REG_CLOCK_PIN, OUTPUT);
pinMode(SHIFT_REG_LATCH_PIN, OUTPUT);
// راهاندازی پینهای مشترک
pinMode(COMMON_STEP_PIN, OUTPUT);
pinMode(COMMON_DIR_PIN, OUTPUT);
// غیرفعال کردن همه درایورها در ابتدا
emergencyStop();
Serial.println("MotorControl with 40 independent drivers Initialized.");
return true;
}
void MotorControl::updateShiftRegisters() {
digitalWrite(SHIFT_REG_LATCH_PIN, LOW);
for (int i = NUM_SHIFT_REGISTERS - 1; i >= 0; i--) {
shiftOut(SHIFT_REG_DATA_PIN, SHIFT_REG_CLOCK_PIN, MSBFIRST, enableStates[i]);
}
digitalWrite(SHIFT_REG_LATCH_PIN, HIGH);
}
void MotorControl::setDriverEnable(uint8_t driverIndex, bool enable) {
if (driverIndex < 1 || driverIndex > NUM_DRIVERS) return;
int byteIndex = (driverIndex - 1) / 8;
int bitIndex = (driverIndex - 1) % 8;
if (enable) {
bitSet(enableStates[byteIndex], bitIndex);
} else {
bitClear(enableStates[byteIndex], bitIndex);
}
updateShiftRegisters();
}
void MotorControl::disableAllDrivers() {
for (int i = 0; i < NUM_SHIFT_REGISTERS; i++) {
enableStates[i] = 0;
}
updateShiftRegisters();
}
uint16_t MotorControl::getStepDelay() {
switch(currentSpeed) {
case 1: return SPEED_LEVEL_1;
case 2: return SPEED_LEVEL_2;
case 3: return SPEED_LEVEL_3;
default: return SPEED_LEVEL_2;
}
}
void MotorControl::setMotorSpeed(uint8_t speedLevel) {
if (speedLevel >= 1 && speedLevel <= 3) {
currentSpeed = speedLevel;
}
}
bool MotorControl::dispenseProduct(uint8_t driverIndex, uint8_t speedLevel) {
if (isMotorBusy) return false;
isMotorBusy = true;
setMotorSpeed(speedLevel);
uint16_t stepDelay = getStepDelay();
float initialWeight = _loadcell->getStableValue(3, 100);
Serial.printf("Initial weight: %.2f, Speed: %d\n", initialWeight, speedLevel);
disableAllDrivers();
setDriverEnable(driverIndex, true);
delay(50);
digitalWrite(COMMON_DIR_PIN, HIGH);
delay(20);
unsigned long start = millis();
bool productDropped = false;
for (int i = 0; i < MOTOR_STEPS_PER_REV; i++) {
digitalWrite(COMMON_STEP_PIN, HIGH);
delayMicroseconds(stepDelay);
digitalWrite(COMMON_STEP_PIN, LOW);
delayMicroseconds(stepDelay);
if (i % 20 == 0) {
if (_loadcell && _loadcell->isReady()) {
float currentWeight = _loadcell->getStableValue(2, 20);
if (currentWeight - initialWeight > LOADCELL_THRESHOLD) {
Serial.println("Item dropped, weight detected.");
productDropped = true;
break;
}
}
}
if (millis() - start > DISPENSE_TIMEOUT_MS) {
Serial.println("Dispense timeout!");
break;
}
if (_keypad->getKey() == 'D') {
Serial.println("Emergency stop!");
break;
}
}
setDriverEnable(driverIndex, false);
digitalWrite(COMMON_DIR_PIN, LOW);
isMotorBusy = false;
delay(500);
return productDropped;
}
bool MotorControl::testMotor(uint8_t driverIndex, uint8_t speedLevel, uint16_t steps) {
if (isMotorBusy) return false;
isMotorBusy = true;
setMotorSpeed(speedLevel);
uint16_t stepDelay = getStepDelay();
disableAllDrivers();
setDriverEnable(driverIndex, true);
delay(50);
digitalWrite(COMMON_DIR_PIN, HIGH);
delay(20);
for (int i = 0; i < steps; i++) {
digitalWrite(COMMON_STEP_PIN, HIGH);
delayMicroseconds(stepDelay);
digitalWrite(COMMON_STEP_PIN, LOW);
delayMicroseconds(stepDelay);
if (_keypad->getKey() == 'D') {
break;
}
}
setDriverEnable(driverIndex, false);
digitalWrite(COMMON_DIR_PIN, LOW);
isMotorBusy = false;
return true;
}
void MotorControl::emergencyStop() {
disableAllDrivers();
digitalWrite(COMMON_STEP_PIN, LOW);
digitalWrite(COMMON_DIR_PIN, LOW);
isMotorBusy = false;
Serial.println("EMERGENCY STOP - ALL DRIVERS DISABLED");
}
void MotorControl::calculatePowerRequirements() {
float max_current_per_motor = 2.0;
float operating_voltage = 12.0;
float max_power = operating_voltage * max_current_per_motor;
Serial.printf("Max power required per motor: %.2fW\n", max_power);
Serial.printf("Power supply should be rated for at least: %.2fA at %.2fV\n",
max_current_per_motor, operating_voltage);
}
// ------------------ USER FLOW ------------------
class UserFlow {
public:
UserFlow(DisplayManager* d, KeypadManager* k, Database* db, MotorControl* mc, VendingNetworkManager* nm);
void init();
void handleInput(char key);
void update();
bool shouldEnterAdminMode();
void resetAdminFlag();
void checkInactivity();
private:
enum State { IDLE, GETTING_CODE, GETTING_ADMIN_PASSWORD, CONFIRM_PURCHASE, PROCESSING_PAYMENT, DISPENSING, SHOW_RESULT, OUT_OF_STOCK };
State state;
String inputBuffer;
bool enterAdmin = false;
Product* selectedProduct = nullptr;
unsigned long lastActivityTime = 0;
unsigned long paymentStartTime = 0;
DisplayManager* display;
KeypadManager* keypad;
Database* database;
MotorControl* motorControl;
VendingNetworkManager* network;
void resetToIdle();
void recordTransaction(bool success);
};
UserFlow::UserFlow(DisplayManager* d, KeypadManager* k, Database* db, MotorControl* mc, VendingNetworkManager* nm) {
display = d;
keypad = k;
database = db;
motorControl = mc;
network = nm;
}
void UserFlow::init() {
state = IDLE;
inputBuffer = "";
lastActivityTime = millis();
display->showMessage("Enter Code:", "");
}
void UserFlow::resetToIdle() {
state = IDLE;
inputBuffer = "";
selectedProduct = nullptr;
lastActivityTime = millis();
display->showMessage("Enter Code:", "");
}
bool UserFlow::shouldEnterAdminMode() {
return enterAdmin;
}
void UserFlow::resetAdminFlag() {
enterAdmin = false;
}
void UserFlow::recordTransaction(bool success) {
Transaction transaction;
if (selectedProduct) {
strlcpy(transaction.productCode, selectedProduct->code, sizeof(transaction.productCode));
strlcpy(transaction.productName, selectedProduct->name, sizeof(transaction.productName));
transaction.price = selectedProduct->price;
if (success) {
selectedProduct->stock--;
database->saveProducts();
}
} else {
strlcpy(transaction.productCode, inputBuffer.c_str(), sizeof(transaction.productCode));
strlcpy(transaction.productName, "Unknown", sizeof(transaction.productName));
transaction.price = 0;
}
String datetime = network->getDateTimeString();
strlcpy(transaction.datetime, datetime.c_str(), sizeof(transaction.datetime));
transaction.success = success;
xQueueSend(transactionQueue, &transaction, portMAX_DELAY);
database->addTransaction(transaction);
}
void UserFlow::checkInactivity() {
if (state != IDLE && millis() - lastActivityTime > INACTIVITY_TIMEOUT_MS) {
display->showMessage("Timeout", "Returning to main", 2000);
resetToIdle();
}
}
void UserFlow::handleInput(char key) {
lastActivityTime = millis();
// میتوانید این کد را به هر مقدار ثابت دیگری تغییر دهید
const String ADMIN_TRIGGER_CODE = "*0*0";
switch (state) {
case IDLE:
// اگر دستگاه در حالت بیکار است، با فشردن هر کلیدی به حالت دریافت کد میرود
if (key) {
state = GETTING_CODE;
inputBuffer = String(key);
display->showMessage("Enter Code:", inputBuffer);
}
break;
case GETTING_CODE:
// حالت عادی برای ورود کد محصول یا کد راهانداز ادمین
if (key == '#') {
// ۱. چک کردن کد ثابت برای ورود به حالت ادمین
if (inputBuffer == ADMIN_TRIGGER_CODE) {
state = GETTING_ADMIN_PASSWORD;
inputBuffer = "";
display->showMessage("Enter Password:", "_");
} else {
// ۲. اگر کد ادمین نبود، پس کد محصول است
selectedProduct = database->findProductByCode(inputBuffer.c_str());
if (selectedProduct) {
if (selectedProduct->stock > 0) {
state = CONFIRM_PURCHASE;
char priceStr[20];
snprintf(priceStr, sizeof(priceStr), "Price: %d", selectedProduct->price);
display->showMessage(selectedProduct->name, priceStr);
} else {
state = OUT_OF_STOCK;
display->showMessage("Out of Stock", "", 2000);
resetToIdle();
}
} else {
display->showMessage("Invalid Code", "", 2000);
resetToIdle();
}
}
} else if (key == 'D') {
resetToIdle();
} else if (key == '*' && inputBuffer.length() > 0) {
inputBuffer.remove(inputBuffer.length() - 1);
display->showMessage("Enter Code:", inputBuffer);
} else if (key != '*' && key != 'D' && key != '#') {
if (inputBuffer.length() < 10) {
inputBuffer += key;
display->showMessage("Enter Code:", inputBuffer);
}
}
break;
case GETTING_ADMIN_PASSWORD:
// حالت جدید برای دریافت پسورد
if (key == '#') {
if (inputBuffer == config.adminCode) {
display->showMessage("Access Granted", "...", 1000);
enterAdmin = true; // فلگ برای ورود به منوی ادمین در حلقه اصلی
resetToIdle();
} else {
display->showMessage("Wrong Password!", "", 2000);
resetToIdle();
}
} else if (key == 'D') {
resetToIdle();
} else if (key == '*' && inputBuffer.length() > 0) {
inputBuffer.remove(inputBuffer.length() - 1);
String stars = "";
for(int i=0; i<inputBuffer.length(); i++) stars += "*";
display->showMessage("Enter Password:", stars + "_");
} else if (isdigit(key) && inputBuffer.length() < 10) { // فقط ارقام برای پسورد
inputBuffer += key;
String stars = "";
for(int i=0; i<inputBuffer.length(); i++) stars += "*";
display->showMessage("Enter Password:", stars + "_");
}
break;
case CONFIRM_PURCHASE:
if (key == '#') {
state = PROCESSING_PAYMENT;
paymentStartTime = millis();
display->showMessage("Processing...", "Please wait");
} else if (key == 'D' || key == '*') {
resetToIdle();
}
break;
// در این حالتها ورودی کاربر نادیده گرفته میشود
case OUT_OF_STOCK:
case PROCESSING_PAYMENT:
case DISPENSING:
case SHOW_RESULT:
break;
}
}
void UserFlow::update() {
checkInactivity();
if (state == PROCESSING_PAYMENT) {
if (millis() - paymentStartTime > 3000) {
state = DISPENSING;
display->showMessage("Dispensing...", "Please wait");
bool success = motorControl->dispenseProduct(
selectedProduct->driverIndex,
selectedProduct->speedLevel
);
state = SHOW_RESULT;
if (success) {
display->showMessage("Thank You!", "Enjoy!", 3000);
recordTransaction(true);
} else {
display->showMessage("Failed!", "Try again later", 3000);
recordTransaction(false);
}
resetToIdle();
}
}
}
// ------------------ ADMIN MENU ------------------
class AdminMenu {
public:
AdminMenu(DisplayManager* d, KeypadManager* k, Database* db, MotorControl* mc, Loadcell* lc, VendingNetworkManager* nm, TextInputHelper* ti);
void start();
void handleInput(char key);
void update();
bool isActive();
void exit();
private:
enum AdminState {
ADMIN_IDLE,
ADMIN_MAIN_MENU,
ADMIN_VIEW_PRODUCTS,
ADMIN_ADD_PRODUCT,
ADMIN_EDIT_PRODUCT,
ADMIN_DELETE_PRODUCT,
ADMIN_SET_STOCK,
ADMIN_SET_SPEED,
ADMIN_VIEW_HISTORY,
ADMIN_CLEAR_HISTORY,
ADMIN_NETWORK_SETUP,
ADMIN_TEST_MOTOR,
ADMIN_CALIBRATE_LOADCELL,
ADMIN_INPUT_TEXT,
ADMIN_CONFIRM_ACTION,
ADMIN_SET_PASSWORD
};
AdminState state;
int menuIndex;
int productIndex;
int historyIndex;
int currentPage;
int itemsPerPage;
Product editingProduct;
TextInputHelper* textInput;
String inputBuffer;
String statusMessage;
unsigned long messageDisplayTime;
int inputStep;
String tempInput;
DisplayManager* display;
KeypadManager* keypad;
Database* database;
MotorControl* motorControl;
Loadcell* loadcell;
VendingNetworkManager* network;
void showMainMenu();
void showProducts();
void showProductDetails(Product* product);
void showHistory();
void addProduct();
void editProduct();
void deleteProduct();
void setStock();
void setSpeed();
void networkSetup();
void testMotor();
void calibrateLoadcell();
void clearHistory();
void handleTextInput(char key);
void handleStockInput(char key);
void handleSpeedInput(char key);
void showConfirmationDialog(const String& message);
void showStatusMessage(const String& message, int duration = 2000);
};
AdminMenu::AdminMenu(DisplayManager* d, KeypadManager* k, Database* db, MotorControl* mc, Loadcell* lc, VendingNetworkManager* nm, TextInputHelper* ti) {
display = d;
keypad = k;
database = db;
motorControl = mc;
loadcell = lc;
network = nm;
textInput = ti;
itemsPerPage = 3;
}
void AdminMenu::start() {
state = ADMIN_MAIN_MENU;
menuIndex = 0;
productIndex = 0;
historyIndex = 0;
currentPage = 0;
statusMessage = "";
showMainMenu();
}
void AdminMenu::handleInput(char key) {
if (state == ADMIN_INPUT_TEXT) {
handleTextInput(key);
return;
}
if (state == ADMIN_SET_STOCK) {
handleStockInput(key);
return;
}
if (state == ADMIN_SET_SPEED) {
handleSpeedInput(key);
return;
}
if (state == ADMIN_CONFIRM_ACTION) {
if (key == '#') {
if (menuIndex == 8) {
clearHistory();
} else if (menuIndex == 5) {
Product* product = database->getProductByIndex(productIndex);
if (product) {
database->deleteProduct(product->code);
showStatusMessage("Product deleted");
}
}
state = ADMIN_MAIN_MENU;
showMainMenu();
} else if (key == '*' || key == 'D') {
state = ADMIN_MAIN_MENU;
showMainMenu();
}
return;
}
switch (state) {
case ADMIN_MAIN_MENU:
if (key == '2') {
menuIndex = (menuIndex + 1) % 11;
showMainMenu();
} else if (key == '8') {
menuIndex = (menuIndex == 0) ? 10 : menuIndex - 1;
showMainMenu();
} else if (key == '#') {
switch (menuIndex) {
case 0:
state = ADMIN_VIEW_PRODUCTS;
productIndex = 0;
currentPage = 0;
showProducts();
break;
case 1:
state = ADMIN_ADD_PRODUCT;
memset(&editingProduct, 0, sizeof(Product));
editingProduct.isValid = true;
editingProduct.speedLevel = 2;
textInput->start();
inputBuffer = "";
display->showMessage("Enter Product Code:", textInput->getDisplayBuffer());
break;
case 2:
state = ADMIN_VIEW_PRODUCTS;
productIndex = 0;
currentPage = 0;
showProducts();
break;
case 3:
state = ADMIN_VIEW_PRODUCTS;
productIndex = 0;
currentPage = 0;
showProducts();
break;
case 4:
state = ADMIN_VIEW_PRODUCTS;
productIndex = 0;
currentPage = 0;
showProducts();
break;
case 5:
state = ADMIN_VIEW_PRODUCTS;
productIndex = 0;
currentPage = 0;
showProducts();
break;
case 6:
state = ADMIN_VIEW_HISTORY;
historyIndex = 0;
currentPage = 0;
showHistory();
break;
case 7:
state = ADMIN_CONFIRM_ACTION;
showConfirmationDialog("Clear all history?");
break;
case 8:
state = ADMIN_NETWORK_SETUP;
networkSetup();
break;
case 9:
state = ADMIN_TEST_MOTOR;
testMotor();
break;
case 10:
state = ADMIN_CALIBRATE_LOADCELL;
calibrateLoadcell();
break;
}
} else if (key == 'D') {
exit();
}
break;
case ADMIN_VIEW_PRODUCTS:
if (key == '2') {
productIndex = (productIndex + 1) % database->getProductCount();
showProducts();
} else if (key == '8') {
productIndex = (productIndex == 0) ? database->getProductCount() - 1 : productIndex - 1;
showProducts();
} else if (key == '#') {
Product* product = database->getProductByIndex(productIndex);
if (product) {
if (menuIndex == 2) {
state = ADMIN_EDIT_PRODUCT;
editingProduct = *product;
textInput->start(editingProduct.name);
inputBuffer = editingProduct.name;
display->showMessage("Edit Name:", textInput->getDisplayBuffer());
} else if (menuIndex == 3) {
state = ADMIN_CONFIRM_ACTION;
showConfirmationDialog("Delete " + String(product->name) + "?");
} else if (menuIndex == 4) {
setStock();
} else if (menuIndex == 5) {
setSpeed();
} else {
showProductDetails(product);
}
}
} else if (key == '*') {
currentPage = (currentPage + 1) % ((database->getProductCount() + itemsPerPage - 1) / itemsPerPage);
showProducts();
} else if (key == 'D') {
state = ADMIN_MAIN_MENU;
showMainMenu();
}
break;
case ADMIN_VIEW_HISTORY:
if (key == '2') {
historyIndex = min(historyIndex + 1, database->getHistoryCount() - 1);
showHistory();
} else if (key == '8') {
historyIndex = max(historyIndex - 1, 0);
showHistory();
} else if (key == '*') {
int historyCount = database->getHistoryCount();
currentPage = (currentPage + 1) % ((historyCount + itemsPerPage - 1) / itemsPerPage);
showHistory();
} else if (key == 'D') {
state = ADMIN_MAIN_MENU;
showMainMenu();
}
break;
case ADMIN_TEST_MOTOR:
if (key == 'D') {
state = ADMIN_MAIN_MENU;
showMainMenu();
} else if (key == '#') {
motorControl->testMotor(1, 2, 100);
showStatusMessage("Motor test started");
}
break;
case ADMIN_CALIBRATE_LOADCELL:
if (key == 'D') {
state = ADMIN_MAIN_MENU;
showMainMenu();
} else if (key == '#') {
loadcell->tare();
showStatusMessage("Loadcell calibrated");
}
break;
case ADMIN_NETWORK_SETUP:
if (key == 'D') {
state = ADMIN_MAIN_MENU;
showMainMenu();
} else if (key == '#') {
network->init();
showStatusMessage("Network reconnecting");
}
break;
default:
break;
}
}
void AdminMenu::handleTextInput(char key) {
textInput->handleKey(key);
if (state == ADMIN_ADD_PRODUCT || state == ADMIN_EDIT_PRODUCT) {
if (inputBuffer.length() < PRODUCT_CODE_LENGTH && state == ADMIN_ADD_PRODUCT) {
display->showMessage("Enter Product Code:", textInput->getDisplayBuffer());
} else if (inputBuffer.length() == PRODUCT_CODE_LENGTH && state == ADMIN_ADD_PRODUCT) {
strncpy(editingProduct.code, textInput->getFinalString().c_str(), PRODUCT_CODE_LENGTH);
textInput->start();
display->showMessage("Enter Product Name:", textInput->getDisplayBuffer());
} else if (key == '#') {
String finalText = textInput->getFinalString();
if (state == ADMIN_ADD_PRODUCT) {
if (inputBuffer.length() < PRODUCT_CODE_LENGTH) {
inputBuffer = finalText;
} else {
strncpy(editingProduct.name, finalText.c_str(), PRODUCT_NAME_LENGTH);
state = ADMIN_MAIN_MENU;
if (database->addOrUpdateProduct(editingProduct)) {
showStatusMessage("Product added");
} else {
showStatusMessage("Error adding product");
}
showMainMenu();
}
} else if (state == ADMIN_EDIT_PRODUCT) {
strncpy(editingProduct.name, finalText.c_str(), PRODUCT_NAME_LENGTH);
state = ADMIN_MAIN_MENU;
if (database->addOrUpdateProduct(editingProduct)) {
showStatusMessage("Product updated");
} else {
showStatusMessage("Error updating");
}
showMainMenu();
}
} else if (key == 'D') {
state = ADMIN_MAIN_MENU;
showMainMenu();
}
}
}
void AdminMenu::handleStockInput(char key) {
if (key == '#') {
if (tempInput.length() > 0) {
int stock = tempInput.toInt();
editingProduct.stock = stock;
if (database->addOrUpdateProduct(editingProduct)) {
showStatusMessage("Stock updated: " + String(stock));
} else {
showStatusMessage("Error updating stock");
}
}
state = ADMIN_VIEW_PRODUCTS;
showProducts();
}
else if (key == '*') {
if (tempInput.length() > 0) {
tempInput.remove(tempInput.length() - 1);
display->showMessage("Enter stock:", tempInput);
}
}
else if (isdigit(key)) {
if (tempInput.length() < 4) {
tempInput += key;
display->showMessage("Enter stock:", tempInput);
}
}
else if (key == 'D') {
state = ADMIN_VIEW_PRODUCTS;
showProducts();
}
}
void AdminMenu::handleSpeedInput(char key) {
if (key == '1' || key == '2' || key == '3') {
int speedLevel = key - '0';
editingProduct.speedLevel = speedLevel;
if (database->addOrUpdateProduct(editingProduct)) {
showStatusMessage("Speed set to: " + String(speedLevel));
} else {
showStatusMessage("Error setting speed");
}
state = ADMIN_VIEW_PRODUCTS;
showProducts();
}
else if (key == 'D') {
state = ADMIN_VIEW_PRODUCTS;
showProducts();
}
}
void AdminMenu::update() {
if (state == ADMIN_INPUT_TEXT) {
textInput->checkTimeout();
if (state == ADMIN_ADD_PRODUCT) {
if (inputBuffer.length() < PRODUCT_CODE_LENGTH) {
display->showMessage("Enter Product Code:", textInput->getDisplayBuffer());
} else {
display->showMessage("Enter Product Name:", textInput->getDisplayBuffer());
}
} else if (state == ADMIN_EDIT_PRODUCT) {
display->showMessage("Edit Name:", textInput->getDisplayBuffer());
}
}
if (statusMessage != "" && millis() - messageDisplayTime > 2000) {
statusMessage = "";
if (state == ADMIN_MAIN_MENU) {
showMainMenu();
}
}
}
bool AdminMenu::isActive() {
return state != ADMIN_IDLE;
}
void AdminMenu::exit() {
state = ADMIN_IDLE;
display->showMessage("Exiting Admin", "Returning to main", 2000);
}
void AdminMenu::showMainMenu() {
const char* menuItems[] = {
"View Products",
"Add Product",
"Edit Product",
"Delete Product",
"Set Stock",
"Set Speed",
"View History",
"Clear History",
"Network Setup",
"Test Motor",
"Calibrate Loadcell"
};
String line2 = menuItems[menuIndex];
if (statusMessage != "") {
line2 = statusMessage;
}
display->showMessage("ADMIN MENU", line2);
}
void AdminMenu::showProducts() {
int productCount = database->getProductCount();
if (productCount == 0) {
display->showMessage("No Products", "Press D to return");
return;
}
Product* product = database->getProductByIndex(productIndex);
if (!product) {
display->showMessage("Error", "Product not found");
return;
}
String title = "Products " + String(productIndex + 1) + "/" + String(productCount);
String line2 = product->name;
if (menuIndex == 2) {
line2 = "Edit: " + line2;
} else if (menuIndex == 3) {
line2 = "Delete: " + line2;
} else if (menuIndex == 4) {
line2 = "Set Stock: " + line2;
} else if (menuIndex == 5) {
line2 = "Set Speed: " + line2;
}
display->showMessage(title, line2);
}
void AdminMenu::showProductDetails(Product* product) {
char details[64];
snprintf(details, sizeof(details), "Code: %s\nPrice: %d\nStock: %d\nMotor: %d\nSpeed: %d",
product->code, product->price, product->stock, product->driverIndex, product->speedLevel);
display->showMessage(product->name, details, 3000, 1, 1);
}
void AdminMenu::showHistory() {
int historyCount = database->getHistoryCount();
if (historyCount == 0) {
display->showMessage("No History", "Press D to return");
return;
}
int tempCount;
Transaction* history = database->getHistory(tempCount);
Transaction* transaction = &history[historyIndex];
String title = "History " + String(historyIndex + 1) + "/" + String(historyCount);
String line2 = String(transaction->productName) + " $" + String(transaction->price);
if (!transaction->success) {
line2 += " (Failed)";
}
display->showMessage(title, line2);
}
void AdminMenu::showConfirmationDialog(const String& message) {
display->showMessage("Confirm:", message, 0, 1, 1);
display->showScrollMessage("#: Confirm *: Cancel");
}
void AdminMenu::showStatusMessage(const String& message, int duration) {
statusMessage = message;
messageDisplayTime = millis();
showMainMenu();
}
void AdminMenu::setStock() {
Product* product = database->getProductByIndex(productIndex);
if (product) {
editingProduct = *product;
inputStep = 0;
tempInput = "";
state = ADMIN_SET_STOCK;
display->showMessage("Set stock for:", product->name);
delay(1000);
display->showMessage("Enter stock:", "0");
}
}
void AdminMenu::setSpeed() {
Product* product = database->getProductByIndex(productIndex);
if (product) {
editingProduct = *product;
state = ADMIN_SET_SPEED;
display->showMessage("Set speed for:", product->name);
delay(1000);
display->showMessage("Speed: 1=Slow 2=Med 3=Fast", "Current: " + String(product->speedLevel));
}
}
void AdminMenu::clearHistory() {
database->clearHistory();
showStatusMessage("History cleared");
}
void AdminMenu::networkSetup() {
display->showMessage("Network Setup", "#: Reconnect D: Back");
}
void AdminMenu::testMotor() {
display->showMessage("Test Motor", "#: Test Motor1 D: Back");
}
void AdminMenu::calibrateLoadcell() {
display->showMessage("Calibrate Loadcell", "#: Tare D: Back");
}
// ------------------ GLOBAL OBJECTS ------------------
DisplayManager display;
KeypadManager keypadManager;
Loadcell loadcell(HX711_DOUT_PIN, HX711_SCK_PIN);
Database db;
MotorControl motorControl;
VendingNetworkManager networkManager;
TextInputHelper textInput;
UserFlow userFlow(&display, &keypadManager, &db, &motorControl, &networkManager);
AdminMenu adminMenu(&display, &keypadManager, &db, &motorControl, &loadcell, &networkManager, &textInput);
Config config;
// ------------------ TASKS ------------------
void networkTask(void *pvParameters) {
Serial.println("Network task started.");
for (;;) {
if (!networkManager.isConnected()) {
Serial.println("Reconnecting to WiFi...");
networkManager.init();
}
networkManager.syncTime();
Transaction transaction;
while (xQueueReceive(transactionQueue, &transaction, 0) == pdTRUE) {
networkManager.sendTransaction(transaction);
}
db.createBackup();
vTaskDelay(pdMS_TO_TICKS(30000));
}
}
void uiTask(void *pvParameters) {
Serial.println("UI task started.");
for (;;) {
char key = keypadManager.getKey();
if (adminMenu.isActive()) {
adminMenu.handleInput(key);
adminMenu.update();
} else {
if (key) {
Serial.printf("Key pressed: %c\n", key);
userFlow.handleInput(key);
}
userFlow.update();
if (userFlow.shouldEnterAdminMode()) {
adminMenu.start();
userFlow.resetAdminFlag();
}
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
// ------------------ CONFIGURATION ------------------
void loadConfiguration() {
File configFile = SPIFFS.open("/config.json", "r");
if (!configFile) {
Serial.println("Failed to open config file. Using defaults.");
return;
}
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, configFile);
configFile.close();
if (error) {
Serial.println("Failed to parse config file. Using defaults.");
return;
}
config.serverURL = doc["server_url"] | DEFAULT_SERVER_URL;
config.adminCode = doc["admin_code"] | DEFAULT_ADMIN_CODE;
networkManager.setServerURL(config.serverURL.c_str());
Serial.println("Configuration loaded from file.");
}
// این تابع را به بخش CONFIGURATION اضافه کنید
void saveConfiguration() {
File configFile = SPIFFS.open("/config.json", "w");
if (!configFile) {
Serial.println("Failed to open config file for writing");
return;
}
DynamicJsonDocument doc(1024);
doc["server_url"] = config.serverURL;
doc["admin_code"] = config.adminCode;
if (serializeJson(doc, configFile) == 0) {
Serial.println("Failed to write to config file");
}
configFile.close();
}
// ------------------ MAIN ------------------
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("Vending Machine Starting...");
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Mount Failed");
while(1) delay(1000);
}
loadConfiguration();
if (!display.init()) {
Serial.println("Display init failed, halting.");
while (1) delay(1000);
}
display.showWelcomeMessage("Booting...", 1000);
if (!db.init(&display)) {
display.showMessage("DB ERROR", "Halting.", 0);
while(1) delay(1000);
}
keypadManager.init();
if (!loadcell.init()) {
display.showMessage("LOADCELL ERR", "Check wiring", 2000);
}
if (!motorControl.init(&loadcell, &keypadManager)) {
display.showMessage("MOTOR ERROR", "Halting.", 0);
while(1) delay(1000);
}
networkManager.init();
transactionQueue = xQueueCreate(10, sizeof(Transaction));
xTaskCreatePinnedToCore(
networkTask,
"Network Task",
4096,
NULL,
1,
NULL,
0
);
xTaskCreatePinnedToCore(
uiTask,
"UI Task",
4096,
NULL,
2,
NULL,
1
);
db.createBackup();
display.showWelcomeMessage(networkManager.isConnected() ? "Online" : "Offline", 1500);
userFlow.init();
Serial.println("Setup complete. Tasks are running.");
}
void loop() {
vTaskDelay(pdMS_TO_TICKS(1000));
}
// توقف اضطراری
void emergencyStop() {
motorControl.emergencyStop();
display.showMessage("EMERGENCY STOP", "System halted", 0);
Preferences preferences;
preferences.begin("system", false);
preferences.putULong64("last_shutdown", millis());
preferences.end();
while (true) {
display.clear();
delay(500);
display.showMessage("EMERGENCY STOP", "Restart required", 0);
delay(500);
}
}