RFID reading and authorization
This commit is contained in:
@@ -6,6 +6,7 @@ import base64
|
|||||||
import asyncio
|
import asyncio
|
||||||
import fcntl
|
import fcntl
|
||||||
import atexit
|
import atexit
|
||||||
|
import re
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from flask import Flask, Response, request, jsonify, current_app, abort, g
|
from flask import Flask, Response, request, jsonify, current_app, abort, g
|
||||||
from pymongo import MongoClient, ASCENDING, DESCENDING
|
from pymongo import MongoClient, ASCENDING, DESCENDING
|
||||||
@@ -45,6 +46,7 @@ alert_collection = db["alert_data"]
|
|||||||
webex_tokens_collection = db["webex_tokens"]
|
webex_tokens_collection = db["webex_tokens"]
|
||||||
device_ip_collection = db["device_ips"]
|
device_ip_collection = db["device_ips"]
|
||||||
cve_audit_results = db["cve_audit_results"]
|
cve_audit_results = db["cve_audit_results"]
|
||||||
|
rfid_cards_collection = db["rfid_cards"]
|
||||||
|
|
||||||
# Initialize Database Indexes
|
# Initialize Database Indexes
|
||||||
def init_db_indexes():
|
def init_db_indexes():
|
||||||
@@ -490,6 +492,52 @@ def run_cve_audit():
|
|||||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||||
}), status_code
|
}), 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)
|
# Debug Endpoints (Protected)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -570,11 +618,18 @@ def odata_metadata():
|
|||||||
<Property Name="webex_status" Type="Edm.String"/>
|
<Property Name="webex_status" Type="Edm.String"/>
|
||||||
<Property Name="sms_sent" Type="Edm.Boolean"/>
|
<Property Name="sms_sent" Type="Edm.Boolean"/>
|
||||||
</EntityType>
|
</EntityType>
|
||||||
|
<EntityType Name="RfidCard">
|
||||||
|
<Key><PropertyRef Name="id"/></Key>
|
||||||
|
<Property Name="id" Type="Edm.String" Nullable="false"/>
|
||||||
|
<Property Name="card_id" Type="Edm.String" Nullable="false"/>
|
||||||
|
<Property Name="valid" Type="Edm.Boolean" Nullable="false"/>
|
||||||
|
</EntityType>
|
||||||
<EntityContainer Name="Container">
|
<EntityContainer Name="Container">
|
||||||
<EntitySet Name="Telemetry" EntityType="MicrowaveNetwork.Telemetry"/>
|
<EntitySet Name="Telemetry" EntityType="MicrowaveNetwork.Telemetry"/>
|
||||||
<EntitySet Name="CookingParams" EntityType="MicrowaveNetwork.CookingParams"/>
|
<EntitySet Name="CookingParams" EntityType="MicrowaveNetwork.CookingParams"/>
|
||||||
<EntitySet Name="CveAudit" EntityType="MicrowaveNetwork.CveAudit"/>
|
<EntitySet Name="CveAudit" EntityType="MicrowaveNetwork.CveAudit"/>
|
||||||
<EntitySet Name="Alerts" EntityType="MicrowaveNetwork.Alert"/>
|
<EntitySet Name="Alerts" EntityType="MicrowaveNetwork.Alert"/>
|
||||||
|
<EntitySet Name="RfidCards" EntityType="MicrowaveNetwork.RfidCard"/>
|
||||||
</EntityContainer>
|
</EntityContainer>
|
||||||
</Schema>
|
</Schema>
|
||||||
</edmx:DataServices>
|
</edmx:DataServices>
|
||||||
|
|||||||
@@ -3,3 +3,10 @@
|
|||||||
|
|
||||||
- AP (dynamic) : `10.110.0.190`
|
- AP (dynamic) : `10.110.0.190`
|
||||||
- Generated Wifi : `192.168.50.1`
|
- Generated Wifi : `192.168.50.1`
|
||||||
|
|
||||||
|
# RFID
|
||||||
|
|
||||||
|
## Cards
|
||||||
|
|
||||||
|
- 2700423026
|
||||||
|
- 270042302B
|
||||||
+214
-127
@@ -5,13 +5,12 @@ import asyncio
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from shared.microwave_state import MicrowaveState, MicrowaveStateFields
|
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 import get_lora, get_mqtt_client, deviceTypes, config, payloads, db
|
||||||
from shared.logging import log
|
from shared.logging import log
|
||||||
from shared.cookingState import CookingStates
|
from shared.cookingState import CookingStates
|
||||||
from shared.lora_device import LoraCommands
|
from shared.lora_device import LoraCommands
|
||||||
from shared.alerts import AlertManager, Alert, AlertType
|
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 lib.systemd_logs import get_systemd_logs
|
||||||
from external_display_manager import DisplayManager
|
from external_display_manager import DisplayManager
|
||||||
from rgb_lcd_manager import RGBLCDManager
|
from rgb_lcd_manager import RGBLCDManager
|
||||||
@@ -21,6 +20,7 @@ rgb_lcd.setText("SmartWave\nOrchestrateur")
|
|||||||
|
|
||||||
# --- CONFIGURATION CONSTANTS ---
|
# --- CONFIGURATION CONSTANTS ---
|
||||||
DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting
|
DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting
|
||||||
|
TECHNICIAN_MODE = False
|
||||||
|
|
||||||
# --- DB SETUP ---
|
# --- DB SETUP ---
|
||||||
DB_PATH = "orchestrateur/db.sqlite"
|
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_SENSOR, qos=config.MQTT_QOS)
|
||||||
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, 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
|
# DHT sensor warmup
|
||||||
time.sleep(2) # Allow GrovePi MCU and DHT sensor circuits to settle post-service start
|
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
|
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.")
|
print("[MQTT Cloud] Paho background loop started.")
|
||||||
|
|
||||||
|
|
||||||
# --- BACKGROUND TASKS (PRODUCERS) ---
|
# --- BACKGROUND TASKS ---
|
||||||
async def lora_listener_task():
|
async def lora_listener_task():
|
||||||
"""Polls LoRa and pushes to the async queue."""
|
"""Polls LoRa and pushes to the async queue."""
|
||||||
print("[LoRa] Async listener started.")
|
print("[LoRa] Async listener started.")
|
||||||
@@ -230,32 +233,47 @@ async def cloud_mqtt_listener_task():
|
|||||||
})
|
})
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
# === LCD DISPLAY ===
|
async def rfid_listener_task():
|
||||||
def on_screens_change(screens, is_removed: bool, modified_name: str):
|
"""Polls RFID reader, verifies cards via OData API, and toggles TECHNICIAN_MODE."""
|
||||||
"""Callback for when screens are added or removed."""
|
global TECHNICIAN_MODE
|
||||||
log(f"[DisplayManager] Screens changed. Current screens: {list(screens.keys())}")
|
print("[RFID] Async task started on GPIO 17.")
|
||||||
rgb_lcd_manager.set_external_screen_count(len(screens))
|
|
||||||
|
|
||||||
# Add Or remove from database of connected components
|
last_scanned_tag = None
|
||||||
# 1. Extract id from name (assuming format "smartwave-epaper-<id>._displaytcp._tcp.local")
|
last_scan_time = 0
|
||||||
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)
|
while True:
|
||||||
|
try:
|
||||||
|
# Quick non-blocking read via pigpio IPC
|
||||||
|
tag_id = await asyncio.to_thread(rfid.read_tag)
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
def update_lcd_microwave_count():
|
if tag_id:
|
||||||
"""Updates the LCD with the current number of connected microwaves."""
|
# Debounce same tag within 3 seconds to avoid spamming the API
|
||||||
global microwave_states
|
if tag_id == last_scanned_tag and (current_time - last_scan_time) < 3.0:
|
||||||
count = len(microwave_states)
|
await asyncio.sleep(0.1)
|
||||||
rgb_lcd_manager.set_microwave_count(count)
|
continue
|
||||||
|
|
||||||
def update_lcd_cloud_alert():
|
last_scanned_tag = tag_id
|
||||||
"""Updates the LCD with the current cloud connectivity status."""
|
last_scan_time = current_time
|
||||||
global cloud_alert
|
print(f"[RFID] Tag detected: {tag_id}. Verifying with API...")
|
||||||
rgb_lcd_manager.set_cloud_alert(cloud_alert)
|
|
||||||
|
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():
|
async def display_broadcast_worker_task():
|
||||||
"""
|
"""
|
||||||
@@ -289,6 +307,136 @@ async def display_broadcast_worker_task():
|
|||||||
cloud_alert=cloud_alert
|
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-<id>._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():
|
def button_callback():
|
||||||
"""Button physical interrupt callback."""
|
"""Button physical interrupt callback."""
|
||||||
global button_state
|
global button_state
|
||||||
@@ -428,6 +576,44 @@ async def handle_new_dish(microwave_id, detected_height):
|
|||||||
finally:
|
finally:
|
||||||
ir_data_events.pop(microwave_id, None)
|
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):
|
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)."""
|
"""Sends all data to the cloud with up to 3 retries (330s timeout for AI generation)."""
|
||||||
global cloud_alert, alert_manager
|
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)
|
microwave_states[microwave_id].set_state(MicrowaveState.DONE)
|
||||||
|
|
||||||
# --- MAIN LOGIC TASKS ---
|
# --- 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):
|
async def handle_telemetry_request(endpoint: str, do_timeout=True):
|
||||||
"""
|
"""
|
||||||
@@ -787,7 +872,8 @@ async def main():
|
|||||||
cloud_mqtt_listener_task(),
|
cloud_mqtt_listener_task(),
|
||||||
process_messages_task(),
|
process_messages_task(),
|
||||||
monitor_dish_height_task(),
|
monitor_dish_height_task(),
|
||||||
display_broadcast_worker_task()
|
display_broadcast_worker_task(),
|
||||||
|
rfid_listener_task() # <-- ADDED HERE
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -796,6 +882,7 @@ if __name__ == "__main__":
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nArrêt manuel.")
|
print("\nArrêt manuel.")
|
||||||
finally:
|
finally:
|
||||||
|
rfid.close() # <-- ADDED CLEANUP HERE
|
||||||
asyncio.run(display_manager.stop())
|
asyncio.run(display_manager.stop())
|
||||||
if hasattr(mqtt_client._client, "loop_stop"):
|
if hasattr(mqtt_client._client, "loop_stop"):
|
||||||
mqtt_client._client.loop_stop()
|
mqtt_client._client.loop_stop()
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ pyserial>=3.5,<4
|
|||||||
# sudo apt install -y python3-opencv
|
# sudo apt install -y python3-opencv
|
||||||
# sudo apt install -y opencv-data
|
# sudo apt install -y opencv-data
|
||||||
zeroconf>=0.131.0
|
zeroconf>=0.131.0
|
||||||
|
# sudo apt install pigpio python3-pigpio
|
||||||
|
# sudo systemctl enable pigpiod --now
|
||||||
@@ -7,3 +7,4 @@ import sensors.gps as gps
|
|||||||
import sensors.camera as camera
|
import sensors.camera as camera
|
||||||
import sensors.buzzer as buzzer
|
import sensors.buzzer as buzzer
|
||||||
import sensors.rgb_lcd as rgb_lcd
|
import sensors.rgb_lcd as rgb_lcd
|
||||||
|
import sensors.rfid_reader as rfid_reader
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user