Check-In API Integration
Check-In API Integration
Overview
Section titled “Overview”This section combines the RFID tag reading, HTTP requests, and file-based state management into a complete check-in/check-out flow in Node-RED. Learning this section will enable you to:
- Build a complete Node-RED flow for RFID-based check-in/check-out
- Implement file-based state tracking to distinguish check-in from check-out
- Handle the complete lifecycle: tag scan → file check → create/update → delete
- Debug and test the complete flow end-to-end
Prerequisites
Section titled “Prerequisites”Before starting this section, ensure you have:
- ESP32 reading RFID tags and publishing to MQTT (05-04)
- TimeTagger installed and API token ready (05-05, 05-06)
- HTTP POST request tested (05-07)
- Node-RED installed and MQTT configured
- Node-RED FS (File System) node installed
Key Concepts
Section titled “Key Concepts”Check-In/Check-Out State Machine
Section titled “Check-In/Check-Out State Machine”The system uses a simple state tracking mechanism based on file existence on the Node-RED server:
┌─────────────────────┐ │ RFID Tag Detected │ │ (MQTT message) │ └──────────┬──────────┘ │ ┌───────▼────────┐ │ Check File: │ │ timerecord.txt│ └───────┬────────┘ │ ┌────────────┴────────────┐ │ │ ┌────▼────┐ ┌────▼────┐ │ FILE NOT│ │ FILE │ │ EXISTS │ │ EXISTS │ └────┬────┘ └────┬────┘ │ │ ┌────▼────┐ ┌────▼──────────┐ │ CHECK-IN│ │ CHECK-OUT │ │ │ │ │ │ Create │ │ Read file │ │ JSON │ │ Update t2 │ │ file │ │ POST to API │ │ with t1 │ │ Delete file │ └─────────┘ └────────────────┘Key Logic:
- Check-in (file does not exist): Create a JSON file with start timestamp
- Check-out (file exists): Read file, add end timestamp, POST to TimeTagger, delete file
File-Based State Storage
Section titled “File-Based State Storage”Why use a flat file instead of an in-memory variable?
| Reason | Explanation |
|---|---|
| Persistence | Survives Node-RED restarts |
| Simplicity | No database required |
| Debuggable | Easily inspect file contents |
| Single-user | Suitable for demo/PoC scenarios |
File Location: /data/timerecord.txt inside the Node-RED container
File Format (JSON):
[ { "key": "1715942400a1b2c3d4e5f6", "t1": 1715942400, "t2": 0, "ds": "ESP32_TEAM - check-in", "mt": 1715942400, "st": 0 }]Implementation Steps
Section titled “Implementation Steps”Step 1: Install Required Node-RED Nodes
Section titled “Step 1: Install Required Node-RED Nodes”# In Node-RED container or terminalnpm install node-red-contrib-fs-ops
# Or via Node-RED Palette Manager:# Search for "node-red-contrib-fs-ops" and installAlternatively, use the built-in File System node (if available) or install:
Manage Palette → Install → search "node-red-contrib-fs"Step 2: Create the Complete Flow
Section titled “Step 2: Create the Complete Flow”Below is the full Node-RED flow for the check-in/check-out system:
Flow Structure:[MQTT In: RFID Tag] │ ▼[Function: Build Record Object] │ ▼[FS Access: Check if file exists] │ ├──[OUTPUT 1: File EXISTS → Check-OUT]──→[Function: Process Check-Out] │ │ │ ▼ │ [FS Read: Read file] │ │ │ ▼ │ [Function: Add End Time] │ │ │ ▼ │ [HTTP Request: PUT to API] │ │ │ ▼ │ [FS Remove: Delete file] │ └──[OUTPUT 2: File NOT exists → Check-IN]──→[Function: Process Check-In] │ ▼ [FS Write: Create file]Step 3: MQTT In Node Configuration
Section titled “Step 3: MQTT In Node Configuration”Node: MQTT In (RFID Tag)┌──────────────────────────────────┐│ Server: localhost:1883 ││ Topic: asset/tracking/tag ││ QoS: 1 ││ Output: a parsed JSON object │└──────────────────────────────────┘ESP32 publishes:
{ "uid": "04A3B2C1", "name": "ESP32_TEAM"}Step 4: Build Record Object Function Node
Section titled “Step 4: Build Record Object Function Node”// Function Node: "Build Record Object"// Creates the initial record object from RFID tag data
const authToken = global.get("timetaggerAuth")?.token || "YOUR_TOKEN_HERE";const baseUrl = global.get("timetaggerAuth")?.baseUrl || "http://timetagger:80/api/v2";
// Extract tag info from MQTT messageconst uid = msg.payload.uid || "UNKNOWN";const tagName = msg.payload.name || uid;
// Generate unique keyconst now = Math.floor(Date.now() / 1000);const key = now.toString(16) + Math.random().toString(36).substring(2, 10);
// Store record data for later usemsg.record = { key: key, t1: now, t2: 0, // Will be set on check-out ds: tagName + " - " + uid, mt: now, st: 0};
// Store auth infomsg.authToken = authToken;msg.baseUrl = baseUrl;
// Store file pathmsg.filePath = "/data/timerecord.txt";msg.fileName = "timerecord.txt";
return msg;Step 5: FS Access Node (Check File Existence)
Section titled “Step 5: FS Access Node (Check File Existence)”Node: FS Access (Check File)┌──────────────────────────────────┐│ Operation: Access ││ Filename: msg.filePath ││ Type: msg.fileName ││ Output 1: File accessible ││ Output 2: File NOT accessible │└──────────────────────────────────┘How it works:
- Output 1 (accessible): File exists → This is a CHECK-OUT
- Output 2 (not accessible): File does not exist → This is a CHECK-IN
Step 6: Process Check-In Flow
Section titled “Step 6: Process Check-In Flow”// Function Node: "Process Check-In"// Creates a new JSON file with the initial record
// Get the record dataconst record = msg.record;const filePath = msg.filePath;
// The file content should be a JSON arrayconst fileContent = JSON.stringify([record]);
// Set up the write operationmsg.filename = "timerecord.txt"; // Simple filename (relative to Node-RED data dir)msg.filedata = fileContent;
// Also publish confirmation to MQTT for LED feedbackconst confirmMsg = { topic: "asset/tracking/feedback", payload: JSON.stringify({ action: "CHECK_IN", name: record.ds, key: record.key, t1: record.t1 })};
// Send to both outputsreturn [[msg], [confirmMsg]];FS Write Node:
Node: FS Write (Write File)┌──────────────────────────────────┐│ Operation: Write to file ││ Filename: msg.filename ││ Data: msg.filedata ││ Encoding: utf8 ││ Action: overwrite file ││ Create if missing: yes │└──────────────────────────────────┘Step 7: Process Check-Out Flow
Section titled “Step 7: Process Check-Out Flow”// Function Node: "Process Check-Out"// Reads existing file, updates with end time, sends to API
const record = msg.record;const authToken = msg.authToken;const baseUrl = msg.baseUrl;const now = Math.floor(Date.now() / 1000);
// The FS Read node will put file content in msg.payload// We need to parse it and update the end time
// Store the auth info for later nodesmsg.authToken = authToken;msg.baseUrl = baseUrl;
// Store for the next stepmsg.currentTime = now;
return msg;FS Read Node:
Node: FS Read (Read File)┌──────────────────────────────────┐│ Operation: Read file ││ Filename: msg.filename ││ Encoding: utf8 ││ Output: msg.payload │└──────────────────────────────────┘Step 8: Add End Time and POST to API
Section titled “Step 8: Add End Time and POST to API”// Function Node: "Add End Time and Send"// Updates the record with end time and prepares API request
const authToken = msg.authToken;const baseUrl = msg.baseUrl;const now = msg.currentTime;
// Parse the stored recordlet records;try { records = JSON.parse(msg.payload);} catch (e) { node.error("Failed to parse stored record: " + e.toString()); msg.status = { fill: "red", shape: "dot", text: "parse error" }; return null;}
if (!Array.isArray(records) || records.length === 0) { node.error("Invalid records array in file"); return null;}
// Update the record with end timeconst record = records[0];record.t2 = now; // Set end time to nowrecord.mt = now; // Update modified time
// Prepare HTTP requestmsg.headers = { "Authorization": "Bearer " + authToken, "Content-Type": "application/json"};msg.method = "PUT";msg.url = baseUrl + "/records";msg.payload = [record]; // Array wrapper
// Also prepare MQTT feedback for LEDmsg.feedback = { topic: "asset/tracking/feedback", payload: JSON.stringify({ action: "CHECK_OUT", name: record.ds, key: record.key, t1: record.t1, t2: record.t2, duration: record.t2 - record.t1 })};
return msg;Step 9: Remove File After Check-Out
Section titled “Step 9: Remove File After Check-Out”After successfully posting to the API, delete the file:
Node: FS Remove (Delete File)┌──────────────────────────────────┐│ Operation: Delete file ││ Filename: msg.filename ││ Type: string │└──────────────────────────────────┘Flow completion: After file deletion, send a confirmation MQTT message to the ESP32 for LED feedback.
Step 10: Complete Flow JSON
Section titled “Step 10: Complete Flow JSON”Import this complete flow into Node-RED:
[ { "id": "mqtt-in-tag", "type": "mqtt in", "name": "RFID Tag", "topic": "asset/tracking/tag", "qos": "1", "broker": "localhost", "wires": [["build-record"]] }, { "id": "build-record", "type": "function", "name": "Build Record", "func": "const authToken = global.get(\"timetaggerAuth\")?.token || \"YOUR_TOKEN\";\nconst baseUrl = global.get(\"timetaggerAuth\")?.baseUrl || \"http://timetagger:80/api/v2\";\nconst uid = msg.payload.uid || \"UNKNOWN\";\nconst tagName = msg.payload.name || uid;\nconst now = Math.floor(Date.now() / 1000);\nconst key = now.toString(16) + Math.random().toString(36).substring(2, 10);\nmsg.record = { key: key, t1: now, t2: 0, ds: tagName, mt: now, st: 0 };\nmsg.authToken = authToken;\nmsg.baseUrl = baseUrl;\nmsg.filename = \"timerecord.txt\";\nreturn msg;", "wires": [["fs-access-check"]] }, { "id": "fs-access-check", "type": "fs-access", "name": "Check File", "path": "/data/timerecord.txt", "wire": false, "wires": [["check-out-flow"], ["check-in-flow"]] }, { "id": "check-out-flow", "type": "function", "name": "Check-Out", "func": "msg.authToken = msg.authToken;\nmsg.baseUrl = msg.baseUrl;\nmsg.currentTime = Math.floor(Date.now() / 1000);\nmsg.filename = \"timerecord.txt\";\nreturn msg;", "wires": [["fs-read-file"]] }, { "id": "fs-read-file", "type": "fs-read", "name": "Read File", "filename": "timerecord.txt", "format": "utf8", "wires": [["update-and-send"]] }, { "id": "update-and-send", "type": "function", "name": "Update & POST", "func": "const authToken = msg.authToken;\nconst baseUrl = msg.baseUrl;\nconst now = msg.currentTime;\nlet records;\ntry { records = JSON.parse(msg.payload); } catch(e) { return null; }\nif (!Array.isArray(records) || records.length === 0) return null;\nrecords[0].t2 = now;\nrecords[0].mt = now;\nmsg.headers = { \"Authorization\": \"Bearer \" + authToken, \"Content-Type\": \"application/json\" };\nmsg.method = \"PUT\";\nmsg.url = baseUrl + \"/records\";\nmsg.payload = [records[0]];\nreturn msg;", "wires": [["http-put-api", "mqtt-feedback-out"]] }, { "id": "http-put-api", "type": "http request", "name": "PUT to TimeTagger", "method": "PUT", "ret": "txt", "url": "", "tls": "", "wires": [["fs-remove-file"]] }, { "id": "fs-remove-file", "type": "fs-remove", "name": "Delete File", "path": "timerecord.txt", "wires": [["debug-result"]] }, { "id": "check-in-flow", "type": "function", "name": "Check-In", "func": "const record = msg.record;\nconst fileContent = JSON.stringify([record]);\nmsg.filedata = fileContent;\nmsg.filename = \"timerecord.txt\";\nreturn msg;", "wires": [["fs-write-file", "mqtt-feedback-in"]] }, { "id": "fs-write-file", "type": "fs-write", "name": "Write File", "filename": "timerecord.txt", "format": "utf8", "wires": [] }, { "id": "mqtt-feedback-in", "type": "mqtt out", "name": "Feedback IN", "topic": "asset/tracking/feedback", "qos": "1", "broker": "localhost", "wires": [] }, { "id": "mqtt-feedback-out", "type": "mqtt out", "name": "Feedback OUT", "topic": "asset/tracking/feedback", "qos": "1", "broker": "localhost", "wires": [] }, { "id": "debug-result", "type": "debug", "name": "Result", "active": true, "wires": [] }]Verification
Section titled “Verification”Test the complete flow:
# 1. Simulate a CHECK-IN:mosquitto_pub -t "asset/tracking/tag" \ -m '{"uid":"04A3B2C1","name":"ESP32_TEAM"}'
# Expected:# - File created: /data/timerecord.txt# - MQTT feedback: {"action":"CHECK_IN","name":"ESP32_TEAM",...}
# 2. Verify file exists:docker exec -it nodered cat /data/timerecord.txt# Expected: [{"key":"...","t1":...,"t2":0,"ds":"ESP32_TEAM",...}]
# 3. Simulate a CHECK-OUT (publish same tag again):mosquitto_pub -t "asset/tracking/tag" \ -m '{"uid":"04A3B2C1","name":"ESP32_TEAM"}'
# Expected:# - File read and deleted# - PUT request to TimeTagger API# - New record appears in TimeTagger# - MQTT feedback: {"action":"CHECK_OUT",...}Verification Checklist:
- First tag scan creates file (check-in)
- Second tag scan sends record to API (check-out)
- File is deleted after check-out
- Record appears correctly in TimeTagger
- MQTT feedback messages are published
- System resets and is ready for next check-in
Troubleshooting
Section titled “Troubleshooting”Issue 1: File Not Created on Check-In
Section titled “Issue 1: File Not Created on Check-In”Symptom: FS Access node always goes to “file exists” path
Cause: File path incorrect or permissions issue
Solution:
# Check Node-RED data directorydocker exec -it nodered ls -la /data/# Ensure writabledocker exec -it nodered touch /data/test.txtIssue 2: API Returns Error on Check-Out
Section titled “Issue 2: API Returns Error on Check-Out”Symptom: PUT request fails or returns error
Cause: Record key changed between check-in and check-out
Solution: Store the exact record object and use it unchanged
Issue 3: Double Check-In Detection
Section titled “Issue 3: Double Check-In Detection”Symptom: If file exists but user checks in again
Solution: Handle edge case in the flow:
if (msg.action === "CHECK_IN" && fileExists) { node.warn("User already checked in!"); // Publish error feedback}Best Practices
Section titled “Best Practices”- ✅ Recommended: Always verify the API response before deleting the file
- ✅ Recommended: Add error handling for file read/write failures
- ✅ Recommended: Use unique file names per user if multi-user support is needed
- ❌ Avoid: Hardcoding file paths — use Node-RED environment variables
- ❌ Avoid: Deleting the file before confirming API success
Summary
Section titled “Summary”- State tracking uses a simple JSON flat file on the server
- Check-in: File does not exist → create file with start timestamp
- Check-out: File exists → read file, add end time, POST to API, delete file
- The complete flow integrates MQTT, File System, and HTTP nodes in Node-RED
- MQTT feedback informs the ESP32 to light appropriate LEDs
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