Yahya Zulfikri
Published © MIT

Smart Attendance Machine with ESP32-C3 & RFID (Hybrid IoT)

Offline-first RFID attendance system on ESP32-C3 with hybrid sync, OTA updates, and zero data loss — even without internet.

AdvancedFull instructions provided1 hour28
Smart Attendance Machine with ESP32-C3 & RFID (Hybrid IoT)

Things used in this project

Hardware components

ESP32-C3 Super Mini
Main microcontroller. Handles WiFi, RFID processing, FreeRTOS tasks, OTA updates, and NVS storage.
×1
OLED Display 0.96" SSD1306
Displays connection status, current time, and queue counter. Connected via I2C on GPIO 8 (SDA), 9 (SCL).
×1
MicroSD Card Module
Stores offline attendance queue (up to 1,500,000 records in CSV format) and local RFID database.
×1
Active Buzzer 5V
Audio feedback for successful tap, rejected card, and system notifications. Connected to GPIO 10 (PWM).
×1
MFRC522 / RC522 RFID Reader
Reads 13.56 MHz RFID cards via SPI. Connected to GPIO 4 (SCK), 5 (MISO), 6 (MOSI), 7 (SS), 3 (RST).
×1

Software apps and online services

Arduino IDE
Arduino IDE
Used to compile and upload firmware to ESP32-C3. Requires ESP32 Arduino core v3.x and libraries: MFRC522, Adafruit SSD1306, Adafruit GFX, ArduinoJson, SdFat.
Madrasah Universe - Attendance System
The backend web system that receives attendance data from the device via REST API. Manages student data, RFID registration, and attendance reports.

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Used to solder pin headers and connections between ESP32-C3, RC522, MicroSD module, OLED, and buzzer.

Story

Read more

Schematics

ESP32-C3 Wiring Diagram

Pin connections for RC522 RFID, MicroSD, OLED SSD1306, and Buzzer to ESP32-C3 Super Mini.

Code

Attendance Machine Firmware v2.3.0

C/C++
Main firmware for ESP32-C3. Handles RFID tap, offline queue, background sync, OTA update, and provisioning mode.
#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.

Credits

Yahya Zulfikri
1 project • 0 followers
Nothing!

Comments