State Management Logic
Esta página aún no está disponible en tu idioma.
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
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 →