Ir al contenido

State Management Logic

Esta página aún no está disponible en tu idioma.

State Management Logic

This section covers the state management logic for the asset tracking system, including the ESP32-side state handling, server-side file management, and race condition prevention. Learning this section will enable you to:

  • Implement ESP32-side check-in/check-out toggle logic
  • Handle server-side state with flat files
  • Prevent race conditions in MQTT communication
  • Implement retry logic for failed API calls

Before starting this section, ensure you have:

  • Check-in/check-out flow implemented in Node-RED (05-08)
  • ESP32 MQTT publishing working (see Chapter 01)
  • Understanding of the file-based state mechanism
  • Basic knowledge of JavaScript async patterns

The asset tracking system manages state at two levels:

State Management Layers:
┌──────────────────────────────────┐
│ Layer 1: ESP32 (Local) │
│ ┌────────────────────────────┐ │
│ │ • ledState (which LED on) │ │
│ │ • lastTagUID (debounce) │ │
│ │ • connectionStatus │ │
│ └────────────────────────────┘ │
├──────────────────────────────────┤
│ Layer 2: Node-RED (Server) │
│ ┌────────────────────────────┐ │
│ │ • timerecord.txt file │ │
│ │ • API auth tokens │ │
│ │ • MQTT subscription cache │ │
│ └────────────────────────────┘ │
├──────────────────────────────────┤
│ Layer 3: TimeTagger (Final) │
│ ┌────────────────────────────┐ │
│ │ • All completed records │ │
│ │ • Reporting data │ │
│ │ • Historical tracking │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
  • Layer 1 (ESP32): Manages LEDs, debounce timers, connection state
  • Layer 2 (Node-RED): Manages pending check-ins via file system
  • Layer 3 (TimeTagger): Stores completed records permanently
Complete State Transition Diagram:
┌─────────┐
│ IDLE │
│ (Waiting)│
└────┬─────┘
│ Tag Detected
▼
┌───────────────┐
│ MQTT Publish │
│ Tag UID+Name │
└───────┬───────┘
│
┌──────▼───────┐
│ Wait for ACK │
│ (MQTT msg) │
└──────┬───────┘
│
┌─────────┴──────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ CHECK-IN │ │ CHECK-OUT│
│ Success │ │ Success │
├──────────┤ ├──────────┤
│ LED GREEN│ │ LED RED │
│ ON 3 sec │ │ ON 3 sec │
└────┬─────┘ └────┬─────┘
│ │
└──────┬──────────┘
▼
┌─────────┐
│ IDLE │
└─────────┘

The ESP32 maintains a local state to track whether operations are in progress:

// ESP32 State Variables
bool isProcessing = false; // Prevents multiple concurrent operations
unsigned long lastTagTime = 0; // Debounce timestamp
String lastProcessedUID = ""; // Last processed tag UID
// MQTT Callback
void callback(char* topic, byte* payload, unsigned int length) {
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
// Parse the feedback message from Node-RED
// Expected: {"action":"CHECK_IN","name":"ESP32_TEAM",...}
DynamicJsonDocument doc(256);
deserializeJson(doc, message);
String action = doc["action"] | "";
if (action == "CHECK_IN") {
// Green LED indicates successful check-in
digitalWrite(LED_GREEN, HIGH);
delay(3000);
digitalWrite(LED_GREEN, LOW);
isProcessing = false;
}
else if (action == "CHECK_OUT") {
// Red LED indicates successful check-out
digitalWrite(LED_RED, HIGH);
delay(3000);
digitalWrite(LED_RED, LOW);
isProcessing = false;
}
}

Prevent rapid re-reads of the same tag:

// Debounce configuration
const unsigned long DEBOUNCE_TIME = 5000; // 5 seconds
const unsigned long PROCESSING_TIMEOUT = 15000; // 15 seconds max
void handleRFIDTag() {
String uid = readTagUID();
if (uid.length() > 0) {
unsigned long now = millis();
// Check debounce: same tag within debounce period
if (uid == lastProcessedUID && (now - lastTagTime) < DEBOUNCE_TIME) {
Serial.println("Debounce: ignoring repeated read");
return;
}
// Check if already processing
if (isProcessing) {
Serial.println("Busy: still waiting for previous operation");
return;
}
// Check for processing timeout
if (isProcessing && (now - lastTagTime) > PROCESSING_TIMEOUT) {
Serial.println("Timeout: resetting processing state");
isProcessing = false;
}
// Process the tag
lastProcessedUID = uid;
lastTagTime = now;
isProcessing = true;
// Publish to MQTT
publishTagData(uid);
}
}

The Node-RED flow manages the JSON flat file:

// Function Node: "File State Manager"
// Handles all file operations with error handling
const fs = require('fs');
const path = '/data/';
// File configuration
const FILE_NAME = 'timerecord.txt';
const FILE_PATH = path + FILE_NAME;
// Check file state
function checkFileState(filePath) {
try {
fs.accessSync(filePath, fs.constants.F_OK);
return 'EXISTS';
} catch (err) {
return 'NOT_FOUND';
}
}
// Read and parse the record file
function readRecordFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const records = JSON.parse(content);
if (Array.isArray(records) && records.length > 0) {
return records[0]; // Return the first record
}
return null;
} catch (err) {
node.error('Failed to read record file: ' + err.message);
return null;
}
}
// Write record to file
function writeRecordFile(filePath, record) {
try {
const content = JSON.stringify([record]);
fs.writeFileSync(filePath, content, 'utf8');
return true;
} catch (err) {
node.error('Failed to write record file: ' + err.message);
return false;
}
}
// Delete the record file
function deleteRecordFile(filePath) {
try {
fs.unlinkSync(filePath);
return true;
} catch (err) {
node.error('Failed to delete record file: ' + err.message);
return false;
}
}
// Determine action based on file state
const fileState = checkFileState(FILE_PATH);
if (fileState === 'NOT_FOUND') {
// --- CHECK-IN ---
msg.action = 'CHECK_IN';
msg.record.t1 = Math.floor(Date.now() / 1000);
if (writeRecordFile(FILE_PATH, msg.record)) {
msg.success = true;
msg.feedback = { action: 'CHECK_IN', name: msg.record.ds };
} else {
msg.success = false;
msg.error = 'Failed to create record file';
}
} else {
// --- CHECK-OUT ---
const storedRecord = readRecordFile(FILE_PATH);
if (storedRecord) {
msg.action = 'CHECK_OUT';
msg.storedRecord = storedRecord;
msg.currentTime = Math.floor(Date.now() / 1000);
msg.success = true;
} else {
msg.success = false;
msg.error = 'Found file but could not read record';
}
}
return msg;

Race conditions can occur when two MQTT messages arrive in quick succession:

// Function Node: "Race Condition Handler"
// Prevents concurrent processing of the same tag
const processingState = context.get('processingState') || {
isProcessing: false,
lastProcessedUID: '',
processingStartTime: 0,
timeout: 15000
};
const currentUID = msg.payload?.uid || '';
const now = Date.now();
// Check timeout
if (processingState.isProcessing) {
const elapsed = now - processingState.processingStartTime;
if (elapsed > processingState.timeout) {
// Timeout — force reset
node.warn('Processing timeout for UID: ' + processingState.lastProcessedUID);
processingState.isProcessing = false;
}
}
// Check for concurrent processing
if (processingState.isProcessing &&
currentUID !== processingState.lastProcessedUID) {
// Different tag is being processed — queue this one
msg.status = 'QUEUED';
node.warn('Another tag is being processed. Queueing: ' + currentUID);
// Store for later processing
const queue = context.get('queue') || [];
queue.push(msg);
context.set('queue', queue);
return null; // Don't process now
}
// Start processing
processingState.isProcessing = true;
processingState.lastProcessedUID = currentUID;
processingState.processingStartTime = now;
context.set('processingState', processingState);
// Store for cleanup after processing
context.set('currentMsg', msg);
return msg;

Step 5: Queue Processing for Delayed Operations

Section titled “Step 5: Queue Processing for Delayed Operations”

After the current operation completes, process any queued messages:

// Function Node: "Process Queue"
// After completing an operation, process the next queued message
const queue = context.get('queue') || [];
// Reset processing state
context.set('processingState', {
isProcessing: false,
lastProcessedUID: '',
processingStartTime: 0,
timeout: 15000
});
// Process next in queue
if (queue.length > 0) {
const nextMsg = queue.shift();
context.set('queue', queue);
node.warn('Processing queued tag: ' + nextMsg.payload?.uid);
return nextMsg;
}
return null; // Nothing queued

Implement retry when the TimeTagger API is temporarily unavailable:

// Function Node: "API Retry Logic"
// Retries failed API calls with exponential backoff
const maxRetries = 3;
const retryCount = msg.retryCount || 0;
if (msg.statusCode === 503 || msg.statusCode === 502 || msg.statusCode === 0) {
// Server error or timeout — retry with backoff
if (retryCount < maxRetries) {
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
msg.retryCount = retryCount + 1;
msg.retryDelay = delay;
node.warn(`API retry ${retryCount + 1}/${maxRetries} in ${delay}ms`);
// Send to a delay node for retry
return msg;
} else {
node.error(`API failed after ${maxRetries} retries`);
msg.apiFailed = true;
// Store the file for later manual processing
return msg;
}
}
// Success — continue
return msg;

Test the state management logic:

// Upload the ESP32 sketch with state management
// 1. Test rapid successive reads
// Hold tag on reader, remove, re-hold within 5 seconds
// Expected: Second read is ignored (debounce)
// 2. Test check-in → check-out cycle
// Scan tag → Green LED → Scan same tag → Red LED
// Expected: Complete cycle works
// 3. Test processing timeout
// Scan tag, kill Node-RED within 15 seconds
// Expected: ESP32 resets processing state after timeout
// 4. Test queue handling
// Scan Tag A, immediately scan Tag B
// Expected: A processed first, B processed after

Expected Behavior:

ScenarioExpected Result
Same tag twice in 5 secondsSecond ignored (debounce)
Tag A → process → Tag A againBookended → check-out
Tag A → Tag B during processingB queued, processed after A
API unavailable for check-outRetry 3 times, then keep file for manual processing

Issue 1: State Drift Between ESP32 and Server

Section titled “Issue 1: State Drift Between ESP32 and Server”

Symptom: ESP32 thinks tag is checked in, but server says otherwise

Solution: Implement state synchronization:

// ESP32: Request current state on startup
void requestStateSync() {
client.publish("asset/tracking/sync", "REQUEST");
}
// In callback, handle sync response
if (strcmp(topic, "asset/tracking/state") == 0) {
// Update local state based on server response
}

Symptom: Two Node-RED flows try to write the same file simultaneously

Solution: Use Node-RED’s built-in locking:

// Use context-based locking
const lock = context.get('fileLock');
if (lock && (Date.now() - lock) < 5000) {
node.warn('File is locked, queuing operation');
return msg; // Route to queue
}
context.set('fileLock', Date.now());
// ...perform file operation...
context.set('fileLock', 0); // Release lock

Issue 3: Missing Check-Out After Node-RED Restart

Section titled “Issue 3: Missing Check-Out After Node-RED Restart”

Symptom: After restart, file still exists → checked in but not notified

Solution: Add startup check:

// In a "Node-RED startup" triggered flow
const fs = require('fs');
const filePath = '/data/timerecord.txt';
try {
fs.accessSync(filePath, fs.constants.F_OK);
node.warn('WARNING: Stale check-in found: ' + filePath);
node.warn('File will be used when next RFID tag is scanned');
// Send notification to admin
} catch (err) {
// No stale file — normal operation
}
  • ✅ Recommended: Always include debounce logic on the ESP32 side
  • ✅ Recommended: Use processing timeouts to recover from failures
  • ✅ Recommended: Implement retry with exponential backoff for API calls
  • ❌ Avoid: Relying solely on ESP32 state — prefer server-authoritative state management
  • ❌ Avoid: Blocking ESP32 loop() during MQTT publishing (use non-blocking patterns)
  1. Three state layers: ESP32 (local) → Node-RED (file) → TimeTagger (permanent)
  2. Debounce: Prevent duplicate reads within a 5-second window
  3. Race condition handling: Queue competing requests, process sequentially
  4. Retry logic: Exponential backoff for transient API failures
  5. Stale state recovery: Handle server restarts without data loss

Writing Date: 2026-05-17
Based on Source File: 校正版/10 Time recording witht RFID und TimeTagger.md
Target Audience: Alibaba.com IoT Pre-sales Engineer
Status: ✅ Completed


Building a commercial IoT product?

We provide ESP32 ODM design-to-manufacturing services. From prototype to production — the team behind this tutorial can build it with you.

Talk to us →