#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <MFRC522.h>
#include <SPI.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>
#include <time.h>
#include <SdFat.h>
#include <esp_mac.h>
#include <esp_efuse_table.h>
#include <esp_task_wdt.h>
#include <Preferences.h>
#include <Update.h>
#include <esp_ota_ops.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <mbedtls/aes.h>
#include <mbedtls/md.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <freertos/queue.h>
#define PIN_SPI_SCK 4
#define PIN_SPI_MOSI 6
#define PIN_SPI_MISO 5
#define PIN_RFID_SS 7
#define PIN_RFID_RST 3
#define PIN_SD_CS 1
#define PIN_OLED_SDA 8
#define PIN_OLED_SCL 9
#define PIN_BUZZER 10
#define PIN_BOOT 9
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define DEBOUNCE_TIME 150UL
#define SYNC_INTERVAL 300000UL
#define MAX_OFFLINE_AGE 31536000UL
#define MIN_REPEAT_INTERVAL 1800UL
#define TIME_SYNC_INTERVAL 1800000UL
#define RECONNECT_INTERVAL 60000UL
#define RECONNECT_TIMEOUT 20000UL
#define DISPLAY_UPDATE_INTERVAL 1000UL
#define PERIODIC_CHECK_INTERVAL 1000UL
#define OLED_SCHEDULE_CHECK_INTERVAL 60000UL
#define RFID_FEEDBACK_DISPLAY_MS 1800UL
#define SD_REDETECT_INTERVAL 30000UL
#define MAX_TIME_ESTIMATE_AGE 43200UL
#define OTA_CHECK_INTERVAL 30000UL
#define RFID_DB_CHECK_INTERVAL 30000UL
#define TELEMETRY_INTERVAL 300000UL
#define REMOTE_CONFIG_INTERVAL 600000UL
#define FACTORY_RESET_HOLD_MS 5000UL
#define PROVISIONING_TIMEOUT_MS 300000UL
#define WDT_TIMEOUT_SEC 60
#define WDT_SYNC_TIMEOUT_MS 180000UL
#define WDT_NORMAL_TIMEOUT_MS 90000UL
#define SD_MUTEX_TIMEOUT_MS 5000UL
#define MAX_RECORDS_PER_FILE 25
#define MAX_QUEUE_FILES 60000
#define MAX_DUPLICATE_CHECK_FILES 3
#define MAX_DUPLICATE_CHECK_LINES (MAX_RECORDS_PER_FILE + 1)
#define QUEUE_WARN_THRESHOLD 48000
#define METADATA_FILE "/queue_meta.txt"
#define MAX_SYNC_FILES_PER_CYCLE 5
#define MAX_SYNC_RETRIES 2
#define SYNC_RETRY_DELAY_MS 2000UL
#define FAILED_LOG_MAX_LINES 500
#define NVS_MAX_RECORDS 40
#define NVS_NAMESPACE "presensi"
#define NVS_KEY_COUNT "nvs_count"
#define NVS_KEY_PREFIX "rec_"
#define NVS_KEY_LAST_TIME "last_time"
#define NVS_KEY_RFID_VER "rfid_db_ver"
#define NVS_KEY_SCAN_DATE "scan_date"
#define NVS_KEY_SCAN_COUNT "scan_count"
#define NVS_NS_CONFIG "cfg"
#define NVS_KEY_SSID1 "ssid1"
#define NVS_KEY_PASS1 "pass1"
#define NVS_KEY_SSID2 "ssid2"
#define NVS_KEY_PASS2 "pass2"
#define NVS_KEY_SSID3 "ssid3"
#define NVS_KEY_PASS3 "pass3"
#define NVS_KEY_APIKEY "apikey"
#define NVS_KEY_DEVNAME "devname"
#define NVS_KEY_APIURL "apiurl"
#define NVS_KEY_CFG_SLP_S "slp_s"
#define NVS_KEY_CFG_SLP_E "slp_e"
#define NVS_KEY_CFG_DIM_S "dim_s"
#define NVS_KEY_CFG_DIM_E "dim_e"
#define NVS_KEY_CFG_SYNCIV "sync_iv"
#define NVS_KEY_CFG_OTAIV "ota_iv"
#define NVS_KEY_PROVISIONED "prov"
#define NVS_KEY_LAST_RFID "last_rfid"
#define NVS_KEY_LAST_SCAN_T "last_scan_t"
#define RFID_DB_FILE "/rfid_db.txt"
#define RFID_CACHE_MAX 5000
#define ADMIN_RFID_FILE "/admin_rfid.txt"
#define SLEEP_START_HOUR_DEFAULT 18
#define SLEEP_END_HOUR_DEFAULT 5
#define OLED_DIM_START_HOUR_DEFAULT 8
#define OLED_DIM_END_HOUR_DEFAULT 12
#define GMT_OFFSET_SEC 25200L
#define SIGNAL_THRESHOLD_WEAK -85
#define SIGNAL_THRESHOLD_CRITICAL -90
#define FIRMWARE_VERSION "2.3.0"
#define PROV_AP_SSID "ATTENDANCE MACHINE"
#define PROV_AP_PASS "P@ssw0rd"
#define PROV_DNS_PORT 53
#define CRC8_POLY 0x07
#define TASK_RFID_STACK 8192
#define TASK_SYNC_STACK 8192
#define TASK_DISPLAY_STACK 12288
#define TASK_RFID_PRIORITY 3
#define TASK_SYNC_PRIORITY 2
#define TASK_DISPLAY_PRIORITY 1
#define RFID_QUEUE_LEN 8
#define DEEP_SLEEP_TASK_WAIT_MS 5000UL
#define DEVICE_NAME_MAX_LEN 31
static const char NTP_SERVER_1[] PROGMEM = "pool.ntp.org";
static const char NTP_SERVER_2[] PROGMEM = "time.google.com";
static const char NTP_SERVER_3[] PROGMEM = "id.pool.ntp.org";
RTC_DATA_ATTR time_t lastValidTime = 0;
RTC_DATA_ATTR bool timeWasSynced = false;
RTC_DATA_ATTR unsigned long bootTime = 0;
RTC_DATA_ATTR bool bootTimeSet = false;
RTC_DATA_ATTR int currentQueueFile = 0;
RTC_DATA_ATTR bool rtcQueueFileValid = false;
RTC_DATA_ATTR uint64_t sleepDurationSeconds = 0;
enum ReconnectState
{
RECONNECT_IDLE,
RECONNECT_INIT,
RECONNECT_TRYING,
RECONNECT_SUCCESS,
RECONNECT_FAILED
};
enum SaveResult
{
SAVE_OK,
SAVE_DUPLICATE,
SAVE_QUEUE_FULL,
SAVE_SD_ERROR
};
enum SyncFileResult
{
SYNC_FILE_OK,
SYNC_FILE_EMPTY,
SYNC_FILE_HTTP_FAIL,
SYNC_FILE_NO_WIFI
};
struct Timers
{
unsigned long lastScan, lastSync, lastTimeSync, lastReconnect;
unsigned long lastDisplayUpdate, lastPeriodicCheck, lastOLEDScheduleCheck;
unsigned long lastSDRedetect, lastNvsSync, lastOtaCheck, lastRfidDbCheck;
unsigned long lastTelemetry, lastRemoteConfig, lastFactoryCheck;
};
struct DisplayState
{
bool isOnline;
char time[6];
int pendingRecords;
int wifiSignal;
};
struct OfflineRecord
{
char rfid[11];
char timestamp[20];
char deviceId[20];
unsigned long unixTime;
};
struct SyncState
{
int currentFile;
bool inProgress;
unsigned long startTime;
int filesProcessed;
int filesSucceeded;
};
struct RfidFeedback
{
bool active;
unsigned long shownAt;
bool wasOledOff;
};
struct OtaState
{
bool updateAvailable;
char version[16];
char url[128];
char md5[36];
};
struct RfidScanEvent
{
uint8_t uid[10];
uint8_t uidLen;
};
struct RuntimeConfig
{
int sleepStartHour;
int sleepEndHour;
int dimStartHour;
int dimEndHour;
unsigned long syncIntervalMs;
unsigned long otaCheckIntervalMs;
};
struct EncryptedCredential
{
uint8_t iv[16];
uint8_t data[48];
uint8_t len;
};
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
MFRC522 rfidReader(PIN_RFID_SS, PIN_RFID_RST);
SdFat sd;
FsFile file;
Preferences prefs;
WebServer provServer(80);
DNSServer dnsServer;
Timers timers = {};
DisplayState currentDisplay = {false, "00:00", 0, 0};
DisplayState previousDisplay = {false, "--:--", -1, -1};
SyncState syncState = {0, false, 0, 0, 0};
RfidFeedback rfidFeedback = {false, 0, false};
OtaState otaState = {false, "", "", ""};
RuntimeConfig rtCfg = {
SLEEP_START_HOUR_DEFAULT, SLEEP_END_HOUR_DEFAULT,
OLED_DIM_START_HOUR_DEFAULT, OLED_DIM_END_HOUR_DEFAULT,
SYNC_INTERVAL, OTA_CHECK_INTERVAL};
char lastUID[11] = "";
char deviceId[20] = "";
char deviceName[DEVICE_NAME_MAX_LEN + 1] = "";
bool isOnline = false;
bool sdCardAvailable = false;
bool oledIsOn = true;
bool isProvisioned = false;
volatile bool wdtExtended = false;
portMUX_TYPE wdtMux = portMUX_INITIALIZER_UNLOCKED;
int cachedPendingRecords = 0;
bool pendingCacheDirty = true;
int cachedQueueFileCount = 0;
ReconnectState reconnectState = RECONNECT_IDLE;
unsigned long reconnectStartTime = 0;
int currentSsidIdx = 0;
char rfidCacheFlat[RFID_CACHE_MAX][11];
int rfidCacheCount = 0;
bool rfidCacheLoaded = false;
bool rfidDbValid = false;
char adminRfidList[5][11];
int adminRfidCount = 0;
TaskHandle_t hTaskRfid = nullptr;
TaskHandle_t hTaskSync = nullptr;
TaskHandle_t hTaskDisplay = nullptr;
TaskHandle_t hTaskLoop = nullptr;
SemaphoreHandle_t xSdMutex = nullptr;
SemaphoreHandle_t xDisplayMutex = nullptr;
QueueHandle_t xRfidQueue = nullptr;
volatile bool sleepRequested = false;
static uint8_t crc8(const uint8_t *data, size_t len)
{
uint8_t crc = 0x00;
for (size_t i = 0; i < len; i++)
{
crc ^= data[i];
for (int b = 0; b < 8; b++)
crc = (crc & 0x80) ? ((crc << 1) ^ CRC8_POLY) : (crc << 1);
}
return crc;
}
static uint8_t recordCrc8(const char *rfid, unsigned long t)
{
uint8_t buf[14];
memcpy(buf, rfid, 10);
buf[10] = (t >> 24) & 0xFF;
buf[11] = (t >> 16) & 0xFF;
buf[12] = (t >> 8) & 0xFF;
buf[13] = (t) & 0xFF;
return crc8(buf, 14);
}
static void deriveAesKey(uint8_t key[16])
{
Serial.println("[AES] Deriving AES key from eFuse MAC...");
uint8_t mac[6];
esp_efuse_mac_get_default(mac);
uint8_t seed[22];
memcpy(seed, mac, 6);
const char *salt = "ZEDLABS_PRESENSI";
memcpy(seed + 6, salt, 16);
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 0);
mbedtls_md_starts(&ctx);
mbedtls_md_update(&ctx, seed, 22);
uint8_t hash[32];
mbedtls_md_finish(&ctx, hash);
mbedtls_md_free(&ctx);
memcpy(key, hash, 16);
Serial.println("[AES] Key derivation done.");
}
static bool encryptString(const char *plain, EncryptedCredential &out)
{
Serial.printf("[ENC] Encrypting string (len=%d)...\n", strlen(plain));
uint8_t key[16];
deriveAesKey(key);
size_t plen = strlen(plain);
if (plen > 47)
{
Serial.println("[ENC] ERROR: string too long (>47)");
return false;
}
uint8_t buf[48] = {};
memcpy(buf, plain, plen);
out.len = (uint8_t)plen;
esp_fill_random(out.iv, 16);
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, key, 128);
uint8_t iv[16];
memcpy(iv, out.iv, 16);
mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, 48, iv, buf, out.data);
mbedtls_aes_free(&aes);
Serial.println("[ENC] Encrypt OK.");
return true;
}
static bool decryptString(const EncryptedCredential &in, char *plain, size_t maxLen)
{
Serial.println("[DEC] Decrypting credential...");
uint8_t key[16];
deriveAesKey(key);
uint8_t buf[48];
uint8_t iv[16];
memcpy(iv, in.iv, 16);
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_dec(&aes, key, 128);
mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, 48, iv, in.data, buf);
mbedtls_aes_free(&aes);
size_t copyLen = (in.len < maxLen - 1) ? in.len : maxLen - 1;
memcpy(plain, buf, copyLen);
plain[copyLen] = '\0';
Serial.println("[DEC] Decrypt OK.");
return true;
}
static void saveEncryptedNvs(const char *ns, const char *key, const char *plain)
{
Serial.printf("[NVS] saveEncrypted ns=%s key=%s\n", ns, key);
EncryptedCredential ec;
if (!encryptString(plain, ec))
{
Serial.println("[NVS] saveEncrypted FAILED (encrypt error)");
return;
}
prefs.begin(ns, false);
prefs.putBytes(key, &ec, sizeof(EncryptedCredential));
prefs.end();
Serial.println("[NVS] saveEncrypted done.");
}
static bool loadEncryptedNvs(const char *ns, const char *key, char *plain, size_t maxLen)
{
Serial.printf("[NVS] loadEncrypted ns=%s key=%s\n", ns, key);
prefs.begin(ns, true);
size_t len = prefs.getBytesLength(key);
if (len != sizeof(EncryptedCredential))
{
prefs.end();
Serial.printf("[NVS] loadEncrypted FAILED: len mismatch (%d vs %d)\n", len, sizeof(EncryptedCredential));
return false;
}
EncryptedCredential ec;
prefs.getBytes(key, &ec, sizeof(EncryptedCredential));
prefs.end();
bool ok = decryptString(ec, plain, maxLen);
Serial.printf("[NVS] loadEncrypted result=%d\n", ok);
return ok;
}
struct WifiCredential
{
char ssid[32];
char pass[64];
};
static WifiCredential wifiCreds[3];
static char apiKey[48] = "";
static char apiBaseUrl[80] = "https://presensi.zedlabs.id";
static void loadCredentials()
{
Serial.println("[CFG] Loading credentials from NVS...");
loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_SSID1, wifiCreds[0].ssid, sizeof(wifiCreds[0].ssid));
loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_PASS1, wifiCreds[0].pass, sizeof(wifiCreds[0].pass));
loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_SSID2, wifiCreds[1].ssid, sizeof(wifiCreds[1].ssid));
loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_PASS2, wifiCreds[1].pass, sizeof(wifiCreds[1].pass));
loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_SSID3, wifiCreds[2].ssid, sizeof(wifiCreds[2].ssid));
loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_PASS3, wifiCreds[2].pass, sizeof(wifiCreds[2].pass));
loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_APIKEY, apiKey, sizeof(apiKey));
loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_DEVNAME, deviceName, sizeof(deviceName));
char tmpUrl[80] = "";
if (loadEncryptedNvs(NVS_NS_CONFIG, NVS_KEY_APIURL, tmpUrl, sizeof(tmpUrl)))
{
if (strlen(tmpUrl) > 0)
{
int ul = strlen(tmpUrl);
while (ul > 0 && tmpUrl[ul - 1] == '/')
tmpUrl[--ul] = '\0';
strncpy(apiBaseUrl, tmpUrl, sizeof(apiBaseUrl) - 1);
apiBaseUrl[sizeof(apiBaseUrl) - 1] = '\0';
}
}
prefs.begin(NVS_NS_CONFIG, true);
int slpS = prefs.getInt(NVS_KEY_CFG_SLP_S, SLEEP_START_HOUR_DEFAULT);
int slpE = prefs.getInt(NVS_KEY_CFG_SLP_E, SLEEP_END_HOUR_DEFAULT);
int dimS = prefs.getInt(NVS_KEY_CFG_DIM_S, OLED_DIM_START_HOUR_DEFAULT);
int dimE = prefs.getInt(NVS_KEY_CFG_DIM_E, OLED_DIM_END_HOUR_DEFAULT);
prefs.end();
auto clampHour = [](int h, int def)
{
return (h >= 0 && h <= 23) ? h : def;
};
rtCfg.sleepStartHour = clampHour(slpS, SLEEP_START_HOUR_DEFAULT);
rtCfg.sleepEndHour = clampHour(slpE, SLEEP_END_HOUR_DEFAULT);
rtCfg.dimStartHour = clampHour(dimS, OLED_DIM_START_HOUR_DEFAULT);
rtCfg.dimEndHour = clampHour(dimE, OLED_DIM_END_HOUR_DEFAULT);
Serial.printf("[CFG] apiBaseUrl: %s\n", apiBaseUrl);
Serial.printf("[CFG] sleep: %d-%d, dim: %d-%d\n",
rtCfg.sleepStartHour, rtCfg.sleepEndHour,
rtCfg.dimStartHour, rtCfg.dimEndHour);
Serial.println("[CFG] Credentials loaded.");
}
static void saveCredential(const char *key, const char *val)
{
saveEncryptedNvs(NVS_NS_CONFIG, key, val);
}
static void markProvisioned()
{
Serial.println("[PROV] Marking device as provisioned...");
prefs.begin(NVS_NS_CONFIG, false);
prefs.putBool(NVS_KEY_PROVISIONED, true);
prefs.end();
isProvisioned = true;
Serial.println("[PROV] Device marked provisioned.");
}
static bool checkProvisioned()
{
prefs.begin(NVS_NS_CONFIG, true);
bool v = prefs.getBool(NVS_KEY_PROVISIONED, false);
prefs.end();
Serial.printf("[PROV] checkProvisioned=%d\n", v);
return v;
}
static WiFiClientSecure _httpClient;
static WiFiClientSecure &getHttpClient()
{
_httpClient.setInsecure();
_httpClient.setHandshakeTimeout(10);
return _httpClient;
}
inline bool acquireSD(TickType_t timeout = pdMS_TO_TICKS(SD_MUTEX_TIMEOUT_MS))
{
return xSemaphoreTake(xSdMutex, timeout) == pdTRUE;
}
inline void releaseSD()
{
xSemaphoreGive(xSdMutex);
}
inline void selectSD()
{
digitalWrite(PIN_RFID_SS, HIGH);
digitalWrite(PIN_SD_CS, LOW);
}
inline void deselectSD()
{
digitalWrite(PIN_SD_CS, HIGH);
}
bool isWifiConnected()
{
return WiFi.status() == WL_CONNECTED;
}
bool isSignalWeak()
{
return !isWifiConnected() || WiFi.RSSI() < SIGNAL_THRESHOLD_WEAK;
}
bool isSignalCritical()
{
return !isWifiConnected() || WiFi.RSSI() < SIGNAL_THRESHOLD_CRITICAL;
}
void extendWdtForSync()
{
if (!hTaskRfid && !hTaskSync && !hTaskDisplay)
{
Serial.println("[WDT] Tasks not ready, skip extend.");
return;
}
portENTER_CRITICAL(&wdtMux);
if (wdtExtended)
{
portEXIT_CRITICAL(&wdtMux);
return;
}
wdtExtended = true;
portEXIT_CRITICAL(&wdtMux);
if (hTaskLoop)
esp_task_wdt_delete(hTaskLoop);
if (hTaskRfid)
esp_task_wdt_delete(hTaskRfid);
if (hTaskSync)
esp_task_wdt_delete(hTaskSync);
if (hTaskDisplay)
esp_task_wdt_delete(hTaskDisplay);
TaskHandle_t idle0 = xTaskGetIdleTaskHandleForCore(0);
if (idle0)
esp_task_wdt_delete(idle0);
esp_task_wdt_deinit();
const esp_task_wdt_config_t cfg = {
.timeout_ms = (uint32_t)WDT_SYNC_TIMEOUT_MS,
.idle_core_mask = 0,
.trigger_panic = true};
esp_task_wdt_init(&cfg);
if (hTaskLoop)
esp_task_wdt_add(hTaskLoop);
if (hTaskRfid)
esp_task_wdt_add(hTaskRfid);
if (hTaskSync)
esp_task_wdt_add(hTaskSync);
if (hTaskDisplay)
esp_task_wdt_add(hTaskDisplay);
Serial.println("[WDT] Extended to 180s for sync/OTA.");
}
void restoreWdtNormal()
{
if (!hTaskRfid && !hTaskSync && !hTaskDisplay)
{
Serial.println("[WDT] Tasks not ready, skip restore.");
return;
}
portENTER_CRITICAL(&wdtMux);
if (!wdtExtended)
{
portEXIT_CRITICAL(&wdtMux);
return;
}
wdtExtended = false;
portEXIT_CRITICAL(&wdtMux);
if (hTaskLoop)
esp_task_wdt_delete(hTaskLoop);
if (hTaskRfid)
esp_task_wdt_delete(hTaskRfid);
if (hTaskSync)
esp_task_wdt_delete(hTaskSync);
if (hTaskDisplay)
esp_task_wdt_delete(hTaskDisplay);
TaskHandle_t idle0 = xTaskGetIdleTaskHandleForCore(0);
if (idle0)
esp_task_wdt_delete(idle0);
esp_task_wdt_deinit();
const esp_task_wdt_config_t cfg = {
.timeout_ms = WDT_NORMAL_TIMEOUT_MS,
.idle_core_mask = 0,
.trigger_panic = true};
esp_task_wdt_init(&cfg);
if (hTaskLoop)
esp_task_wdt_add(hTaskLoop);
if (hTaskRfid)
esp_task_wdt_add(hTaskRfid);
if (hTaskSync)
esp_task_wdt_add(hTaskSync);
if (hTaskDisplay)
esp_task_wdt_add(hTaskDisplay);
Serial.println("[WDT] Restored to 90s normal.");
}
void turnOffOLED()
{
if (!oledIsOn)
return;
if (xSemaphoreTake(xDisplayMutex, pdMS_TO_TICKS(100)) == pdTRUE)
{
display.clearDisplay();
display.display();
display.ssd1306_command(SSD1306_DISPLAYOFF);
oledIsOn = false;
xSemaphoreGive(xDisplayMutex);
}
}
void turnOnOLED()
{
if (oledIsOn)
return;
if (xSemaphoreTake(xDisplayMutex, pdMS_TO_TICKS(100)) == pdTRUE)
{
display.ssd1306_command(SSD1306_DISPLAYON);
oledIsOn = true;
memset(previousDisplay.time, 0xFF, sizeof(previousDisplay.time));
previousDisplay.pendingRecords = -1;
previousDisplay.wifiSignal = -1;
previousDisplay.isOnline = !currentDisplay.isOnline;
xSemaphoreGive(xDisplayMutex);
}
}
void showOLED(const __FlashStringHelper *l1, const char *l2)
{
if (!oledIsOn)
return;
if (xSemaphoreTake(xDisplayMutex, pdMS_TO_TICKS(200)) != pdTRUE)
return;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
int16_t x, y;
uint16_t w, h;
display.getTextBounds(l1, 0, 0, &x, &y, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 10);
display.println(l1);
display.getTextBounds(l2, 0, 0, &x, &y, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 30);
display.println(l2);
display.display();
xSemaphoreGive(xDisplayMutex);
}
void showOLED(const __FlashStringHelper *l1, const __FlashStringHelper *l2)
{
char buf[32];
strncpy_P(buf, (const char *)l2, 31);
buf[31] = '\0';
showOLED(l1, buf);
}
void showProgress(const __FlashStringHelper *msg, int ms)
{
if (!oledIsOn)
return;
if (xSemaphoreTake(xDisplayMutex, pdMS_TO_TICKS(200)) != pdTRUE)
return;
const int step = 8, total = 80;
int perStep = ms / (total / step);
int startX = (SCREEN_WIDTH - total) / 2;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
int16_t x, y;
uint16_t w, h;
display.getTextBounds(msg, 0, 0, &x, &y, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 20);
display.println(msg);
display.display();
xSemaphoreGive(xDisplayMutex);
for (int i = 0; i <= total; i += step)
{
esp_task_wdt_reset();
if (xSemaphoreTake(xDisplayMutex, pdMS_TO_TICKS(50)) == pdTRUE)
{
display.fillRect(startX, 40, i, 4, WHITE);
display.display();
xSemaphoreGive(xDisplayMutex);
}
vTaskDelay(pdMS_TO_TICKS(perStep));
}
esp_task_wdt_reset();
vTaskDelay(pdMS_TO_TICKS(300));
}
void playToneSuccess()
{
for (int i = 0; i < 2; i++)
{
tone(PIN_BUZZER, 3000, 100);
delay(150);
}
noTone(PIN_BUZZER);
}
void playToneError()
{
for (int i = 0; i < 3; i++)
{
tone(PIN_BUZZER, 3000, 150);
delay(200);
}
noTone(PIN_BUZZER);
}
void playToneNotify()
{
tone(PIN_BUZZER, 3000, 100);
delay(120);
noTone(PIN_BUZZER);
}
void playStartupMelody()
{
static const int mel[] = {2500, 3000, 2500, 3000};
for (int i = 0; i < 4; i++)
{
tone(PIN_BUZZER, mel[i], 100);
delay(150);
}
noTone(PIN_BUZZER);
}
void nvsSaveLastTime(time_t t)
{
prefs.begin(NVS_NAMESPACE, false);
prefs.putULong(NVS_KEY_LAST_TIME, (unsigned long)t);
prefs.end();
}
time_t nvsLoadLastTime()
{
prefs.begin(NVS_NAMESPACE, true);
unsigned long t = prefs.getULong(NVS_KEY_LAST_TIME, 0);
prefs.end();
return (time_t)t;
}
void nvsBumpScanCount()
{
prefs.begin(NVS_NAMESPACE, false);
struct tm ti;
time_t now = time(nullptr);
localtime_r(&now, &ti);
char today[9];
snprintf(today, sizeof(today), "%04d%02d%02d", ti.tm_year + 1900, ti.tm_mon + 1, ti.tm_mday);
char stored[9];
strncpy(stored, prefs.getString(NVS_KEY_SCAN_DATE, "").c_str(), 8);
stored[8] = '\0';
int cnt = strcmp(stored, today) == 0 ? prefs.getInt(NVS_KEY_SCAN_COUNT, 0) : 0;
prefs.putString(NVS_KEY_SCAN_DATE, today);
prefs.putInt(NVS_KEY_SCAN_COUNT, cnt + 1);
prefs.end();
}
int nvsGetScanCount()
{
prefs.begin(NVS_NAMESPACE, true);
int c = prefs.getInt(NVS_KEY_SCAN_COUNT, 0);
prefs.end();
return c;
}
int nvsGetCount()
{
prefs.begin(NVS_NAMESPACE, true);
int c = prefs.getInt(NVS_KEY_COUNT, 0);
prefs.end();
return c;
}
void nvsSetCount(int count)
{
prefs.begin(NVS_NAMESPACE, false);
prefs.putInt(NVS_KEY_COUNT, count);
prefs.end();
}
bool nvsLoadRecord(int idx, OfflineRecord &rec)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_KEY_PREFIX, idx);
prefs.begin(NVS_NAMESPACE, true);
size_t len = prefs.getBytesLength(key);
if (len != sizeof(OfflineRecord))
{
prefs.end();
return false;
}
prefs.getBytes(key, &rec, sizeof(OfflineRecord));
prefs.end();
return true;
}
bool nvsSaveRecord(int idx, const OfflineRecord &rec)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_KEY_PREFIX, idx);
prefs.begin(NVS_NAMESPACE, false);
size_t w = prefs.putBytes(key, &rec, sizeof(OfflineRecord));
prefs.end();
return w == sizeof(OfflineRecord);
}
void nvsDeleteRecord(int idx)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_KEY_PREFIX, idx);
prefs.begin(NVS_NAMESPACE, false);
prefs.remove(key);
prefs.end();
}
bool nvsIsDuplicate(const char *rfid, unsigned long t)
{
int cnt = nvsGetCount();
for (int i = 0; i < cnt; i++)
{
OfflineRecord rec;
if (!nvsLoadRecord(i, rec))
continue;
if (strcmp(rec.rfid, rfid) == 0 && t >= rec.unixTime && (t - rec.unixTime) < MIN_REPEAT_INTERVAL)
return true;
}
return false;
}
bool nvsSaveToBuffer(const char *rfid, const char *ts, unsigned long t)
{
int cnt = nvsGetCount();
if (cnt >= NVS_MAX_RECORDS)
return false;
OfflineRecord rec;
strncpy(rec.rfid, rfid, sizeof(rec.rfid) - 1);
rec.rfid[sizeof(rec.rfid) - 1] = '\0';
strncpy(rec.timestamp, ts, sizeof(rec.timestamp) - 1);
rec.timestamp[sizeof(rec.timestamp) - 1] = '\0';
strncpy(rec.deviceId, deviceId, sizeof(rec.deviceId) - 1);
rec.deviceId[sizeof(rec.deviceId) - 1] = '\0';
rec.unixTime = t;
if (!nvsSaveRecord(cnt, rec))
return false;
nvsSetCount(cnt + 1);
return true;
}
unsigned long nvsGetRfidDbVer()
{
prefs.begin(NVS_NAMESPACE, true);
unsigned long v = prefs.getULong(NVS_KEY_RFID_VER, 0);
prefs.end();
return v;
}
void nvsSetRfidDbVer(unsigned long ver)
{
prefs.begin(NVS_NAMESPACE, false);
prefs.putULong(NVS_KEY_RFID_VER, ver);
prefs.end();
}
void nvsSaveLastScan(const char *rfid, unsigned long t)
{
prefs.begin(NVS_NAMESPACE, false);
prefs.putString(NVS_KEY_LAST_RFID, rfid);
prefs.putULong(NVS_KEY_LAST_SCAN_T, t);
prefs.end();
}
bool nvsIsRecentScan(const char *rfid, unsigned long t)
{
prefs.begin(NVS_NAMESPACE, true);
String storedRfid = prefs.getString(NVS_KEY_LAST_RFID, "");
unsigned long storedT = prefs.getULong(NVS_KEY_LAST_SCAN_T, 0);
prefs.end();
if (storedRfid.length() == 0 || storedT == 0)
return false;
if (storedRfid != String(rfid))
return false;
return (t >= storedT && (t - storedT) < MIN_REPEAT_INTERVAL);
}
void clearRfidCache()
{
memset(rfidCacheFlat, 0, sizeof(rfidCacheFlat));
rfidCacheCount = 0;
rfidCacheLoaded = false;
rfidDbValid = false;
}
bool loadRfidCacheFromFileLocked()
{
Serial.println("[RFID] Loading RFID cache from file...");
clearRfidCache();
if (!sd.exists(RFID_DB_FILE))
{
Serial.println("[RFID] rfid_db.txt not found.");
return false;
}
FsFile f;
if (!f.open(RFID_DB_FILE, O_RDONLY))
{
Serial.println("[RFID] Failed to open rfid_db.txt.");
return false;
}
char line[12];
int idx = 0;
while (f.fgets(line, sizeof(line)) > 0 && idx < RFID_CACHE_MAX)
{
esp_task_wdt_reset();
taskYIELD();
int len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
if (len != 10)
continue;
bool ok = true;
for (int j = 0; j < 10 && ok; j++)
ok = isdigit((unsigned char)line[j]);
if (!ok)
continue;
memcpy(rfidCacheFlat[idx], line, 10);
rfidCacheFlat[idx][10] = '\0';
idx++;
}
f.close();
rfidCacheCount = idx;
rfidCacheLoaded = (idx > 0);
rfidDbValid = (idx > 0);
Serial.printf("[RFID] Cache loaded: %d entries, valid=%d\n", rfidCacheCount, rfidDbValid);
return rfidDbValid;
}
bool loadRfidCacheFromFile()
{
if (!sdCardAvailable)
return false;
if (!acquireSD())
return false;
selectSD();
bool ok = loadRfidCacheFromFileLocked();
deselectSD();
releaseSD();
return ok;
}
bool isRfidInCache(const char *rfid)
{
if (!rfidDbValid || !rfidCacheLoaded || rfidCacheCount == 0)
return false;
for (int i = 0; i < rfidCacheCount; i++)
if (strcmp(rfidCacheFlat[i], rfid) == 0)
return true;
return false;
}
void loadAdminRfidList()
{
Serial.println("[ADMIN] Loading admin RFID list...");
adminRfidCount = 0;
if (!sdCardAvailable)
{
Serial.println("[ADMIN] SD not available, skip.");
return;
}
if (!acquireSD())
{
Serial.println("[ADMIN] SD mutex timeout.");
return;
...
This file has been truncated, please download it to see its full contents.
Comments