mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Added HMS health status; Added MQTT debug log window
This commit is contained in:
@@ -18,6 +18,7 @@ from backend.app.schemas.printer import (
|
||||
PrinterUpdate,
|
||||
PrinterResponse,
|
||||
PrinterStatus,
|
||||
HMSErrorResponse,
|
||||
)
|
||||
from backend.app.services.printer_manager import printer_manager
|
||||
from backend.app.services.bambu_ftp import (
|
||||
@@ -138,6 +139,12 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
if state.state == "RUNNING" and state.gcode_file:
|
||||
cover_url = f"/api/v1/printers/{printer_id}/cover"
|
||||
|
||||
# Convert HMS errors to response format
|
||||
hms_errors = [
|
||||
HMSErrorResponse(code=e.code, module=e.module, severity=e.severity)
|
||||
for e in (state.hms_errors or [])
|
||||
]
|
||||
|
||||
return PrinterStatus(
|
||||
id=printer_id,
|
||||
name=printer.name,
|
||||
@@ -152,6 +159,7 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
total_layers=state.total_layers,
|
||||
temperatures=state.temperatures,
|
||||
cover_url=cover_url,
|
||||
hms_errors=hms_errors,
|
||||
)
|
||||
|
||||
|
||||
@@ -410,3 +418,72 @@ async def get_printer_storage(
|
||||
storage_info = await get_storage_info_async(printer.ip_address, printer.access_code)
|
||||
|
||||
return storage_info or {"used_bytes": None, "free_bytes": None}
|
||||
|
||||
|
||||
# ============================================
|
||||
# MQTT Debug Logging Endpoints
|
||||
# ============================================
|
||||
|
||||
@router.post("/{printer_id}/logging/enable")
|
||||
async def enable_mqtt_logging(printer_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Enable MQTT message logging for a printer."""
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
printer = result.scalar_one_or_none()
|
||||
if not printer:
|
||||
raise HTTPException(404, "Printer not found")
|
||||
|
||||
success = printer_manager.enable_logging(printer_id, True)
|
||||
if not success:
|
||||
raise HTTPException(400, "Printer not connected")
|
||||
|
||||
return {"logging_enabled": True}
|
||||
|
||||
|
||||
@router.post("/{printer_id}/logging/disable")
|
||||
async def disable_mqtt_logging(printer_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Disable MQTT message logging for a printer."""
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
printer = result.scalar_one_or_none()
|
||||
if not printer:
|
||||
raise HTTPException(404, "Printer not found")
|
||||
|
||||
success = printer_manager.enable_logging(printer_id, False)
|
||||
if not success:
|
||||
raise HTTPException(400, "Printer not connected")
|
||||
|
||||
return {"logging_enabled": False}
|
||||
|
||||
|
||||
@router.get("/{printer_id}/logging")
|
||||
async def get_mqtt_logs(printer_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Get MQTT message logs for a printer."""
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
printer = result.scalar_one_or_none()
|
||||
if not printer:
|
||||
raise HTTPException(404, "Printer not found")
|
||||
|
||||
logs = printer_manager.get_logs(printer_id)
|
||||
return {
|
||||
"logging_enabled": printer_manager.is_logging_enabled(printer_id),
|
||||
"logs": [
|
||||
{
|
||||
"timestamp": log.timestamp,
|
||||
"topic": log.topic,
|
||||
"direction": log.direction,
|
||||
"payload": log.payload,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{printer_id}/logging")
|
||||
async def clear_mqtt_logs(printer_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Clear MQTT message logs for a printer."""
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
printer = result.scalar_one_or_none()
|
||||
if not printer:
|
||||
raise HTTPException(404, "Printer not found")
|
||||
|
||||
printer_manager.clear_logs(printer_id)
|
||||
return {"status": "cleared"}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -34,6 +34,12 @@ class PrinterResponse(PrinterBase):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class HMSErrorResponse(BaseModel):
|
||||
code: str
|
||||
module: int
|
||||
severity: int # 1=fatal, 2=serious, 3=common, 4=info
|
||||
|
||||
|
||||
class PrinterStatus(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
@@ -48,3 +54,4 @@ class PrinterStatus(BaseModel):
|
||||
total_layers: int | None = None
|
||||
temperatures: dict | None = None
|
||||
cover_url: str | None = None
|
||||
hms_errors: list[HMSErrorResponse] = []
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
import json
|
||||
import ssl
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import Callable
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
|
||||
@dataclass
|
||||
class MQTTLogEntry:
|
||||
"""Log entry for MQTT message debugging."""
|
||||
timestamp: str
|
||||
topic: str
|
||||
direction: str # "in" or "out"
|
||||
payload: dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class HMSError:
|
||||
"""Health Management System error from printer."""
|
||||
code: str
|
||||
module: int
|
||||
severity: int # 1=fatal, 2=serious, 3=common, 4=info
|
||||
message: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrinterState:
|
||||
connected: bool = False
|
||||
@@ -21,6 +41,7 @@ class PrinterState:
|
||||
raw_data: dict = field(default_factory=dict)
|
||||
gcode_file: str | None = None
|
||||
subtask_id: str | None = None
|
||||
hms_errors: list = field(default_factory=list) # List of HMSError
|
||||
|
||||
|
||||
class BambuMQTTClient:
|
||||
@@ -49,6 +70,8 @@ class BambuMQTTClient:
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
self._previous_gcode_state: str | None = None
|
||||
self._previous_gcode_file: str | None = None
|
||||
self._message_log: deque[MQTTLogEntry] = deque(maxlen=100)
|
||||
self._logging_enabled: bool = False
|
||||
|
||||
@property
|
||||
def topic_subscribe(self) -> str:
|
||||
@@ -75,6 +98,14 @@ class BambuMQTTClient:
|
||||
def _on_message(self, client, userdata, msg):
|
||||
try:
|
||||
payload = json.loads(msg.payload.decode())
|
||||
# Log message if logging is enabled
|
||||
if self._logging_enabled:
|
||||
self._message_log.append(MQTTLogEntry(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
topic=msg.topic,
|
||||
direction="in",
|
||||
payload=payload,
|
||||
))
|
||||
self._process_message(payload)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
@@ -131,6 +162,32 @@ class BambuMQTTClient:
|
||||
if temps:
|
||||
self.state.temperatures = temps
|
||||
|
||||
# Parse HMS (Health Management System) errors
|
||||
if "hms" in data:
|
||||
hms_list = data["hms"]
|
||||
self.state.hms_errors = []
|
||||
if isinstance(hms_list, list):
|
||||
for hms in hms_list:
|
||||
if isinstance(hms, dict):
|
||||
# HMS format: {"attr": code, "code": full_code}
|
||||
# The code is a hex string, severity is in bits
|
||||
code = hms.get("code", hms.get("attr", "0"))
|
||||
if isinstance(code, int):
|
||||
code = hex(code)
|
||||
# Parse severity from code (typically last 4 bits indicate level)
|
||||
try:
|
||||
code_int = int(str(code).replace("0x", ""), 16) if code else 0
|
||||
severity = (code_int >> 16) & 0xF # Extract severity bits
|
||||
module = (code_int >> 24) & 0xFF # Extract module bits
|
||||
except (ValueError, TypeError):
|
||||
severity = 3
|
||||
module = 0
|
||||
self.state.hms_errors.append(HMSError(
|
||||
code=str(code),
|
||||
module=module,
|
||||
severity=severity if severity > 0 else 3,
|
||||
))
|
||||
|
||||
self.state.raw_data = data
|
||||
|
||||
# Detect print start (state changes TO RUNNING with a file)
|
||||
@@ -236,4 +293,31 @@ class BambuMQTTClient:
|
||||
def send_command(self, command: dict):
|
||||
"""Send a command to the printer."""
|
||||
if self._client and self.state.connected:
|
||||
# Log outgoing message if logging is enabled
|
||||
if self._logging_enabled:
|
||||
self._message_log.append(MQTTLogEntry(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
topic=self.topic_publish,
|
||||
direction="out",
|
||||
payload=command,
|
||||
))
|
||||
self._client.publish(self.topic_publish, json.dumps(command))
|
||||
|
||||
def enable_logging(self, enabled: bool = True):
|
||||
"""Enable or disable MQTT message logging."""
|
||||
self._logging_enabled = enabled
|
||||
if not enabled:
|
||||
self._message_log.clear()
|
||||
|
||||
def get_logs(self) -> list[MQTTLogEntry]:
|
||||
"""Get all logged MQTT messages."""
|
||||
return list(self._message_log)
|
||||
|
||||
def clear_logs(self):
|
||||
"""Clear the message log."""
|
||||
self._message_log.clear()
|
||||
|
||||
@property
|
||||
def logging_enabled(self) -> bool:
|
||||
"""Check if logging is enabled."""
|
||||
return self._logging_enabled
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from backend.app.models.printer import Printer
|
||||
from backend.app.services.bambu_mqtt import BambuMQTTClient, PrinterState
|
||||
from backend.app.services.bambu_mqtt import BambuMQTTClient, PrinterState, MQTTLogEntry
|
||||
from backend.app.services.bambu_ftp import BambuFTPClient
|
||||
|
||||
|
||||
@@ -118,6 +118,32 @@ class PrinterManager:
|
||||
return self._clients[printer_id].start_print(filename)
|
||||
return False
|
||||
|
||||
def enable_logging(self, printer_id: int, enabled: bool = True) -> bool:
|
||||
"""Enable or disable MQTT logging for a printer."""
|
||||
if printer_id in self._clients:
|
||||
self._clients[printer_id].enable_logging(enabled)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_logs(self, printer_id: int) -> list[MQTTLogEntry]:
|
||||
"""Get MQTT logs for a printer."""
|
||||
if printer_id in self._clients:
|
||||
return self._clients[printer_id].get_logs()
|
||||
return []
|
||||
|
||||
def clear_logs(self, printer_id: int) -> bool:
|
||||
"""Clear MQTT logs for a printer."""
|
||||
if printer_id in self._clients:
|
||||
self._clients[printer_id].clear_logs()
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_logging_enabled(self, printer_id: int) -> bool:
|
||||
"""Check if logging is enabled for a printer."""
|
||||
if printer_id in self._clients:
|
||||
return self._clients[printer_id].logging_enabled
|
||||
return False
|
||||
|
||||
async def test_connection(
|
||||
self,
|
||||
ip_address: str,
|
||||
@@ -159,6 +185,10 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) ->
|
||||
"layer_num": state.layer_num,
|
||||
"total_layers": state.total_layers,
|
||||
"temperatures": state.temperatures,
|
||||
"hms_errors": [
|
||||
{"code": e.code, "module": e.module, "severity": e.severity}
|
||||
for e in (state.hms_errors or [])
|
||||
],
|
||||
}
|
||||
# Add cover URL if there's an active print and printer_id is provided
|
||||
if printer_id and state.state == "RUNNING" and state.gcode_file:
|
||||
|
||||
@@ -34,6 +34,12 @@ export interface Printer {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface HMSError {
|
||||
code: string;
|
||||
module: number;
|
||||
severity: number; // 1=fatal, 2=serious, 3=common, 4=info
|
||||
}
|
||||
|
||||
export interface PrinterStatus {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -54,6 +60,7 @@ export interface PrinterStatus {
|
||||
chamber?: number;
|
||||
} | null;
|
||||
cover_url: string | null;
|
||||
hms_errors: HMSError[];
|
||||
}
|
||||
|
||||
export interface PrinterCreate {
|
||||
@@ -235,6 +242,19 @@ export interface SmartPlugTestResult {
|
||||
device_name: string | null;
|
||||
}
|
||||
|
||||
// MQTT Logging types
|
||||
export interface MQTTLogEntry {
|
||||
timestamp: string;
|
||||
topic: string;
|
||||
direction: 'in' | 'out';
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MQTTLogsResponse {
|
||||
logging_enabled: boolean;
|
||||
logs: MQTTLogEntry[];
|
||||
}
|
||||
|
||||
// API functions
|
||||
export const api = {
|
||||
// Printers
|
||||
@@ -263,6 +283,22 @@ export const api = {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
// MQTT Debug Logging
|
||||
enableMQTTLogging: (printerId: number) =>
|
||||
request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
disableMQTTLogging: (printerId: number) =>
|
||||
request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
getMQTTLogs: (printerId: number) =>
|
||||
request<MQTTLogsResponse>(`/printers/${printerId}/logging`),
|
||||
clearMQTTLogs: (printerId: number) =>
|
||||
request<{ status: string }>(`/printers/${printerId}/logging`, {
|
||||
method: 'DELETE',
|
||||
}),
|
||||
|
||||
// Printer File Manager
|
||||
getPrinterFiles: (printerId: number, path = '/') =>
|
||||
request<{
|
||||
@@ -507,4 +543,5 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ip_address, username, password }),
|
||||
}),
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { X, Play, Square, Trash2, RefreshCw, ArrowDown, ArrowUp } from 'lucide-react';
|
||||
import { api, type MQTTLogEntry } from '../api/client';
|
||||
import { Button } from './Button';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface MQTTDebugModalProps {
|
||||
printerId: number;
|
||||
printerName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function MQTTDebugModal({ printerId, printerName, onClose }: MQTTDebugModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const [expandedLogs, setExpandedLogs] = useState<Set<number>>(new Set());
|
||||
const logContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['mqtt-logs', printerId],
|
||||
queryFn: () => api.getMQTTLogs(printerId),
|
||||
refetchInterval: 1000, // Poll every second when logging is enabled
|
||||
});
|
||||
|
||||
const enableMutation = useMutation({
|
||||
mutationFn: () => api.enableMQTTLogging(printerId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['mqtt-logs', printerId] });
|
||||
},
|
||||
});
|
||||
|
||||
const disableMutation = useMutation({
|
||||
mutationFn: () => api.disableMQTTLogging(printerId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['mqtt-logs', printerId] });
|
||||
},
|
||||
});
|
||||
|
||||
const clearMutation = useMutation({
|
||||
mutationFn: () => api.clearMQTTLogs(printerId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['mqtt-logs', printerId] });
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-scroll to bottom when new logs arrive
|
||||
useEffect(() => {
|
||||
if (autoScroll && logContainerRef.current) {
|
||||
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [data?.logs, autoScroll]);
|
||||
|
||||
const toggleExpand = (index: number) => {
|
||||
setExpandedLogs((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(index)) {
|
||||
newSet.delete(index);
|
||||
} else {
|
||||
newSet.add(index);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('en-US', { hour12: false, fractionalSecondDigits: 3 });
|
||||
};
|
||||
|
||||
const formatPayload = (payload: Record<string, unknown>, expanded: boolean) => {
|
||||
const json = JSON.stringify(payload, null, expanded ? 2 : 0);
|
||||
if (!expanded && json.length > 100) {
|
||||
return json.substring(0, 100) + '...';
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
const loggingEnabled = data?.logging_enabled ?? false;
|
||||
const logs = data?.logs ?? [];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-bambu-dark-secondary rounded-lg max-w-4xl w-full max-h-[85vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">MQTT Debug Log</h2>
|
||||
<p className="text-sm text-bambu-gray">{printerName}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-bambu-gray hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-2 p-4 border-b border-bambu-dark-tertiary">
|
||||
{loggingEnabled ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => disableMutation.mutate()}
|
||||
disabled={disableMutation.isPending}
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
Stop
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => enableMutation.mutate()}
|
||||
disabled={enableMutation.isPending}
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
Start Logging
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => clearMutation.mutate()}
|
||||
disabled={clearMutation.isPending || logs.length === 0}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<label className="flex items-center gap-2 text-sm text-bambu-gray cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoScroll}
|
||||
onChange={(e) => setAutoScroll(e.target.checked)}
|
||||
className="rounded border-bambu-dark-tertiary"
|
||||
/>
|
||||
Auto-scroll
|
||||
</label>
|
||||
<span className="text-sm text-bambu-gray">
|
||||
{logs.length} message{logs.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Log Content */}
|
||||
<div
|
||||
ref={logContainerRef}
|
||||
className="flex-1 overflow-auto p-4 font-mono text-xs bg-bambu-dark min-h-[400px]"
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-bambu-gray">
|
||||
<p className="mb-2">No messages logged yet</p>
|
||||
{!loggingEnabled && (
|
||||
<p className="text-sm">Click "Start Logging" to begin capturing MQTT messages</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{logs.map((log: MQTTLogEntry, index: number) => {
|
||||
const isExpanded = expandedLogs.has(index);
|
||||
const isIncoming = log.direction === 'in';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-2 rounded cursor-pointer hover:bg-bambu-dark-secondary transition-colors ${
|
||||
isExpanded ? 'bg-bambu-dark-secondary' : ''
|
||||
}`}
|
||||
onClick={() => toggleExpand(index)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-bambu-gray shrink-0">
|
||||
{formatTimestamp(log.timestamp)}
|
||||
</span>
|
||||
<span
|
||||
className={`shrink-0 ${
|
||||
isIncoming ? 'text-blue-400' : 'text-green-400'
|
||||
}`}
|
||||
title={isIncoming ? 'Incoming' : 'Outgoing'}
|
||||
>
|
||||
{isIncoming ? (
|
||||
<ArrowDown className="w-3 h-3" />
|
||||
) : (
|
||||
<ArrowUp className="w-3 h-3" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-purple-400 shrink-0">{log.topic}</span>
|
||||
</div>
|
||||
<pre
|
||||
className={`mt-1 text-white/80 overflow-x-auto ${
|
||||
isExpanded ? 'whitespace-pre-wrap' : 'truncate'
|
||||
}`}
|
||||
>
|
||||
{formatPayload(log.payload, isExpanded)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-bambu-dark-tertiary">
|
||||
<div className="text-sm text-bambu-gray">
|
||||
{loggingEnabled ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
Logging active - messages will auto-refresh
|
||||
</span>
|
||||
) : (
|
||||
<span>Logging stopped</span>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
RefreshCw,
|
||||
Box,
|
||||
HardDrive,
|
||||
AlertTriangle,
|
||||
Terminal,
|
||||
} from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import type { Printer, PrinterCreate } from '../api/client';
|
||||
@@ -18,6 +20,7 @@ import { Card, CardContent } from '../components/Card';
|
||||
import { Button } from '../components/Button';
|
||||
import { ConfirmModal } from '../components/ConfirmModal';
|
||||
import { FileManagerModal } from '../components/FileManagerModal';
|
||||
import { MQTTDebugModal } from '../components/MQTTDebugModal';
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
@@ -79,6 +82,7 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showFileManager, setShowFileManager] = useState(false);
|
||||
const [showMQTTDebug, setShowMQTTDebug] = useState(false);
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ['printerStatus', printer.id],
|
||||
@@ -131,6 +135,28 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
|
||||
)}
|
||||
{status?.connected ? 'Connected' : 'Offline'}
|
||||
</span>
|
||||
{/* HMS Status Indicator */}
|
||||
{status?.connected && (
|
||||
<span
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs ${
|
||||
status.hms_errors && status.hms_errors.length > 0
|
||||
? status.hms_errors.some(e => e.severity <= 2)
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: 'bg-orange-500/20 text-orange-400'
|
||||
: 'bg-bambu-green/20 text-bambu-green'
|
||||
}`}
|
||||
title={
|
||||
status.hms_errors && status.hms_errors.length > 0
|
||||
? `${status.hms_errors.length} HMS error(s)`
|
||||
: 'No HMS errors'
|
||||
}
|
||||
>
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
{status.hms_errors && status.hms_errors.length > 0
|
||||
? status.hms_errors.length
|
||||
: 'OK'}
|
||||
</span>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -151,6 +177,16 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Reconnect
|
||||
</button>
|
||||
<button
|
||||
className="w-full px-4 py-2 text-left text-sm hover:bg-bambu-dark-tertiary flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setShowMQTTDebug(true);
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<Terminal className="w-4 h-4" />
|
||||
MQTT Debug
|
||||
</button>
|
||||
<button
|
||||
className="w-full px-4 py-2 text-left text-sm text-red-400 hover:bg-bambu-dark-tertiary flex items-center gap-2"
|
||||
onClick={() => {
|
||||
@@ -289,6 +325,15 @@ function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIf
|
||||
onClose={() => setShowFileManager(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* MQTT Debug Modal */}
|
||||
{showMQTTDebug && (
|
||||
<MQTTDebugModal
|
||||
printerId={printer.id}
|
||||
printerName={printer.name}
|
||||
onClose={() => setShowMQTTDebug(false)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DZsA8zAw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DwtaaDcN.css">
|
||||
<script type="module" crossorigin src="/assets/index-BPRATuOd.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DUX4pLTn.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user