diff --git a/cloud/app.py b/cloud/app.py
index d1bade8..4eeb5d4 100644
--- a/cloud/app.py
+++ b/cloud/app.py
@@ -6,6 +6,7 @@ import base64
import asyncio
import fcntl
import atexit
+import re
from datetime import datetime, timedelta, timezone
from flask import Flask, Response, request, jsonify, current_app, abort, g
from pymongo import MongoClient, ASCENDING, DESCENDING
@@ -45,6 +46,7 @@ alert_collection = db["alert_data"]
webex_tokens_collection = db["webex_tokens"]
device_ip_collection = db["device_ips"]
cve_audit_results = db["cve_audit_results"]
+rfid_cards_collection = db["rfid_cards"]
# Initialize Database Indexes
def init_db_indexes():
@@ -490,6 +492,52 @@ def run_cve_audit():
"timestamp": datetime.now(timezone.utc).isoformat()
}), status_code
+@app.route("/rfid-card", methods=["GET"])
+def get_rfid_cards():
+ """OData compliant endpoint for technician RFID card validation."""
+ filter_str = request.args.get("$filter", "")
+ card_id = request.args.get("card_id")
+
+ mongo_query = {}
+
+ # Extract card_id from OData $filter string if not supplied directly
+ if not card_id and filter_str:
+ match = re.search(r"(?:card_id|cardId)\s+eq\s+['\"]([^'\"]+)['\"]", filter_str, re.IGNORECASE)
+ if match:
+ card_id = match.group(1)
+ else:
+ # Generic fallback to extract quoted strings
+ generic_match = re.search(r"['\"]([a-zA-Z0-9]+)['\"]", filter_str)
+ if generic_match:
+ card_id = generic_match.group(1)
+
+ if card_id:
+ mongo_query["card_id"] = card_id
+
+ # Filter for active/valid cards only
+ mongo_query["valid"] = True
+
+ try:
+ cards = list(rfid_cards_collection.find(mongo_query))
+
+ result_value = [
+ {
+ "id": str(card["_id"]),
+ "card_id": card.get("card_id"),
+ "valid": card.get("valid", False)
+ }
+ for card in cards
+ ]
+
+ return jsonify({
+ "@odata.context": f"{request.host_url.rstrip('/')}/$metadata#RfidCards",
+ "value": result_value
+ }), 200
+
+ except Exception as e:
+ current_app.logger.exception("Error querying rfid_cards collection")
+ return jsonify({"@odata.error": {"code": "500", "message": "Database query error"}}), 500
+
# ---------------------------------------------------------
# Debug Endpoints (Protected)
# ---------------------------------------------------------
@@ -570,11 +618,18 @@ def odata_metadata():
+
+
+
+
+
+
+
diff --git a/orchestrateur/README.md b/orchestrateur/README.md
index d32e0a7..88f4ffc 100644
--- a/orchestrateur/README.md
+++ b/orchestrateur/README.md
@@ -2,4 +2,11 @@
# IP Adresses
- AP (dynamic) : `10.110.0.190`
-- Generated Wifi : `192.168.50.1`
\ No newline at end of file
+- Generated Wifi : `192.168.50.1`
+
+# RFID
+
+## Cards
+
+- 2700423026
+- 270042302B
\ No newline at end of file
diff --git a/orchestrateur/main.py b/orchestrateur/main.py
index 4daedec..52bbae3 100644
--- a/orchestrateur/main.py
+++ b/orchestrateur/main.py
@@ -5,13 +5,12 @@ import asyncio
import requests
from shared.microwave_state import MicrowaveState, MicrowaveStateFields
-from orchestrateur.sensors import gps
from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads, db
from shared.logging import log
from shared.cookingState import CookingStates
from shared.lora_device import LoraCommands
from shared.alerts import AlertManager, Alert, AlertType
-from sensors import ultrasonicRanger, temp_hum, button, camera, buzzer, rgb_lcd
+from sensors import ultrasonicRanger, temp_hum, button, camera, buzzer, rgb_lcd, gps, rfid_reader
from lib.systemd_logs import get_systemd_logs
from external_display_manager import DisplayManager
from rgb_lcd_manager import RGBLCDManager
@@ -21,6 +20,7 @@ rgb_lcd.setText("SmartWave\nOrchestrateur")
# --- CONFIGURATION CONSTANTS ---
DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting
+TECHNICIAN_MODE = False
# --- DB SETUP ---
DB_PATH = "orchestrateur/db.sqlite"
@@ -145,6 +145,9 @@ mqtt_client.connect()
mqtt_client.subscribe(config.MQTT_TOPIC_SENSOR, qos=config.MQTT_QOS)
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
+# RFID reader
+rfid = rfid_reader.RFIDReader(rx_pin=17)
+
# DHT sensor warmup
time.sleep(2) # Allow GrovePi MCU and DHT sensor circuits to settle post-service start
temp_hum.get_temperature_and_humidity() # Dummy read to flush initial NaN state
@@ -174,7 +177,7 @@ if hasattr(cloud_mqtt_client._client, "loop_start"):
print("[MQTT Cloud] Paho background loop started.")
-# --- BACKGROUND TASKS (PRODUCERS) ---
+# --- BACKGROUND TASKS ---
async def lora_listener_task():
"""Polls LoRa and pushes to the async queue."""
print("[LoRa] Async listener started.")
@@ -230,32 +233,47 @@ async def cloud_mqtt_listener_task():
})
await asyncio.sleep(0.1)
-# === LCD DISPLAY ===
-def on_screens_change(screens, is_removed: bool, modified_name: str):
- """Callback for when screens are added or removed."""
- log(f"[DisplayManager] Screens changed. Current screens: {list(screens.keys())}")
- rgb_lcd_manager.set_external_screen_count(len(screens))
+async def rfid_listener_task():
+ """Polls RFID reader, verifies cards via OData API, and toggles TECHNICIAN_MODE."""
+ global TECHNICIAN_MODE
+ print("[RFID] Async task started on GPIO 17.")
- # Add Or remove from database of connected components
- # 1. Extract id from name (assuming format "smartwave-epaper-._displaytcp._tcp.local")
- display_id = modified_name.split("-")[-1].split(".")[0]
- if is_removed:
- remove_connected_component(display_id)
- else:
- save_connected_component(display_id, "external_display")
-
-display_manager.set_on_screens_change_callback(on_screens_change)
+ last_scanned_tag = None
+ last_scan_time = 0
-def update_lcd_microwave_count():
- """Updates the LCD with the current number of connected microwaves."""
- global microwave_states
- count = len(microwave_states)
- rgb_lcd_manager.set_microwave_count(count)
-
-def update_lcd_cloud_alert():
- """Updates the LCD with the current cloud connectivity status."""
- global cloud_alert
- rgb_lcd_manager.set_cloud_alert(cloud_alert)
+ while True:
+ try:
+ # Quick non-blocking read via pigpio IPC
+ tag_id = await asyncio.to_thread(rfid.read_tag)
+ current_time = time.time()
+
+ if tag_id:
+ # Debounce same tag within 3 seconds to avoid spamming the API
+ if tag_id == last_scanned_tag and (current_time - last_scan_time) < 3.0:
+ await asyncio.sleep(0.1)
+ continue
+
+ last_scanned_tag = tag_id
+ last_scan_time = current_time
+ print(f"[RFID] Tag detected: {tag_id}. Verifying with API...")
+
+ is_valid = await check_rfid_card_api(tag_id)
+
+ if is_valid:
+ TECHNICIAN_MODE = True
+ print(f"[RFID] Card {tag_id} VALIDATED! -> TECHNICIAN_MODE = TRUE")
+ # Optional: Sound positive feedback beep
+ buzzer.buzzer_siren(beatsNb=1)
+ else:
+ print(f"[RFID] Card {tag_id} REJECTED / Invalid.")
+ # Optional: Sound error feedback beep
+ buzzer.buzzer_siren(beatsNb=2)
+
+ except Exception as e:
+ print(f"[RFID Task] Error reading RFID tag: {e}")
+
+ # Sleep 100ms between checks to keep CPU usage near zero
+ await asyncio.sleep(0.1)
async def display_broadcast_worker_task():
"""
@@ -288,7 +306,137 @@ async def display_broadcast_worker_task():
microwave_states=microwave_states,
cloud_alert=cloud_alert
)
+
+async def process_messages_task():
+ """Consumes the unified event queue."""
+ # Helper to normalize config topics to str
+ def to_str(val):
+ return val.decode('utf-8') if isinstance(val, bytes) else val
+
+ while True:
+ msg = await async_event_queue.get()
+ source = msg["source"]
+ data = msg["data"]
+
+ if source == "LoRa":
+ if "new_cooking_state" in data.get("data", {}):
+ mw_id = data["data"].get("id")
+ n_state = data["data"].get("new_cooking_state")
+ print(f"[LoRa] Microwave {mw_id} state changed to: {n_state}")
+
+ microwave_states[mw_id].set_cooking_state(n_state)
+ update_lcd_microwave_count() # Update the LCD with new microwave count
+
+ if n_state in (CookingStates.DONE, CookingStates.STIRRING_REQUIRED, CookingStates.ALERT):
+ beats = 5 if n_state == CookingStates.ALERT else (1 if n_state == CookingStates.STIRRING_REQUIRED else 3)
+ buzzer.buzzer_siren(beatsNb=beats)
+ if n_state == CookingStates.IDLE and microwave_states.get(mw_id).state == MicrowaveState.COOKING:
+ microwave_states[mw_id].set_state(MicrowaveState.DONE)
+ print(f"[{mw_id}] Cooking finished. Waiting for user to remove dish.")
+ if "action" in data.get("data", {}):
+ if type(data["data"]) != dict:
+ print(f"[LoRa] Invalid data format received: {data['data']}")
+ continue
+ action = data["data"]["action"]
+ if action == LoraCommands.COOKING_UPDATE:
+ mw_id = data["data"].get("id")
+ if mw_id in microwave_states:
+ state_info = data["data"]
+ microwave_states[mw_id].set_cooking_state(state_info.get("cooking_state", microwave_states[mw_id].cooking_state))
+ microwave_states[mw_id].set_cooking_estimated_remaining_time(state_info.get("estimated_remaining_time", microwave_states[mw_id].cooking_estimated_remaining_time))
+ microwave_states[mw_id].set_paused(state_info.get("paused", microwave_states[mw_id].paused))
+ elif action == LoraCommands.NEW_ALERT:
+ alert_type = data["data"].get("alert_type")
+ alert_message = data["data"].get("alert_message")
+ alert = Alert(alert_type, alert_message, redistributed=True)
+ alert_manager.add_alert(alert)
+ print(f"[LoRa] New alert received: {alert.to_dict()}")
+ else:
+ print(f"[LoRa] Unhandled action received: {action} with data: {data['data']}")
+ elif source == "MQTT":
+ topic = msg["topic"]
+ hello_topic = to_str(config.MQTT_TOPIC_HELLO)
+ sensor_topic = to_str(config.MQTT_TOPIC_SENSOR)
+
+ if topic == hello_topic:
+ if data.get("id_orchestrator") != DEVICE_ID:
+ component_id = data.get("id_microwave")
+ component_type = data.get("type", deviceTypes.DEVICE_TYPES["MICROWAVE"])
+
+ if component_id:
+ print(f"[MQTT] Hello received from '{component_id}' ({component_type}). Updating DB & sending ACK.")
+ # Offload DB insertion to async thread execution pool
+ await asyncio.to_thread(save_connected_component, component_id, component_type)
+
+ # Pass change callback to newly connected microwave
+ microwave_states[component_id] = MicrowaveState(
+ component_id,
+ on_change_callback=on_microwave_change
+ )
+ notify_display_update() # Update the external screens
+ update_lcd_microwave_count() # Update the LCD with new microwave count
+
+ mqtt_client.publish(
+ config.MQTT_TOPIC_HELLO,
+ payloads.mqtt_hello_ack(DEVICE_ID, component_id),
+ qos=config.MQTT_QOS
+ )
+
+ elif topic == sensor_topic:
+ mw_id = str(data.get("id_microwave"))
+ print(f"[MQTT] Sensor data received for microwave {mw_id}: {data}")
+
+ # Store IR data and notify the waiting dish handler
+ ir_data_cache[mw_id] = data
+ if mw_id in ir_data_events:
+ ir_data_events[mw_id].set()
+
+ elif source == "CloudMQTT":
+ topic = msg["topic"]
+ print(f"[CloudMQTT] Command received on '{topic}': {data}")
+
+ action = data.get("action")
+
+ if action == "request_telemetry":
+ endpoint = data.get("endpoint")
+ if endpoint:
+ # Schedule as a background task so processing the queue isn't stalled
+ do_timeout = topic == "cmd/all"
+ asyncio.create_task(handle_telemetry_request(endpoint, do_timeout=do_timeout))
+ else:
+ print("[CloudMQTT] Received 'request_telemetry' but missing 'endpoint' field.")
+ else:
+ print(f"[CloudMQTT] Unknown action: {action}")
+
+# === LCD DISPLAY ===
+def on_screens_change(screens, is_removed: bool, modified_name: str):
+ """Callback for when screens are added or removed."""
+ log(f"[DisplayManager] Screens changed. Current screens: {list(screens.keys())}")
+ rgb_lcd_manager.set_external_screen_count(len(screens))
+
+ # Add Or remove from database of connected components
+ # 1. Extract id from name (assuming format "smartwave-epaper-._displaytcp._tcp.local")
+ display_id = modified_name.split("-")[-1].split(".")[0]
+ if is_removed:
+ remove_connected_component(display_id)
+ else:
+ save_connected_component(display_id, "external_display")
+
+display_manager.set_on_screens_change_callback(on_screens_change)
+
+def update_lcd_microwave_count():
+ """Updates the LCD with the current number of connected microwaves."""
+ global microwave_states
+ count = len(microwave_states)
+ rgb_lcd_manager.set_microwave_count(count)
+
+def update_lcd_cloud_alert():
+ """Updates the LCD with the current cloud connectivity status."""
+ global cloud_alert
+ rgb_lcd_manager.set_cloud_alert(cloud_alert)
+
+# --- Button ---
def button_callback():
"""Button physical interrupt callback."""
global button_state
@@ -428,6 +576,44 @@ async def handle_new_dish(microwave_id, detected_height):
finally:
ir_data_events.pop(microwave_id, None)
+# === API CALLS ===
+
+async def check_rfid_card_api(card_id: str) -> bool:
+ """Queries the OData API asynchronously to verify if card_id is valid."""
+ url = "https://smartwave.matthiasg.dev/rfid-card"
+
+ params = {
+ "$filter": f"card_id eq '{card_id}'"
+ }
+ headers = {"Content-Type": "application/json"}
+ if API_KEY:
+ headers["X-API-Key"] = API_KEY
+
+ try:
+ response = await asyncio.to_thread(
+ requests.get,
+ url,
+ params=params,
+ headers=headers,
+ timeout=5
+ )
+ if response.status_code == 200:
+ set_cloud_alert(False) # Clear cloud alert on success
+ data = response.json()
+
+ if isinstance(data, dict):
+ items = data.get("value", data.get("data", []))
+ return len(items) > 0 if isinstance(items, list) else bool(data)
+ elif isinstance(data, list):
+ return len(data) > 0
+
+ return False
+
+ except Exception as e:
+ print(f"[RFID Task] OData API check error: {e}")
+ set_cloud_alert(True) # Set cloud alert on API connection failure
+ return False
+
async def request_cloud_cooking_plan(microwave_id, sensors_data):
"""Sends all data to the cloud with up to 3 retries (330s timeout for AI generation)."""
global cloud_alert, alert_manager
@@ -535,107 +721,6 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
microwave_states[microwave_id].set_state(MicrowaveState.DONE)
# --- MAIN LOGIC TASKS ---
-async def process_messages_task():
- """Consumes the unified event queue."""
- # Helper to normalize config topics to str
- def to_str(val):
- return val.decode('utf-8') if isinstance(val, bytes) else val
-
- while True:
- msg = await async_event_queue.get()
- source = msg["source"]
- data = msg["data"]
-
- if source == "LoRa":
- if "new_cooking_state" in data.get("data", {}):
- mw_id = data["data"].get("id")
- n_state = data["data"].get("new_cooking_state")
- print(f"[LoRa] Microwave {mw_id} state changed to: {n_state}")
-
- microwave_states[mw_id].set_cooking_state(n_state)
- update_lcd_microwave_count() # Update the LCD with new microwave count
-
- if n_state in (CookingStates.DONE, CookingStates.STIRRING_REQUIRED, CookingStates.ALERT):
- beats = 5 if n_state == CookingStates.ALERT else (1 if n_state == CookingStates.STIRRING_REQUIRED else 3)
- buzzer.buzzer_siren(beatsNb=beats)
- if n_state == CookingStates.IDLE and microwave_states.get(mw_id).state == MicrowaveState.COOKING:
- microwave_states[mw_id].set_state(MicrowaveState.DONE)
- print(f"[{mw_id}] Cooking finished. Waiting for user to remove dish.")
- if "action" in data.get("data", {}):
- if type(data["data"]) != dict:
- print(f"[LoRa] Invalid data format received: {data['data']}")
- continue
- action = data["data"]["action"]
- if action == LoraCommands.COOKING_UPDATE:
- mw_id = data["data"].get("id")
- if mw_id in microwave_states:
- state_info = data["data"]
- microwave_states[mw_id].set_cooking_state(state_info.get("cooking_state", microwave_states[mw_id].cooking_state))
- microwave_states[mw_id].set_cooking_estimated_remaining_time(state_info.get("estimated_remaining_time", microwave_states[mw_id].cooking_estimated_remaining_time))
- microwave_states[mw_id].set_paused(state_info.get("paused", microwave_states[mw_id].paused))
- elif action == LoraCommands.NEW_ALERT:
- alert_type = data["data"].get("alert_type")
- alert_message = data["data"].get("alert_message")
- alert = Alert(alert_type, alert_message, redistributed=True)
- alert_manager.add_alert(alert)
- print(f"[LoRa] New alert received: {alert.to_dict()}")
- else:
- print(f"[LoRa] Unhandled action received: {action} with data: {data['data']}")
- elif source == "MQTT":
- topic = msg["topic"]
-
- hello_topic = to_str(config.MQTT_TOPIC_HELLO)
- sensor_topic = to_str(config.MQTT_TOPIC_SENSOR)
-
- if topic == hello_topic:
- if data.get("id_orchestrator") != DEVICE_ID:
- component_id = data.get("id_microwave")
- component_type = data.get("type", deviceTypes.DEVICE_TYPES["MICROWAVE"])
-
- if component_id:
- print(f"[MQTT] Hello received from '{component_id}' ({component_type}). Updating DB & sending ACK.")
- # Offload DB insertion to async thread execution pool
- await asyncio.to_thread(save_connected_component, component_id, component_type)
-
- # Pass change callback to newly connected microwave
- microwave_states[component_id] = MicrowaveState(
- component_id,
- on_change_callback=on_microwave_change
- )
- notify_display_update() # Update the external screens
- update_lcd_microwave_count() # Update the LCD with new microwave count
-
- mqtt_client.publish(
- config.MQTT_TOPIC_HELLO,
- payloads.mqtt_hello_ack(DEVICE_ID, component_id),
- qos=config.MQTT_QOS
- )
-
- elif topic == sensor_topic:
- mw_id = str(data.get("id_microwave"))
- print(f"[MQTT] Sensor data received for microwave {mw_id}: {data}")
-
- # Store IR data and notify the waiting dish handler
- ir_data_cache[mw_id] = data
- if mw_id in ir_data_events:
- ir_data_events[mw_id].set()
-
- elif source == "CloudMQTT":
- topic = msg["topic"]
- print(f"[CloudMQTT] Command received on '{topic}': {data}")
-
- action = data.get("action")
-
- if action == "request_telemetry":
- endpoint = data.get("endpoint")
- if endpoint:
- # Schedule as a background task so processing the queue isn't stalled
- do_timeout = topic == "cmd/all"
- asyncio.create_task(handle_telemetry_request(endpoint, do_timeout=do_timeout))
- else:
- print("[CloudMQTT] Received 'request_telemetry' but missing 'endpoint' field.")
- else:
- print(f"[CloudMQTT] Unknown action: {action}")
async def handle_telemetry_request(endpoint: str, do_timeout=True):
"""
@@ -787,7 +872,8 @@ async def main():
cloud_mqtt_listener_task(),
process_messages_task(),
monitor_dish_height_task(),
- display_broadcast_worker_task()
+ display_broadcast_worker_task(),
+ rfid_listener_task() # <-- ADDED HERE
)
if __name__ == "__main__":
@@ -796,6 +882,7 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("\nArrĂȘt manuel.")
finally:
+ rfid.close() # <-- ADDED CLEANUP HERE
asyncio.run(display_manager.stop())
if hasattr(mqtt_client._client, "loop_stop"):
mqtt_client._client.loop_stop()
diff --git a/orchestrateur/requirements.txt b/orchestrateur/requirements.txt
index 66d4003..d067341 100644
--- a/orchestrateur/requirements.txt
+++ b/orchestrateur/requirements.txt
@@ -4,4 +4,6 @@ pyserial>=3.5,<4
# OpenCV
# sudo apt install -y python3-opencv
# sudo apt install -y opencv-data
-zeroconf>=0.131.0
\ No newline at end of file
+zeroconf>=0.131.0
+# sudo apt install pigpio python3-pigpio
+# sudo systemctl enable pigpiod --now
\ No newline at end of file
diff --git a/orchestrateur/sensors/__init__.py b/orchestrateur/sensors/__init__.py
index 4f3405b..7bd69b2 100644
--- a/orchestrateur/sensors/__init__.py
+++ b/orchestrateur/sensors/__init__.py
@@ -6,4 +6,5 @@ import sensors.button as button
import sensors.gps as gps
import sensors.camera as camera
import sensors.buzzer as buzzer
-import sensors.rgb_lcd as rgb_lcd
\ No newline at end of file
+import sensors.rgb_lcd as rgb_lcd
+import sensors.rfid_reader as rfid_reader
\ No newline at end of file
diff --git a/orchestrateur/sensors/rfid_reader.py b/orchestrateur/sensors/rfid_reader.py
new file mode 100644
index 0000000..2cc035f
--- /dev/null
+++ b/orchestrateur/sensors/rfid_reader.py
@@ -0,0 +1,95 @@
+import time
+import pigpio
+from typing import Optional
+from sensors.lock import serial_lock
+
+
+class RFIDReader:
+ """
+ Grove 125kHz RFID Reader using bit-banged software serial on GPIO 17 (Pin 11).
+ Parses 14-byte frame: [0x02 STX] + [10 ASCII ID] + [2 ASCII Checksum] + [0x03 ETX]
+ """
+
+ def __init__(self, rx_pin: int = 17, baudrate: int = 9600):
+ self.rx_pin = rx_pin
+ self.baudrate = baudrate
+ self.buffer = bytearray()
+ self.pi = pigpio.pi()
+
+ if not self.pi.connected:
+ raise RuntimeError("pigpio daemon is not running. Run 'sudo systemctl start pigpiod'.")
+
+ with serial_lock:
+ self.pi.set_mode(self.rx_pin, pigpio.INPUT)
+
+ # Clean up lingering serial sessions on this pin
+ try:
+ self.pi.bb_serial_read_close(self.rx_pin)
+ except pigpio.error:
+ pass
+
+ self.pi.bb_serial_read_open(self.rx_pin, self.baudrate, 8)
+
+ def read_tag(self) -> Optional[str]:
+ """
+ Reads and accumulates bytes, returning the 10-digit Tag ID.
+ """
+ with serial_lock:
+ if not self.pi or not self.pi.connected:
+ return None
+
+ count, data = self.pi.bb_serial_read(self.rx_pin)
+ if count > 0:
+ self.buffer.extend(data)
+
+ # Look for Start-of-Text (0x02)
+ stx_idx = self.buffer.find(b"\x02")
+
+ if stx_idx != -1:
+ # Discard noise prior to 0x02
+ if stx_idx > 0:
+ self.buffer = self.buffer[stx_idx:]
+
+ # Wait for full 14-byte payload
+ if len(self.buffer) >= 14:
+ raw_frame = self.buffer[:14]
+ self.buffer = self.buffer[14:] # Flush parsed frame
+
+ # Verify End-of-Text (0x03)
+ if raw_frame[-1] == 0x03:
+ try:
+ # Extract 10-character ID (indices 1 through 10)
+ return raw_frame[1:11].decode("ascii")
+ except UnicodeDecodeError:
+ return None
+ else:
+ # Flush buffer if filled with noise without STX marker
+ if len(self.buffer) > 64:
+ self.buffer.clear()
+
+ return None
+
+ def close(self):
+ with serial_lock:
+ if self.pi and self.pi.connected:
+ try:
+ self.pi.bb_serial_read_close(self.rx_pin)
+ except pigpio.error:
+ pass
+ self.pi.stop()
+
+
+if __name__ == "__main__":
+ reader = RFIDReader(rx_pin=17)
+ print("RFID Reader active on GPIO 17 (Pin 11). Swipe a tag...")
+
+ try:
+ while True:
+ tag = reader.read_tag()
+ if tag:
+ print(f"Scanned Tag ID: {tag}")
+ time.sleep(0.05)
+ except KeyboardInterrupt:
+ print("\nStopping reader.")
+ finally:
+ reader.close()
\ No newline at end of file