diff --git a/backend/app/management/commands/mqtt_listener.py b/backend/app/management/commands/mqtt_listener.py index b9c8ac0..b263dbe 100644 --- a/backend/app/management/commands/mqtt_listener.py +++ b/backend/app/management/commands/mqtt_listener.py @@ -1,41 +1,130 @@ from django.core.management.base import BaseCommand import paho.mqtt.client as mqtt +import json +import socket +import time +import uuid import requests class Command(BaseCommand): - help = "Starts the MQTT client to listen for QR code scans" + help = "Starts the MQTT client to listen for QR code scans and publish presence" def handle(self, *args, **kwargs): self.stdout.write("Starting MQTT client...") - MQTT_BROKER = "mqtt.eclipseprojects.io" - MQTT_TOPIC = "warehouse/qr" + # MQTT Broker Details + MQTT_BROKER = "broker.emqx.io" + MQTT_PORT = 1883 # Standard MQTT port + PRESENCE_TOPIC = "device/raspberry-pi/presence" + QR_TOPIC = "manufacturing/anomalies" BACKEND_URL = "http://127.0.0.1:8000/api/store_qr/" # Update if needed - DEVICE_STATUS_URL = "http://127.0.0.1:8000/api/device_status/" # Endpoint to update device status - def on_connect(client, userdata, flags, rc): - self.stdout.write(f"Connected to MQTT with result code {rc}") - client.subscribe(MQTT_TOPIC) + AUTH_URL = "http://127.0.0.1:8000/api/token/" # Authentication endpoint + USERNAME = "joshuaa" + PASSWORD = "Sanjos123*" - def on_message(client, userdata, msg): - data = msg.payload.decode() - self.stdout.write(f"Received QR data: {data}") + def get_unique_client_id(): + """Generate a unique client ID for the device""" + return f"mqtt-listener-{uuid.uuid4().hex[:8]}" - + def create_presence_payload(client_id, status): + """Create a presence payload with device information""" + return json.dumps({ + "client_id": client_id, + "hostname": socket.gethostname(), + "status": status, + "timestamp": int(time.time()) + }) - # Send HTTP POST request to backend + def get_jwt_token(): + """Fetch JWT token from the authentication endpoint""" try: - response = requests.post(BACKEND_URL, json={"qr_text": data}) # Ensure the key is 'qr_text' - if response.status_code == 200: - self.stdout.write(f"[✅] QR Data sent to backend: {response.json()}") - else: - self.stdout.write(f"[❌] Failed to send QR data. Status: {response.status_code}") + response = requests.post(AUTH_URL, data={"username": USERNAME, "password": PASSWORD}) + response.raise_for_status() + return response.json().get("access") except requests.RequestException as e: - self.stdout.write(f"[⚠] Error sending QR data via HTTP: {e}") + self.stdout.write(f"[⚠] Error fetching JWT token: {e}") + return None + + def on_connect(client, userdata, flags, rc): + """Callback when the client connects to the broker""" + self.stdout.write(f"Connected with result code {rc}") + + # Subscribe to the QR code topic + client.subscribe(QR_TOPIC) + + # Publish online status when connected + presence_payload = create_presence_payload(client._client_id.decode(), "online") + client.publish(PRESENCE_TOPIC, presence_payload, qos=1, retain=True) + self.stdout.write(f"[📡] Published online presence: {presence_payload}") + + def on_disconnect(client, userdata, rc): + """Callback when the client disconnects""" + self.stdout.write(f"Disconnected with result code {rc}") - client = mqtt.Client() + def on_message(client, userdata, msg): + """Callback when a message is received""" + if msg.topic == QR_TOPIC: + # Handle QR code messages + data = msg.payload.decode() + self.stdout.write(f"Received QR data: {data}") + + jwt_token = get_jwt_token() + if not jwt_token: + self.stdout.write("[❌] Failed to obtain JWT token") + return + + headers = { + "Authorization": f"Bearer {jwt_token}", + "Content-Type": "application/json" + } + + # Send HTTP POST request to backend + try: + response = requests.post(BACKEND_URL, json={"qr_text": data}, headers=headers) + if response.status_code in [200, 201]: + self.stdout.write(f"[✅] QR Data sent to backend: {response.json()}") + else: + self.stdout.write(f"[❌] Failed to send QR data. Status: {response.status_code}") + except requests.RequestException as e: + self.stdout.write(f"[⚠] Error sending QR data via HTTP: {e}") + + # Generate a unique client ID + client_id = get_unique_client_id() + + # Create MQTT client + client = mqtt.Client(client_id=client_id, clean_session=True) + + # Set up connection callbacks client.on_connect = on_connect + client.on_disconnect = on_disconnect client.on_message = on_message - client.connect(MQTT_BROKER, 1883, 60) - - self.stdout.write("MQTT client started, waiting for messages...") - client.loop_forever() # Keeps the process running \ No newline at end of file + + # Set up Last Will and Testament to publish "offline" status when disconnected + will_payload = create_presence_payload(client_id, "offline") + client.will_set(PRESENCE_TOPIC, payload=will_payload, qos=1, retain=True) + + try: + # Connect to the broker + client.connect(MQTT_BROKER, MQTT_PORT, 60) + self.stdout.write("MQTT client started, waiting for messages...") + + # Start the MQTT loop + client.loop_start() + + # Periodically send presence messages + while True: + presence_payload = create_presence_payload(client_id, "online") + client.publish(PRESENCE_TOPIC, presence_payload, qos=1, retain=True) + self.stdout.write(f"[📡] Published presence: {presence_payload}") + time.sleep(30) # Send presence message every 30 seconds + + except Exception as e: + self.stdout.write(f"Error: {str(e)}") + + finally: + # Publish offline status before disconnecting + offline_payload = create_presence_payload(client_id, "offline") + client.publish(PRESENCE_TOPIC, offline_payload, qos=1, retain=True) + client.loop_stop() + client.disconnect() + self.stdout.write("MQTT client stopped.") \ No newline at end of file diff --git a/backend/main/settings.py b/backend/main/settings.py index 12a03a0..56c72f0 100644 --- a/backend/main/settings.py +++ b/backend/main/settings.py @@ -83,9 +83,9 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'smartchain_db', - 'USER': 'smartchain', - 'PASSWORD': 'venkat*2005', + 'NAME': 'postgres', + 'USER': 'joshuaa', + 'PASSWORD': 'Sanjos123*', 'HOST': 'localhost', # If running PostgreSQL locally 'PORT': '5432', # Default PostgreSQL port } diff --git a/frontend/app/manufacturer/page.tsx b/frontend/app/manufacturer/page.tsx index 0c09b27..70f8a56 100644 --- a/frontend/app/manufacturer/page.tsx +++ b/frontend/app/manufacturer/page.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useEffect, useState, useCallback } from "react"; +import React, { useEffect, useState, useCallback, useRef } from "react"; +import mqtt from "mqtt"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -34,6 +35,8 @@ import { TrendingUpIcon, TruckIcon, UsersIcon, + WifiIcon, + XIcon, } from "lucide-react"; // Interfaces @@ -86,6 +89,13 @@ interface ShipmentResponse { results: Shipment[]; } +// Interface for anomaly notifications +interface AnomalyNotification { + id: string; + message: string; + timestamp: Date; +} + // Hardcoded data for fallback const testData: OverviewCard = { totalOrders: 0, @@ -182,6 +192,10 @@ export const payments: Payment[] = [ // API URL for fetching counts const API_URL = "http://127.0.0.1:8000/api"; +// MQTT Topics +const ANOMALY_TOPIC = "manufacturing/anomalies"; +const RASPBERRY_PI_PRESENCE_TOPIC = "device/raspberry-pi/presence/raspberrypi"; + const Dashboard: React.FC = () => { const [overviewData, setOverviewData] = useState(testData); const [analytics] = useState(analyticsData); @@ -194,6 +208,11 @@ const Dashboard: React.FC = () => { const [shipmentsError, setShipmentsError] = useState(null); const [allocateLoading, setAllocateLoading] = useState(false); const [allocateError, setAllocateError] = useState(null); + const [mqttConnected, setMqttConnected] = useState(false); + const [raspberryPiConnected, setRaspberryPiConnected] = useState(false); + const [anomalyNotifications, setAnomalyNotifications] = useState([]); + const lastRpiHeartbeatTime = useRef(0); + // Define columns for the shipment data table const shipmentColumns = [ { @@ -245,6 +264,24 @@ const Dashboard: React.FC = () => { }, ]; + // Function to add a new anomaly notification + const addAnomalyNotification = useCallback(() => { + const newNotification: AnomalyNotification = { + id: Date.now().toString(), + message: "Anomaly Detected", + timestamp: new Date(), + }; + + setAnomalyNotifications((prev) => [newNotification, ...prev]); + + // Auto-hide the toast notification after 5 seconds + setTimeout(() => { + setAnomalyNotifications((prev) => + prev.filter((notification) => notification.id !== newNotification.id) + ); + }, 5000); + }, []); + // Get auth token with error handling const getAuthToken = useCallback(() => { const token = localStorage.getItem("access_token"); @@ -253,27 +290,27 @@ const Dashboard: React.FC = () => { } return token; }, []); - + const getRefreshToken = () => localStorage.getItem("refresh_token"); - + const refreshAccessToken = async (): Promise => { const refreshToken = getRefreshToken(); if (!refreshToken) return null; - + try { const response = await fetch(`${API_URL}/token/refresh/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh: refreshToken }), }); - + if (!response.ok) { console.error("Failed to refresh token"); localStorage.removeItem("access_token"); localStorage.removeItem("refresh_token"); return null; } - + const data = await response.json(); if (data.access) { localStorage.setItem("access_token", data.access); @@ -282,13 +319,14 @@ const Dashboard: React.FC = () => { } catch (error) { console.error("Error refreshing token:", error); } - + return null; }; + const fetchWithAuth = async (url: string, options: RequestInit = {}) => { let token = await getAuthToken(); if (!token) throw new Error("Authentication token not found. Please log in again."); - + const response = await fetch(url, { ...options, headers: { @@ -297,11 +335,11 @@ const Dashboard: React.FC = () => { "Content-Type": "application/json", }, }); - + if (response.status === 401) { token = await refreshAccessToken(); if (!token) throw new Error("Authentication token not found. Please log in again."); - + return fetch(url, { ...options, headers: { @@ -311,10 +349,10 @@ const Dashboard: React.FC = () => { }, }); } - + return response; }; - + // Fetch shipments data with improved error handling const fetchShipments = useCallback(async () => { try { @@ -356,14 +394,13 @@ const Dashboard: React.FC = () => { if (!response.ok) { const errorData = await response.json().catch(() => ({})); if (errorData.error) { - setAllocateError(errorData.error); // Set the specific error message + setAllocateError(errorData.error); } else { throw new Error( errorData.detail || `Server responded with status ${response.status}` ); } } else { - // After successful allocation, refresh the data await Promise.all([fetchShipments(), fetchCounts()]); alert("Orders allocated successfully!"); } @@ -394,7 +431,7 @@ const Dashboard: React.FC = () => { const countsData: CountsResponse = await response.json(); setOverviewData((prevData) => ({ - totalOrders: countsData.orders_placed, // Keep existing value since it's not in the API + totalOrders: countsData.orders_placed, numStores: countsData.retailers_available, deliveryAgents: countsData.employees_available, pendingOrders: countsData.pending_orders, @@ -411,26 +448,174 @@ const Dashboard: React.FC = () => { // Set up polling with cleanup useEffect(() => { - // Initial fetch fetchCounts(); fetchShipments(); - // Set up polling const countsIntervalId = setInterval(fetchCounts, 5000); const shipmentsIntervalId = setInterval(fetchShipments, 30000); - // Clean up intervals on unmount return () => { clearInterval(countsIntervalId); clearInterval(shipmentsIntervalId); }; }, [fetchCounts, fetchShipments]); + // MQTT connection setup + useEffect(() => { + console.log("Attempting to connect to MQTT broker..."); + setMqttConnected(false); + setRaspberryPiConnected(false); + + const client = mqtt.connect("wss://broker.emqx.io:8084/mqtt", { + clientId: `mqttjs_${Math.random().toString(16).substr(2, 8)}`, + connectTimeout: 30000, + reconnectPeriod: 5000, + }); + + client.on("connect", () => { + console.log("MQTT connected successfully"); + setMqttConnected(true); + + // Subscribe to Raspberry Pi presence topic + client.subscribe(RASPBERRY_PI_PRESENCE_TOPIC, { qos: 1 }, (err: Error | null) => { + if (err) { + console.error("Failed to subscribe to Raspberry Pi presence topic:", err); + } + }); + + // Subscribe to anomaly topic + client.subscribe(ANOMALY_TOPIC, { qos: 1 }, (err: Error | null) => { + if (err) { + console.error("Failed to subscribe to anomaly topic:", err); + } + }); + }); + + client.on("message", (topic: string, message: Buffer) => { + const messageStr = message.toString(); + + if (topic === RASPBERRY_PI_PRESENCE_TOPIC) { + console.log("Raspberry Pi presence detected:", messageStr); + try { + const payload = JSON.parse(messageStr); + lastRpiHeartbeatTime.current = Date.now(); + setRaspberryPiConnected(payload.status === "online"); + } catch (err) { + console.error("Failed to parse Raspberry Pi presence message:", err); + setRaspberryPiConnected(false); + } + return; + } + + if (topic === ANOMALY_TOPIC) { + const payload = messageStr.trim().toLowerCase(); + console.log(`Received message on topic ${topic}: ${payload}`); + + if (payload === "anomaly detected: no qr code detected for over 10 seconds!") { + console.log("Anomaly detected! Adding notification."); + addAnomalyNotification(); + } + } + }); + + client.on("error", (err: Error) => { + console.error("MQTT connection error:", err); + setMqttConnected(false); + setRaspberryPiConnected(false); + }); + + client.on("close", () => { + console.log("MQTT connection closed"); + setMqttConnected(false); + setRaspberryPiConnected(false); + }); + + const rpiCheckTimer: NodeJS.Timeout = setInterval(() => { + if (Date.now() - lastRpiHeartbeatTime.current > 30000) { + if (raspberryPiConnected) { + console.log("No recent heartbeat from Raspberry Pi, marking as disconnected"); + setRaspberryPiConnected(false); + } + } + }, 10000); + + return () => { + console.log("Cleaning up MQTT client"); + clearInterval(rpiCheckTimer); + if (client) { + client.unsubscribe(ANOMALY_TOPIC); + client.unsubscribe(RASPBERRY_PI_PRESENCE_TOPIC); + client.end(true); + } + }; + }, [addAnomalyNotification]); + + // Determine overall connection status + const isFullyConnected = mqttConnected && raspberryPiConnected; + return (
-

- Dashboard -

+ + +
+

Dashboard

+
+ {/* Connection Status Icon */} + {isFullyConnected ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ + {/* Toast Notifications for Anomalies */} + {anomalyNotifications.length > 0 && ( +
+ {anomalyNotifications.slice(0, 1).map((notification) => ( +
+ +
+

{notification.message}

+

+ {notification.timestamp.toLocaleTimeString()} +

+
+ +
+ ))} +
+ )} @@ -448,7 +633,6 @@ const Dashboard: React.FC = () => { - {/* Authentication Error */} {error && error.includes("Authentication") && (
@@ -456,9 +640,6 @@ const Dashboard: React.FC = () => {
)} - - - {/* Other Errors */} {error && !error.includes("Authentication") && (
@@ -466,14 +647,11 @@ const Dashboard: React.FC = () => {
)} - {/* Display Data */}
- {/* Top Section - Four Cards */}

- Total - Orders + Total Orders

{overviewData.totalOrders} @@ -520,19 +698,13 @@ const Dashboard: React.FC = () => {

- {/* Bottom Section - Chart and Data Table*/}
- {/* Chart Section */}

- Monthly - Sales + Monthly Sales

- + {
- {/* Data Table Section - Updated for Shipment Data */}

Order Details -

) : (
-

- No shipment data available. -

+

No shipment data available.

- {/* Analytics Tab */}
-

- Daily Orders -

-

- {analytics.dailyOrders} -

+

Daily Orders

+

{analytics.dailyOrders}

-

- Avg. Order Value -

-

- ${analytics.avgOrderValue.toFixed(2)} -

+

Avg. Order Value

+

${analytics.avgOrderValue.toFixed(2)}

-

- Returning Customers -

-

- {analytics.returningCustomers} -

+

Returning Customers

+

{analytics.returningCustomers}

-

- Conversion Rate -

-

- {analytics.conversionRate}% -

+

Conversion Rate

+

{analytics.conversionRate}%

- {/* Reports Tab */}
-

- Monthly Revenue -

-

- ${reports.monthlyRevenue.toLocaleString()} -

+

Monthly Revenue

+

${reports.monthlyRevenue.toLocaleString()}

-

- Monthly Expenses -

-

- ${reports.monthlyExpenses.toLocaleString()} -

+

Monthly Expenses

+

${reports.monthlyExpenses.toLocaleString()}

-

- Profit -

-

- ${reports.profit.toLocaleString()} -

+

Profit

+

${reports.profit.toLocaleString()}

-

- Customer Satisfaction -

-

- {reports.customerSatisfaction}% -

+

Customer Satisfaction

+

{reports.customerSatisfaction}%

- {/* Notifications Tab */}
-

- Recent Notifications -

+

Recent Notifications

{notif.length > 0 ? (
    {notif.map((note) => ( @@ -702,9 +831,7 @@ const Dashboard: React.FC = () => { ))}
) : ( -

- No notifications available. -

+

No notifications available.

)}
@@ -713,4 +840,4 @@ const Dashboard: React.FC = () => { ); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a18acde..64fb739 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "clsx": "^2.1.1", "electron-serve": "^2.1.1", "lucide-react": "^0.475.0", + "mqtt": "^5.10.4", "next": "^15.1.7", "next-themes": "^0.4.4", "radix-ui": "^1.1.3", @@ -33,6 +34,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@types/mqtt": "^2.5.0", "@types/node": "20.17.17", "@types/react": "^19", "@types/react-dom": "^19", @@ -3131,6 +3133,16 @@ "@types/node": "*" } }, + "node_modules/@types/mqtt": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@types/mqtt/-/mqtt-2.5.0.tgz", + "integrity": "sha512-n+0/ErBin30j+UbhcHGK/STjHjh65k85WNR6NlUjRG0g9yctpF12pS+SOkwz0wmp+7momAo9Cyi4Wmvy8UsCQg==", + "deprecated": "This is a stub types definition for MQTT (https://github.com/mqttjs/MQTT.js). MQTT provides its own type definitions, so you don't need @types/mqtt installed!", + "dev": true, + "dependencies": { + "mqtt": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -3142,7 +3154,6 @@ "version": "20.17.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz", "integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -3185,6 +3196,20 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/readable-stream": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz", + "integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -3203,6 +3228,14 @@ "license": "MIT", "optional": true }, + "node_modules/@types/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -3444,6 +3477,17 @@ "dev": true, "license": "ISC" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -4219,7 +4263,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4348,7 +4391,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/builder-util": { @@ -4980,6 +5022,11 @@ "node": ">= 6" } }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, "node_modules/compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", @@ -5014,6 +5061,20 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/concurrently": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", @@ -6765,6 +6826,14 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter2": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", @@ -6776,6 +6845,14 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -6895,6 +6972,18 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, "node_modules/fastq": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", @@ -7609,6 +7698,11 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -7724,7 +7818,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -7810,7 +7903,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -8447,6 +8539,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9076,7 +9177,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9197,6 +9297,139 @@ "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" }, + "node_modules/mqtt": { + "version": "5.10.4", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.4.tgz", + "integrity": "sha512-wN+SuhT2/ZaG6NPxca0N6YtRivnMxk6VflxQUEeqDH4erKdj+wPAGhHmcTLzvqfE4sJRxrEJ+XJxUc0No0E7eQ==", + "dependencies": { + "@types/readable-stream": "^4.0.18", + "@types/ws": "^8.5.14", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.0", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.1", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "reinterval": "^1.1.0", + "rfdc": "^1.4.1", + "split2": "^4.2.0", + "worker-timers": "^7.1.8", + "ws": "^8.18.0" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/bl": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz", + "integrity": "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/mqtt-packet/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/mqtt-packet/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/mqtt/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/mqtt/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9483,6 +9716,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10160,13 +10402,19 @@ "node": ">= 0.8" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", @@ -10558,7 +10806,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -10710,6 +10957,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10927,7 +11179,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -11416,6 +11667,14 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -11465,7 +11724,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -12314,6 +12572,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -12351,7 +12614,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unique-filename": { @@ -12652,6 +12914,37 @@ "node": ">=0.10.0" } }, + "node_modules/worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -12729,6 +13022,26 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0e4d124..a760289 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "clsx": "^2.1.1", "electron-serve": "^2.1.1", "lucide-react": "^0.475.0", + "mqtt": "^5.10.4", "next": "^15.1.7", "next-themes": "^0.4.4", "radix-ui": "^1.1.3", @@ -37,6 +38,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@types/mqtt": "^2.5.0", "@types/node": "20.17.17", "@types/react": "^19", "@types/react-dom": "^19",