#include <WiFi.h>
#include <WebServer.h>
#include <SPI.h>
#include <MFRC522.h>
// ================= KONFIGURASI WIFI AP =================
const char* ssid = "Innoiti-CR1";
const char* password = "12345678";
IPAddress local_ip(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);
// ================= KONFIGURASI RFID =================
#define SS_PIN 5
#define RST_PIN 22
MFRC522 mfrc522(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key key;
// ================= WEB SERVER =================
WebServer server(80);
// ================= STATE SESSION =================
struct SessionData {
String uid;
String nama;
String id;
String kode;
bool cardDetected;
bool writeSuccess;
String errorMessage;
};
SessionData session;
// ================= STATE DETEKSI KARTU STABIL =================
bool cardActive = false;
bool cardCurrentlyPresent = false;
unsigned long lastCardReadTime = 0;
MFRC522::Uid lastUid;
// ================= HELPER FUNCTIONS =================
String uidToString(byte* buffer, byte bufferSize) {
String uid = "";
for (byte i = 0; i < bufferSize; i++) {
if (buffer[i] < 0x10) uid += "0";
uid += String(buffer[i], HEX);
}
uid.toUpperCase();
return uid;
}
bool isSameUid(MFRC522::Uid& uid1, MFRC522::Uid& uid2) {
if (uid1.size != uid2.size) return false;
for (byte i = 0; i < uid1.size; i++) {
if (uid1.uidByte[i] != uid2.uidByte[i]) return false;
}
return true;
}
void copyUid(MFRC522::Uid& src, MFRC522::Uid& dst) {
dst.size = src.size;
for (byte i = 0; i < src.size; i++) {
dst.uidByte[i] = src.uidByte[i];
}
}
String readBlock(int block) {
byte buffer[18];
byte size = sizeof(buffer);
MFRC522::StatusCode status = mfrc522.PCD_Authenticate(
MFRC522::PICC_CMD_MF_AUTH_KEY_A,
block,
&key,
&(mfrc522.uid)
);
if (status != MFRC522::STATUS_OK) {
return "";
}
status = mfrc522.MIFARE_Read(block, buffer, &size);
if (status != MFRC522::STATUS_OK) {
return "";
}
String result = "";
for (byte i = 0; i < 16; i++) {
if (buffer[i] == 0) break;
result += (char)buffer[i];
}
result.trim();
return result;
}
bool readFromCard(String &nama, String &id, String &kode) {
nama = readBlock(4);
id = readBlock(5);
kode = readBlock(6);
// Jika semua blok gagal dibaca
if (nama == "" && id == "" && kode == "") {
return false;
}
return true;
}
bool writeBlock(int block, String data) {
data.trim();
if (data.length() > 16) data = data.substring(0, 16);
byte buffer[16];
memset(buffer, 0, 16);
for (int i = 0; i < data.length(); i++) buffer[i] = (byte)data[i];
MFRC522::StatusCode status = mfrc522.PCD_Authenticate(
MFRC522::PICC_CMD_MF_AUTH_KEY_A,
block,
&key,
&(mfrc522.uid)
);
if (status != MFRC522::STATUS_OK) {
Serial.printf("Auth gagal blok %d: %s\n", block, mfrc522.GetStatusCodeName(status));
return false;
}
status = mfrc522.MIFARE_Write(block, buffer, 16);
if (status != MFRC522::STATUS_OK) {
Serial.printf("Write gagal blok %d: %s\n", block, mfrc522.GetStatusCodeName(status));
return false;
}
Serial.printf("Blok %d berhasil ditulis: %s\n", block, data.c_str());
return true;
}
bool writeToCard() {
bool success1 = writeBlock(4, session.nama);
bool success2 = writeBlock(5, session.id);
bool success3 = writeBlock(6, session.kode);
if (success1 && success2 && success3) {
session.writeSuccess = true;
return true;
}
session.writeSuccess = false;
return false;
}
// ================= HTML PAGE - HOME (PILIH MODE) =================
String getHomePage() {
return R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Innoiti CR-1</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
.wrapper {
max-width: 420px;
width: 100%;
text-align: center;
}
.logo {
margin-bottom: 48px;
}
.logo h1 {
color: white;
font-size: 32px;
font-weight: 600;
letter-spacing: -0.5px;
margin-bottom: 8px;
}
.logo p {
color: rgba(255,255,255,0.7);
font-size: 14px;
}
.mode-container {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 32px;
}
.mode-card {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(10px);
border-radius: 24px;
padding: 32px 24px;
border: 1px solid rgba(255,255,255,0.1);
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: block;
}
.mode-card:hover {
background: rgba(255,255,255,0.1);
transform: translateY(-4px);
}
.mode-icon {
font-size: 48px;
margin-bottom: 16px;
}
.mode-title {
font-size: 22px;
font-weight: 600;
color: white;
margin-bottom: 8px;
}
.mode-desc {
font-size: 14px;
color: rgba(255,255,255,0.6);
}
.footer {
font-size: 12px;
color: rgba(255,255,255,0.4);
margin-top: 32px;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="logo">
<h1>Innoiti CR-1</h1>
<p>RFID Reader / Writer</p>
</div>
<div class="mode-container">
<a href="/write" class="mode-card">
<div class="mode-icon">✍️</div>
<div class="mode-title">Mode Tulis</div>
<div class="mode-desc">Tulis data ke kartu RFID</div>
</a>
<a href="/read" class="mode-card">
<div class="mode-icon">📖</div>
<div class="mode-title">Mode Baca</div>
<div class="mode-desc">Baca data dari kartu RFID</div>
</a>
</div>
<div class="footer">Pilih mode yang diinginkan</div>
</div>
</body>
</html>
)rawliteral";
}
// ================= HTML PAGE - SCAN UNTUK MODE TULIS =================
String getWriteScanPage() {
return R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Innoiti CR-1 - Scan Kartu</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
.wrapper {
max-width: 420px;
width: 100%;
text-align: center;
}
.back-link {
display: inline-block;
color: rgba(255,255,255,0.7);
text-decoration: none;
font-size: 14px;
margin-bottom: 24px;
}
.back-link:hover {
color: white;
}
.logo {
margin-bottom: 32px;
}
.logo h1 {
color: white;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.logo p {
color: rgba(255,255,255,0.7);
font-size: 14px;
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 48px;
padding: 0 16px;
}
.step-item {
text-align: center;
flex: 1;
}
.step {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
color: white;
line-height: 32px;
text-align: center;
font-weight: 600;
font-size: 14px;
margin: 0 auto 8px;
}
.step.active {
background: white;
color: #106CFF;
}
.step-label {
font-size: 12px;
color: rgba(255,255,255,0.6);
}
.card-area {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(10px);
border-radius: 32px;
padding: 48px 32px;
margin-bottom: 24px;
border: 1px solid rgba(255,255,255,0.1);
}
.icon {
font-size: 64px;
margin-bottom: 24px;
}
.card-area h3 {
color: white;
font-size: 20px;
font-weight: 500;
margin-bottom: 16px;
}
.status {
padding: 12px 20px;
border-radius: 100px;
font-size: 14px;
font-weight: 500;
display: inline-block;
}
.status.waiting {
background: rgba(255,193,7,0.2);
color: #FFD700;
}
.status.success {
background: rgba(40,167,69,0.2);
color: #4ade80;
}
.uid-display {
background: rgba(255,255,255,0.1);
border-radius: 16px;
padding: 12px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 18px;
font-weight: 600;
color: white;
margin-top: 16px;
letter-spacing: 1px;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.footer {
font-size: 12px;
color: rgba(255,255,255,0.5);
margin-top: 24px;
}
</style>
</head>
<body>
<div class="wrapper">
<a href="/" class="back-link">← Kembali ke Menu</a>
<div class="logo">
<h1>Mode Tulis</h1>
<p>RFID Writer</p>
</div>
<div class="step-indicator">
<div class="step-item"><div class="step active">1</div><div class="step-label">Scan Kartu</div></div>
<div class="step-item"><div class="step">2</div><div class="step-label">Isi Data</div></div>
<div class="step-item"><div class="step">3</div><div class="step-label">Selesai</div></div>
</div>
<div class="card-area">
<div class="icon">💳</div>
<h3>Tempelkan Kartu RFID</h3>
<div id="statusBox" class="status waiting">⏳ Menunggu kartu...</div>
<div id="uidBox" style="display:none;">
<div class="uid-display" id="uidValue"></div>
</div>
</div>
<div class="footer">Kartu akan otomatis melanjutkan</div>
</div>
<script>
let cardDetected = false;
let uid = "";
let redirecting = false;
async function checkStatus() {
if (redirecting) return;
try {
const res = await fetch('/api/status');
const data = await res.json();
if (data.cardDetected && !cardDetected) {
cardDetected = true;
uid = data.uid;
document.getElementById('statusBox').innerHTML = '<span class="spinner"></span> Kartu terdeteksi!';
document.getElementById('statusBox').className = 'status success';
document.getElementById('uidBox').style.display = 'block';
document.getElementById('uidValue').innerHTML = 'UID: ' + uid;
redirecting = true;
setTimeout(() => {
window.location.href = '/write/form?uid=' + uid;
}, 800);
} else if (!data.cardDetected && cardDetected) {
cardDetected = false;
redirecting = false;
document.getElementById('statusBox').innerHTML = '⏳ Menunggu kartu...';
document.getElementById('statusBox').className = 'status waiting';
document.getElementById('uidBox').style.display = 'none';
}
} catch(e) {}
}
setInterval(checkStatus, 500);
checkStatus();
</script>
</body>
</html>
)rawliteral";
}
// ================= HTML PAGE - FORM TULIS =================
String getWriteFormPage(String uid) {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Innoiti CR-1 - Isi Data</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
.wrapper {
max-width: 420px;
width: 100%;
text-align: center;
}
.back-link {
display: inline-block;
color: rgba(255,255,255,0.7);
text-decoration: none;
font-size: 14px;
margin-bottom: 24px;
}
.back-link:hover {
color: white;
}
.logo {
margin-bottom: 32px;
}
.logo h1 {
color: white;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.logo p {
color: rgba(255,255,255,0.7);
font-size: 14px;
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 48px;
padding: 0 16px;
}
.step-item {
text-align: center;
flex: 1;
}
.step {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
color: white;
line-height: 32px;
text-align: center;
font-weight: 600;
font-size: 14px;
margin: 0 auto 8px;
}
.step.completed {
background: white;
color: #106CFF;
}
.step.active {
background: white;
color: #106CFF;
}
.step-label {
font-size: 12px;
color: rgba(255,255,255,0.6);
}
.info-card {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(10px);
border-radius: 24px;
padding: 20px;
margin-bottom: 24px;
border: 1px solid rgba(255,255,255,0.1);
}
.info-card strong {
color: white;
font-size: 14px;
}
.uid-display {
font-family: 'SF Mono', Monaco, monospace;
font-size: 16px;
font-weight: 600;
color: rgba(255,255,255,0.9);
background: rgba(255,255,255,0.1);
padding: 8px 12px;
border-radius: 12px;
margin-top: 8px;
display: inline-block;
}
.info-card small {
color: rgba(255,255,255,0.5);
font-size: 11px;
display: block;
margin-top: 8px;
}
.form-group {
margin-bottom: 16px;
}
input {
width: 100%;
padding: 14px 18px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 16px;
font-size: 16px;
color: white;
transition: all 0.2s;
outline: none;
}
input::placeholder {
color: rgba(255,255,255,0.5);
}
input:focus {
border-color: rgba(255,255,255,0.5);
background: rgba(255,255,255,0.15);
}
button {
width: 100%;
padding: 14px;
background: white;
border: none;
border-radius: 16px;
font-size: 16px;
font-weight: 600;
color: #106CFF;
cursor: pointer;
transition: all 0.2s;
margin-top: 8px;
}
button:hover {
transform: scale(0.98);
background: #f0f0f0;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.warning {
background: rgba(255,193,7,0.15);
border-radius: 12px;
padding: 12px;
font-size: 12px;
margin-top: 16px;
color: #FFD700;
}
.warning.error {
background: rgba(220,53,69,0.15);
color: #ff6b6b;
}
.warning.success {
background: rgba(40,167,69,0.15);
color: #4ade80;
}
.footer {
font-size: 12px;
color: rgba(255,255,255,0.4);
margin-top: 24px;
}
</style>
</head>
<body>
<div class="wrapper">
<a href="/" class="back-link">← Kembali ke Menu</a>
<div class="logo">
<h1>Mode Tulis</h1>
<p>RFID Writer</p>
</div>
<div class="step-indicator">
<div class="step-item"><div class="step completed">✓</div><div class="step-label">Scan Kartu</div></div>
<div class="step-item"><div class="step active">2</div><div class="step-label">Isi Data</div></div>
<div class="step-item"><div class="step">3</div><div class="step-label">Selesai</div></div>
</div>
<div class="info-card">
<strong>🔖 Kartu terdeteksi</strong>
<div class="uid-display">)rawliteral" + uid + R"rawliteral(</div>
<small>Jangan lepaskan kartu sampai selesai</small>
</div>
<form id="dataForm">
<div class="form-group">
<input type="text" id="nama" placeholder="Nama" required autocomplete="off">
</div>
<div class="form-group">
<input type="text" id="id" placeholder="ID" required autocomplete="off">
</div>
<div class="form-group">
<input type="text" id="kode" placeholder="Kode" required autocomplete="off">
</div>
<button type="submit">Tulis ke Kartu</button>
</form>
<div id="warningBox" class="warning">Pastikan kartu masih menempel</div>
<div class="footer">Data akan ditulis ke blok 4, 5, 6 kartu RFID</div>
</div>
<script>
const form = document.getElementById('dataForm');
const warningBox = document.getElementById('warningBox');
let isWriting = false;
let checkInterval = null;
let timeoutId = null;
const urlParams = new URLSearchParams(window.location.search);
const uid = urlParams.get('uid');
async function waitForCardAndWrite(nama, id, kode, uid) {
let cardDetected = false;
const statusCheck = async () => {
try {
const res = await fetch('/api/status');
const data = await res.json();
if (data.cardDetected && data.uid === uid) {
cardDetected = true;
if (checkInterval) clearInterval(checkInterval);
if (timeoutId) clearTimeout(timeoutId);
await doWrite(nama, id, kode, uid);
} else if (!data.cardDetected) {
warningBox.innerHTML = '⏳ Menunggu kartu terdeteksi...';
warningBox.className = 'warning error';
}
} catch(e) {}
};
checkInterval = setInterval(statusCheck, 300);
timeoutId = setTimeout(() => {
if (checkInterval) clearInterval(checkInterval);
if (!cardDetected) {
const submitBtn = form.querySelector('button');
submitBtn.disabled = false;
submitBtn.textContent = 'Tulis ke Kartu';
warningBox.innerHTML = '❌ Timeout! Kartu tidak terdeteksi.';
warningBox.className = 'warning error';
isWriting = false;
}
}, 5000);
}
async function doWrite(nama, id, kode, uid) {
const submitBtn = form.querySelector('button');
submitBtn.textContent = 'Menulis...';
warningBox.innerHTML = '✍️ Sedang menulis...';
warningBox.className = 'warning';
try {
const res = await fetch('/api/write', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, id, kode, uid })
});
const result = await res.json();
if (result.success) {
window.location.href = '/write/result?success=1';
} else {
window.location.href = '/write/result?success=0&error=' + encodeURIComponent(result.error);
}
} catch(e) {
window.location.href = '/write/result?success=0&error=Network+error';
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (isWriting) return;
isWriting = true;
const nama = document.getElementById('nama').value;
const id = document.getElementById('id').value;
const kode = document.getElementById('kode').value;
if (!nama || !id || !kode) {
warningBox.innerHTML = '❌ Semua field harus diisi!';
warningBox.className = 'warning error';
isWriting = false;
return;
}
const submitBtn = form.querySelector('button');
submitBtn.disabled = true;
submitBtn.textContent = 'Menunggu kartu...';
warningBox.innerHTML = '🔄 Menunggu kartu (maks 5 detik)...';
warningBox.className = 'warning error';
await waitForCardAndWrite(nama, id, kode, uid);
});
async function checkCardStillPresent() {
try {
const res = await fetch('/api/status');
const data = await res.json();
if (!data.cardDetected) {
warningBox.innerHTML = '⚠️ Kartu tidak terdeteksi!';
warningBox.className = 'warning error';
} else if (data.uid === uid) {
warningBox.innerHTML = '✅ Kartu siap, klik tombol tulis';
warningBox.className = 'warning success';
}
} catch(e) {}
}
setInterval(checkCardStillPresent, 1000);
checkCardStillPresent();
</script>
</body>
</html>
)rawliteral";
return html;
}
// ================= HTML PAGE - HASIL TULIS =================
String getWriteResultPage(bool success, String errorMsg = "") {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Innoiti CR-1 - Selesai</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
.wrapper {
max-width: 420px;
width: 100%;
text-align: center;
}
.back-link {
display: inline-block;
color: rgba(255,255,255,0.7);
text-decoration: none;
font-size: 14px;
margin-bottom: 24px;
}
.back-link:hover {
color: white;
}
.logo {
margin-bottom: 32px;
}
.logo h1 {
color: white;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.logo p {
color: rgba(255,255,255,0.7);
font-size: 14px;
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 48px;
padding: 0 16px;
}
.step-item {
text-align: center;
flex: 1;
}
.step {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
color: white;
line-height: 32px;
text-align: center;
font-weight: 600;
font-size: 14px;
margin: 0 auto 8px;
}
.step.completed {
background: white;
color: #106CFF;
}
.step-label {
font-size: 12px;
color: rgba(255,255,255,0.6);
}
.result-area {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(10px);
border-radius: 32px;
padding: 48px 32px;
margin-bottom: 24px;
border: 1px solid rgba(255,255,255,0.1);
}
.result-icon {
font-size: 72px;
margin-bottom: 24px;
}
.result-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 12px;
}
.result-title.success {
color: #4ade80;
}
.result-title.error {
color: #ff6b6b;
}
.result-message {
color: rgba(255,255,255,0.7);
font-size: 14px;
line-height: 1.6;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 24px;
}
button {
flex: 1;
padding: 14px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 16px;
font-size: 14px;
font-weight: 500;
color: white;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: rgba(255,255,255,0.2);
}
.btn-primary {
background: white;
color: #106CFF;
}
.btn-primary:hover {
transform: scale(0.98);
background: #f0f0f0;
}
.footer {
font-size: 12px;
color: rgba(255,255,255,0.4);
margin-top: 24px;
}
</style>
</head>
<body>
<div class="wrapper">
<a href="/" class="back-link">← Kembali ke Menu</a>
<div class="logo">
<h1>Mode Tulis</h1>
<p>RFID Writer</p>
</div>
<div class="step-indicator">
<div class="step-item"><div class="step completed">✓</div><div class="step-label">Scan Kartu</div></div>
<div class="step-item"><div class="step completed">✓</div><div class="step-label">Isi Data</div></div>
<div class="step-item"><div class="step completed">3</div><div class="step-label">Selesai</div></div>
</div>
<div class="result-area">
<div class="result-icon">)rawliteral";
if (success) {
html += R"rawliteral(✅</div>
<div class="result-title success">BERHASIL!</div>
<div class="result-message">Data berhasil ditulis ke kartu RFID.<br>Kartu siap digunakan.</div>)rawliteral";
} else {
html += R"rawliteral(❌</div>
<div class="result-title error">GAGAL!</div>
<div class="result-message">)rawliteral" + errorMsg + R"rawliteral(<br><br>Pastikan kartu MIFARE Classic 1K dan masih menempel.</div>)rawliteral";
}
html += R"rawliteral(
</div>
<div class="button-group">
<button class="btn-primary" onclick="location.href='/write'">Tulis Lagi</button>
<button onclick="location.href='/'">Menu Utama</button>
</div>
<div class="footer">Kartu bisa dilepas sekarang</div>
</div>
</body>
</html>
)rawliteral";
return html;
}
// ================= HTML PAGE - SCAN UNTUK MODE BACA =================
String getReadScanPage() {
return R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Innoiti CR-1 - Baca Kartu</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
.wrapper {
max-width: 420px;
width: 100%;
text-align: center;
}
.back-link {
display: inline-block;
color: rgba(255,255,255,0.7);
text-decoration: none;
font-size: 14px;
margin-bottom: 24px;
}
.back-link:hover {
color: white;
}
.logo {
margin-bottom: 32px;
}
.logo h1 {
color: white;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.logo p {
color: rgba(255,255,255,0.7);
font-size: 14px;
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 48px;
padding: 0 16px;
}
.step-item {
text-align: center;
flex: 1;
}
.step {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
color: white;
line-height: 32px;
text-align: center;
font-weight: 600;
font-size: 14px;
margin: 0 auto 8px;
}
.step.active {
background: white;
color: #106CFF;
}
.step-label {
font-size: 12px;
color: rgba(255,255,255,0.6);
}
.card-area {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(10px);
border-radius: 32px;
padding: 48px 32px;
margin-bottom: 24px;
border: 1px solid rgba(255,255,255,0.1);
}
.icon {
font-size: 64px;
margin-bottom: 24px;
}
.card-area h3 {
color: white;
font-size: 20px;
font-weight: 500;
margin-bottom: 16px;
}
.status {
padding: 12px 20px;
border-radius: 100px;
font-size: 14px;
font-weight: 500;
display: inline-block;
}
.status.waiting {
background: rgba(255,193,7,0.2);
color: #FFD700;
}
.status.success {
background: rgba(40,167,69,0.2);
color: #4ade80;
}
.status.error {
background: rgba(220,53,69,0.2);
color: #ff6b6b;
}
.uid-display {
background: rgba(255,255,255,0.1);
border-radius: 16px;
padding: 12px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 18px;
font-weight: 600;
color: white;
margin-top: 16px;
letter-spacing: 1px;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.footer {
font-size: 12px;
color: rgba(255,255,255,0.5);
margin-top: 24px;
}
</style>
</head>
<body>
<div class="wrapper">
<a href="/" class="back-link">← Kembali ke Menu</a>
<div class="logo">
<h1>Mode Baca</h1>
<p>RFID Reader</p>
</div>
<div class="step-indicator">
<div class="step-item"><div class="step active">1</div><div class="step-label">Scan Kartu</div></div>
<div class="step-item"><div class="step">2</div><div class="step-label">Informasi</div></div>
</div>
<div class="card-area">
<div class="icon">📖</div>
<h3>Tempelkan Kartu RFID</h3>
<div id="statusBox" class="status waiting">⏳ Menunggu kartu...</div>
<div id="uidBox" style="display:none;">
<div class="uid-display" id="uidValue"></div>
</div>
</div>
<div class="footer">Kartu akan otomatis dibaca</div>
</div>
<script>
let cardDetected = false;
let uid = "";
let reading = false;
let readTimeout = null;
let readInterval = null;
async function checkAndReadCard() {
if (reading) return;
try {
const res = await fetch('/api/status');
const data = await res.json();
if (data.cardDetected && !cardDetected) {
cardDetected = true;
uid = data.uid;
document.getElementById('statusBox').innerHTML = '<span class="spinner"></span> Membaca data kartu...';
document.getElementById('statusBox').className = 'status success';
document.getElementById('uidBox').style.display = 'block';
document.getElementById('uidValue').innerHTML = 'UID: ' + uid;
reading = true;
// Mulai proses baca
readInterval = setInterval(async () => {
try {
const readRes = await fetch('/api/read', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uid: uid })
});
const result = await readRes.json();
if (result.success) {
clearInterval(readInterval);
if (readTimeout) clearTimeout(readTimeout);
window.location.href = '/read/info?uid=' + uid + '&nama=' + encodeURIComponent(result.nama) + '&id=' + encodeURIComponent(result.id) + '&kode=' + encodeURIComponent(result.kode);
}
} catch(e) {}
}, 500);
readTimeout = setTimeout(() => {
if (readInterval) clearInterval(readInterval);
document.getElementById('statusBox').innerHTML = '❌ Gagal membaca kartu!';
document.getElementById('statusBox').className = 'status error';
reading = false;
cardDetected = false;
}, 5000);
} else if (!data.cardDetected && cardDetected) {
cardDetected = false;
if (readInterval) clearInterval(readInterval);
if (readTimeout) clearTimeout(readTimeout);
reading = false;
document.getElementById('statusBox').innerHTML = '⏳ Menunggu kartu...';
document.getElementById('statusBox').className = 'status waiting';
document.getElementById('uidBox').style.display = 'none';
}
} catch(e) {}
}
setInterval(checkAndReadCard, 500);
</script>
</body>
</html>
)rawliteral";
}
// ================= HTML PAGE - INFORMASI KARTU (HASIL BACA) =================
String getReadInfoPage(String uid, String nama, String id, String kode) {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Innoiti CR-1 - Informasi Kartu</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
.wrapper {
max-width: 420px;
width: 100%;
text-align: center;
}
.back-link {
display: inline-block;
color: rgba(255,255,255,0.7);
text-decoration: none;
font-size: 14px;
margin-bottom: 24px;
}
.back-link:hover {
color: white;
}
.logo {
margin-bottom: 32px;
}
.logo h1 {
color: white;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.logo p {
color: rgba(255,255,255,0.7);
font-size: 14px;
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 48px;
padding: 0 16px;
}
.step-item {
text-align: center;
flex: 1;
}
.step {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
color: white;
line-height: 32px;
text-align: center;
font-weight: 600;
font-size: 14px;
margin: 0 auto 8px;
}
.step.completed {
background: white;
color: #106CFF;
}
.step-label {
font-size: 12px;
color: rgba(255,255,255,0.6);
}
.info-area {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(10px);
border-radius: 32px;
padding: 32px;
margin-bottom: 24px;
border: 1px solid rgba(255,255,255,0.1);
}
.info-icon {
font-size: 64px;
margin-bottom: 24px;
}
.info-row {
background: rgba(255,255,255,0.08);
border-radius: 16px;
padding: 16px;
margin-bottom: 12px;
text-align: left;
}
.info-label {
font-size: 12px;
color: rgba(255,255,255,0.5);
margin-bottom: 4px;
}
.info-value {
font-size: 18px;
font-weight: 600;
color: white;
word-break: break-all;
}
.info-value.empty {
color: rgba(255,255,255,0.4);
font-style: italic;
}
.uid-value {
font-family: 'SF Mono', Monaco, monospace;
font-size: 14px;
letter-spacing: 0.5px;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 24px;
}
button {
flex: 1;
padding: 14px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 16px;
font-size: 14px;
font-weight: 500;
color: white;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: rgba(255,255,255,0.2);
}
.btn-primary {
background: white;
color: #106CFF;
}
.btn-primary:hover {
transform: scale(0.98);
background: #f0f0f0;
}
.footer {
font-size: 12px;
color: rgba(255,255,255,0.4);
margin-top: 24px;
}
</style>
</head>
<body>
<div class="wrapper">
<a href="/" class="back-link">← Kembali ke Menu</a>
<div class="logo">
<h1>Mode Baca</h1>
<p>RFID Reader</p>
</div>
<div class="step-indicator">
<div class="step-item"><div class="step completed">✓</div><div class="step-label">Scan Kartu</div></div>
<div class="step-item"><div class="step completed">2</div><div class="step-label">Informasi</div></div>
</div>
<div class="info-area">
<div class="info-icon">📇</div>
<div class="info-row">
<div class="info-label">UID Kartu</div>
<div class="info-value uid-value">)rawliteral" + uid + R"rawliteral(</div>
</div>
<div class="info-row">
<div class="info-label">Nama</div>
<div class="info-value )rawliteral" + (nama == "" ? "empty" : "") + R"rawliteral(">)rawliteral" + (nama == "" ? "(kosong)" : nama) + R"rawliteral(</div>
</div>
<div class="info-row">
<div class="info-label">ID</div>
<div class="info-value )rawliteral" + (id == "" ? "empty" : "") + R"rawliteral(">)rawliteral" + (id == "" ? "(kosong)" : id) + R"rawliteral(</div>
</div>
<div class="info-row">
<div class="info-label">Kode</div>
<div class="info-value )rawliteral" + (kode == "" ? "empty" : "") + R"rawliteral(">)rawliteral" + (kode == "" ? "(kosong)" : kode) + R"rawliteral(</div>
</div>
</div>
<div class="button-group">
<button class="btn-primary" onclick="location.href='/read'">Baca Kartu Lain</button>
<button onclick="location.href='/'">Menu Utama</button>
</div>
<div class="footer">Kartu bisa dilepas sekarang</div>
</div>
</body>
</html>
)rawliteral";
return html;
}
// ================= API HANDLER =================
void handleRoot() {
server.send(200, "text/html", getHomePage());
}
void handleWrite() {
server.send(200, "text/html", getWriteScanPage());
}
void handleWriteForm() {
if (server.hasArg("uid")) {
String uid = server.arg("uid");
server.send(200, "text/html", getWriteFormPage(uid));
} else {
server.sendHeader("Location", "/write");
server.send(302, "text/plain", "");
}
}
void handleWriteResult() {
bool success = server.hasArg("success") && server.arg("success") == "1";
String error = server.hasArg("error") ? server.arg("error") : "";
server.send(200, "text/html", getWriteResultPage(success, error));
}
void handleRead() {
server.send(200, "text/html", getReadScanPage());
}
void handleReadInfo() {
if (server.hasArg("uid")) {
String uid = server.arg("uid");
String nama = server.hasArg("nama") ? server.arg("nama") : "";
String id = server.hasArg("id") ? server.arg("id") : "";
String kode = server.hasArg("kode") ? server.arg("kode") : "";
server.send(200, "text/html", getReadInfoPage(uid, nama, id, kode));
} else {
server.sendHeader("Location", "/read");
server.send(302, "text/plain", "");
}
}
void handleApiStatus() {
String json = "{\"cardDetected\":" + String(session.cardDetected ? "true" : "false");
if (session.cardDetected) {
json += ",\"uid\":\"" + session.uid + "\"";
}
json += "}";
server.send(200, "application/json", json);
}
void handleApiRead() {
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"success\":false,\"error\":\"No data\"}");
return;
}
String body = server.arg("plain");
String uid = "";
int idxUid = body.indexOf("\"uid\":\"");
if (idxUid != -1) {
int start = idxUid + 7;
int end = body.indexOf("\"", start);
uid = body.substring(start, end);
}
// Cek apakah kartu masih ada
if (!session.cardDetected || !cardCurrentlyPresent) {
server.send(400, "application/json", "{\"success\":false,\"error\":\"Kartu tidak terdeteksi\"}");
return;
}
// Verifikasi UID cocok
if (session.uid != uid) {
server.send(400, "application/json", "{\"success\":false,\"error\":\"UID kartu berbeda\"}");
return;
}
// Baca data dari kartu
String nama, id, kode;
bool success = readFromCard(nama, id, kode);
if (success) {
String json = "{\"success\":true,\"nama\":\"" + nama + "\",\"id\":\"" + id + "\",\"kode\":\"" + kode + "\"}";
server.send(200, "application/json", json);
} else {
server.send(500, "application/json", "{\"success\":false,\"error\":\"Gagal membaca kartu\"}");
}
}
void handleApiWrite() {
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"success\":false,\"error\":\"No data\"}");
return;
}
String body = server.arg("plain");
String nama = "", id = "", kode = "", uid = "";
int idxNama = body.indexOf("\"nama\":\"");
if (idxNama != -1) {
int start = idxNama + 8;
int end = body.indexOf("\"", start);
nama = body.substring(start, end);
}
int idxId = body.indexOf("\"id\":\"");
if (idxId != -1) {
int start = idxId + 6;
int end = body.indexOf("\"", start);
id = body.substring(start, end);
}
int idxKode = body.indexOf("\"kode\":\"");
if (idxKode != -1) {
int start = idxKode + 8;
int end = body.indexOf("\"", start);
kode = body.substring(start, end);
}
int idxUid = body.indexOf("\"uid\":\"");
if (idxUid != -1) {
int start = idxUid + 7;
int end = body.indexOf("\"", start);
uid = body.substring(start, end);
}
// Cek apakah kartu masih ada
if (!session.cardDetected || !cardCurrentlyPresent) {
server.send(400, "application/json", "{\"success\":false,\"error\":\"Kartu tidak terdeteksi, pastikan masih menempel\"}");
return;
}
// Verifikasi UID cocok
if (session.uid != uid) {
server.send(400, "application/json", "{\"success\":false,\"error\":\"UID kartu berbeda, gunakan kartu yang sama\"}");
return;
}
// Tulis data
session.nama = nama;
session.id = id;
session.kode = kode;
bool success = writeToCard();
// Setelah write, halt kartu dan reset status aktif
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
cardActive = false;
cardCurrentlyPresent = false;
session.cardDetected = false;
if (success) {
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(500, "application/json", "{\"success\":false,\"error\":\"Gagal menulis ke kartu. Pastikan kartu MIFARE Classic 1K\"}");
}
}
// ================= SETUP =================
void setup() {
Serial.begin(115200);
Serial.println("\n");
Serial.println("╔════════════════════════════════════════╗");
Serial.println("║ INNOITI CR-1 RFID READER/WRITER ║");
Serial.println("║ Versi Final - Mode Baca & Tulis ║");
Serial.println("╚════════════════════════════════════════╝\n");
// Setup WiFi AP
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(local_ip, gateway, subnet);
WiFi.softAP(ssid, password);
Serial.printf("📡 WiFi AP: %s\n", ssid);
Serial.printf("🔑 Password: %s\n", password);
Serial.printf("🌐 IP Address: http://%s\n\n", WiFi.softAPIP().toString().c_str());
// Setup RFID
SPI.begin(18, 19, 23, 5);
mfrc522.PCD_Init();
for (byte i = 0; i < 6; i++) key.keyByte[i] = 0xFF;
Serial.println("🔍 MFRC522 RFID Reader siap");
Serial.println("💡 Tempelkan kartu MIFARE Classic 1K\n");
// Setup Web Server
server.on("/", handleRoot);
server.on("/write", handleWrite);
server.on("/write/form", handleWriteForm);
server.on("/write/result", handleWriteResult);
server.on("/read", handleRead);
server.on("/read/info", handleReadInfo);
server.on("/api/status", handleApiStatus);
server.on("/api/read", HTTP_POST, handleApiRead);
server.on("/api/write", HTTP_POST, handleApiWrite);
server.begin();
// Inisialisasi state
session.cardDetected = false;
cardActive = false;
cardCurrentlyPresent = false;
Serial.println("✅ Web server siap!");
Serial.println("========================================\n");
}
// ================= LOOP (STABIL - TIDAK BERKEDIP) =================
void loop() {
server.handleClient();
// Scan setiap 200ms (tidak terlalu cepat)
static unsigned long lastScan = 0;
if (millis() - lastScan < 200) return;
lastScan = millis();
// Cek keberadaan kartu dengan PICC_RequestA (non-intrusive)
byte bufferATQA[2];
byte bufferSize = sizeof(bufferATQA);
MFRC522::StatusCode status = mfrc522.PICC_RequestA(bufferATQA, &bufferSize);
if (status == MFRC522::STATUS_OK) {
// Ada kartu di field
if (!cardCurrentlyPresent) {
// Kartu baru masuk! Baca UID sekali saja
cardCurrentlyPresent = true;
if (mfrc522.PICC_ReadCardSerial()) {
cardActive = true;
copyUid(mfrc522.uid, lastUid);
session.cardDetected = true;
session.uid = uidToString(mfrc522.uid.uidByte, mfrc522.uid.size);
Serial.printf("✅ Kartu terdeteksi: %s\n", session.uid.c_str());
// JANGAN panggil HaltA() - biarkan kartu tetap aktif
}
}
lastCardReadTime = millis();
} else {
// Tidak ada kartu di field
if (cardCurrentlyPresent) {
cardCurrentlyPresent = false;
cardActive = false;
session.cardDetected = false;
session.uid = "";
Serial.println("❌ Kartu dilepas");
mfrc522.PCD_StopCrypto1();
}
}
// Timeout safety: jika lebih dari 1,5 detik tidak terdeteksi, anggap lepas
if (cardCurrentlyPresent && (millis() - lastCardReadTime > 1500)) {
cardCurrentlyPresent = false;
cardActive = false;
session.cardDetected = false;
session.uid = "";
mfrc522.PCD_StopCrypto1();
Serial.println("⚠️ Kartu timeout (dilepas tidak terdeteksi)");
}
}