State Management Logic
State Management Logic
Overview
Section titled “Overview”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
Prerequisites
Section titled “Prerequisites”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
Key Concepts
Section titled “Key Concepts”State Management Levels
Section titled “State Management Levels”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
State Transitions
Section titled “State Transitions”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 │ └─────────┘Implementation Steps
Section titled “Implementation Steps”Step 1: ESP32 Check-In State Tracking
Section titled “Step 1: ESP32 Check-In State Tracking”The ESP32 maintains a local state to track whether operations are in progress:
// ESP32 State Variablesbool isProcessing = false; // Prevents multiple concurrent operationsunsigned long lastTagTime = 0; // Debounce timestampString lastProcessedUID = ""; // Last processed tag UID
// MQTT Callbackvoid 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; }}Step 2: Debounce Logic for RFID Reads
Section titled “Step 2: Debounce Logic for RFID Reads”Prevent rapid re-reads of the same tag:
// Debounce configurationconst unsigned long DEBOUNCE_TIME = 5000; // 5 secondsconst 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); }}Step 3: Server-Side File State Management
Section titled “Step 3: Server-Side File State Management”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 configurationconst FILE_NAME = 'timerecord.txt';const FILE_PATH = path + FILE_NAME;
// Check file statefunction checkFileState(filePath) { try { fs.accessSync(filePath, fs.constants.F_OK); return 'EXISTS'; } catch (err) { return 'NOT_FOUND'; }}
// Read and parse the record filefunction 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 filefunction 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 filefunction 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 stateconst 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;Step 4: Race Condition Prevention
Section titled “Step 4: Race Condition Prevention”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 timeoutif (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 processingif (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 processingprocessingState.isProcessing = true;processingState.lastProcessedUID = currentUID;processingState.processingStartTime = now;context.set('processingState', processingState);
// Store for cleanup after processingcontext.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 statecontext.set('processingState', { isProcessing: false, lastProcessedUID: '', processingStartTime: 0, timeout: 15000});
// Process next in queueif (queue.length > 0) { const nextMsg = queue.shift(); context.set('queue', queue); node.warn('Processing queued tag: ' + nextMsg.payload?.uid); return nextMsg;}
return null; // Nothing queuedStep 6: Retry Logic for Failed API Calls
Section titled “Step 6: Retry Logic for Failed API Calls”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 — continuereturn msg;Verification
Section titled “Verification”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 afterExpected Behavior:
| Scenario | Expected Result |
|---|---|
| Same tag twice in 5 seconds | Second ignored (debounce) |
| Tag A → process → Tag A again | Bookended → check-out |
| Tag A → Tag B during processing | B queued, processed after A |
| API unavailable for check-out | Retry 3 times, then keep file for manual processing |
Troubleshooting
Section titled “Troubleshooting”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 startupvoid requestStateSync() { client.publish("asset/tracking/sync", "REQUEST");}
// In callback, handle sync responseif (strcmp(topic, "asset/tracking/state") == 0) { // Update local state based on server response}Issue 2: File Lock Contention
Section titled “Issue 2: File Lock Contention”Symptom: Two Node-RED flows try to write the same file simultaneously
Solution: Use Node-RED’s built-in locking:
// Use context-based lockingconst 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 lockIssue 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 flowconst 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}Best Practices
Section titled “Best Practices”- ✅ 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)
Summary
Section titled “Summary”- Three state layers: ESP32 (local) → Node-RED (file) → TimeTagger (permanent)
- Debounce: Prevent duplicate reads within a 5-second window
- Race condition handling: Queue competing requests, process sequentially
- Retry logic: Exponential backoff for transient API failures
- Stale state recovery: Handle server restarts without data loss
References
Section titled “References”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