Merge pull request #9 from maziggy/0.1.5

v0.1.4
This commit is contained in:
MartinNYHC
2025-12-01 15:46:32 +01:00
committed by GitHub
13 changed files with 953 additions and 353 deletions
+73 -12
View File
@@ -1,5 +1,6 @@
"""API routes for K-profile (pressure advance) management."""
import asyncio
import logging
from fastapi import APIRouter, Depends, HTTPException
@@ -77,10 +78,22 @@ async def set_kprofile(
):
"""Create or update a K-profile on the printer.
For H2D edits (slot_id > 0), this performs an in-place edit using cali_idx.
For other printers or new profiles, this adds a new profile.
Args:
printer_id: ID of the printer
profile: K-profile data to set
"""
is_edit = profile.slot_id > 0
operation = "edit" if is_edit else "add"
logger.info(
f"[API] set_kprofile ({operation}): printer={printer_id}, slot_id={profile.slot_id}, "
f"extruder_id={profile.extruder_id}, nozzle_id={profile.nozzle_id}, "
f"name={profile.name}, filament_id={profile.filament_id}, k_value={profile.k_value}"
)
# Check printer exists
result = await db.execute(select(Printer).where(Printer.id == printer_id))
printer = result.scalar_one_or_none()
@@ -92,22 +105,69 @@ async def set_kprofile(
if not client or not client.state.connected:
raise HTTPException(400, "Printer not connected")
# Send the K-profile to printer
success = client.set_kprofile(
filament_id=profile.filament_id,
name=profile.name,
k_value=profile.k_value,
nozzle_diameter=profile.nozzle_diameter,
nozzle_id=profile.nozzle_id,
extruder_id=profile.extruder_id,
setting_id=profile.setting_id,
slot_id=profile.slot_id,
)
# Detect H2D by serial number prefix
is_h2d = printer.serial_number.startswith("094")
if is_edit and is_h2d:
# H2D in-place edit: use cali_idx with slot_id=0 and empty setting_id
logger.info(f"[API] H2D in-place edit: cali_idx={profile.slot_id}")
success = client.set_kprofile(
filament_id=profile.filament_id,
name=profile.name,
k_value=profile.k_value,
nozzle_diameter=profile.nozzle_diameter,
nozzle_id=profile.nozzle_id,
extruder_id=profile.extruder_id,
setting_id=None,
slot_id=0,
cali_idx=profile.slot_id, # Pass the original slot for in-place edit
)
elif is_edit:
# Non-H2D edit: use delete + add approach
logger.info(f"[API] Edit: deleting existing profile slot_id={profile.slot_id}")
delete_success = client.delete_kprofile(
cali_idx=profile.slot_id,
filament_id=profile.filament_id,
nozzle_id=profile.nozzle_id,
nozzle_diameter=profile.nozzle_diameter,
extruder_id=profile.extruder_id,
setting_id=profile.setting_id,
)
if not delete_success:
raise HTTPException(500, "Failed to delete existing K-profile for edit")
# Wait for printer to process the delete before adding
await asyncio.sleep(0.5)
logger.info("[API] Edit: delete complete, now adding updated profile")
success = client.set_kprofile(
filament_id=profile.filament_id,
name=profile.name,
k_value=profile.k_value,
nozzle_diameter=profile.nozzle_diameter,
nozzle_id=profile.nozzle_id,
extruder_id=profile.extruder_id,
setting_id=None, # Generate new setting_id for add
slot_id=0, # Always 0 for add (new profile)
)
else:
# New profile: add with slot_id=0
success = client.set_kprofile(
filament_id=profile.filament_id,
name=profile.name,
k_value=profile.k_value,
nozzle_diameter=profile.nozzle_diameter,
nozzle_id=profile.nozzle_id,
extruder_id=profile.extruder_id,
setting_id=None, # Generate new setting_id for add
slot_id=0, # Always 0 for add (new profile)
)
if not success:
raise HTTPException(500, "Failed to send K-profile command")
return {"success": True, "message": "K-profile set successfully"}
message = "K-profile updated successfully" if is_edit else "K-profile added successfully"
return {"success": True, "message": message}
@router.delete("/", response_model=dict)
@@ -140,6 +200,7 @@ async def delete_kprofile(
nozzle_id=profile.nozzle_id,
nozzle_diameter=profile.nozzle_diameter,
extruder_id=profile.extruder_id,
setting_id=profile.setting_id,
)
if not success:
+16
View File
@@ -1,4 +1,5 @@
import shutil
from pathlib import Path
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
@@ -95,6 +96,21 @@ async def reset_settings(db: AsyncSession = Depends(get_db)):
async def check_ffmpeg():
"""Check if ffmpeg is installed and available."""
ffmpeg_path = shutil.which("ffmpeg")
# If not found via PATH, check common installation locations
# (systemd services often have limited PATH)
if ffmpeg_path is None:
common_paths = [
"/usr/bin/ffmpeg",
"/usr/local/bin/ffmpeg",
"/opt/homebrew/bin/ffmpeg",
"/snap/bin/ffmpeg",
]
for path in common_paths:
if Path(path).exists():
ffmpeg_path = path
break
return {
"installed": ffmpeg_path is not None,
"path": ffmpeg_path,
+1
View File
@@ -50,3 +50,4 @@ class KProfileDelete(BaseModel):
nozzle_id: str # e.g., "HH00-0.4"
nozzle_diameter: str # e.g., "0.4"
filament_id: str # Bambu filament identifier
setting_id: str | None = None # Setting ID (for X1C series)
+179 -70
View File
@@ -119,6 +119,8 @@ class BambuMQTTClient:
client.subscribe(self.topic_subscribe)
# Request full status update
self._request_push_all()
# Prime K-profile request (Bambu printers often ignore first request)
self._prime_kprofile_request()
# Immediately broadcast connection state change
if self.on_state_change:
self.on_state_change(self.state)
@@ -412,6 +414,25 @@ class BambuMQTTClient:
message = {"pushing": {"command": "pushall"}}
self._client.publish(self.topic_publish, json.dumps(message))
def _prime_kprofile_request(self):
"""Send a priming K-profile request on connect.
Bambu printers often ignore the first K-profile request after connection,
so we send a dummy request on connect to 'prime' the system.
"""
if self._client:
self._sequence_id += 1
command = {
"print": {
"command": "extrusion_cali_get",
"filament_id": "",
"nozzle_diameter": "0.4",
"sequence_id": str(self._sequence_id),
}
}
logger.debug(f"[{self.serial_number}] Sending K-profile priming request")
self._client.publish(self.topic_publish, json.dumps(command))
def connect(self, loop: asyncio.AbstractEventLoop | None = None):
"""Connect to the printer MQTT broker.
@@ -568,12 +589,16 @@ class BambuMQTTClient:
logger.info(f"[{self.serial_number}] Received {len(profiles)} K-profiles")
async def get_kprofiles(self, nozzle_diameter: str = "0.4", timeout: float = 10.0) -> list[KProfile]:
"""Request K-profiles from the printer.
async def get_kprofiles(self, nozzle_diameter: str = "0.4", timeout: float = 5.0, max_retries: int = 3) -> list[KProfile]:
"""Request K-profiles from the printer with retry logic.
Bambu printers sometimes ignore the first K-profile request, so we
implement retry logic to ensure reliable retrieval.
Args:
nozzle_diameter: Filter by nozzle diameter (e.g., "0.4")
timeout: Timeout in seconds to wait for response
timeout: Timeout in seconds to wait for each response attempt
max_retries: Maximum number of retry attempts
Returns:
List of KProfile objects
@@ -589,33 +614,41 @@ class BambuMQTTClient:
logger.warning(f"[{self.serial_number}] No running event loop")
return []
# Set up response event
self._sequence_id += 1
self._pending_kprofile_response = asyncio.Event()
self._kprofile_response_data = None
for attempt in range(max_retries):
# Set up response event for this attempt
self._sequence_id += 1
self._pending_kprofile_response = asyncio.Event()
self._kprofile_response_data = None
# Send the command
command = {
"print": {
"command": "extrusion_cali_get",
"filament_id": "",
"nozzle_diameter": nozzle_diameter,
"sequence_id": str(self._sequence_id),
# Send the command
command = {
"print": {
"command": "extrusion_cali_get",
"filament_id": "",
"nozzle_diameter": nozzle_diameter,
"sequence_id": str(self._sequence_id),
}
}
}
logger.info(f"[{self.serial_number}] Requesting K-profiles for nozzle {nozzle_diameter}")
self._client.publish(self.topic_publish, json.dumps(command))
logger.info(f"[{self.serial_number}] Requesting K-profiles for nozzle {nozzle_diameter} (attempt {attempt + 1}/{max_retries})")
self._client.publish(self.topic_publish, json.dumps(command))
# Wait for response
try:
await asyncio.wait_for(self._pending_kprofile_response.wait(), timeout=timeout)
return self._kprofile_response_data or []
except asyncio.TimeoutError:
logger.warning(f"[{self.serial_number}] Timeout waiting for K-profiles response")
return []
finally:
self._pending_kprofile_response = None
# Wait for response
try:
await asyncio.wait_for(self._pending_kprofile_response.wait(), timeout=timeout)
profiles = self._kprofile_response_data or []
logger.info(f"[{self.serial_number}] Got {len(profiles)} K-profiles on attempt {attempt + 1}")
return profiles
except asyncio.TimeoutError:
logger.warning(f"[{self.serial_number}] Timeout on K-profiles request attempt {attempt + 1}/{max_retries}")
if attempt < max_retries - 1:
# Brief delay before retry
await asyncio.sleep(0.5)
finally:
self._pending_kprofile_response = None
logger.error(f"[{self.serial_number}] Failed to get K-profiles after {max_retries} attempts")
return []
def set_kprofile(
self,
@@ -627,6 +660,7 @@ class BambuMQTTClient:
extruder_id: int = 0,
setting_id: str | None = None,
slot_id: int = 0,
cali_idx: int | None = None,
) -> bool:
"""Set/update a K-profile on the printer.
@@ -639,6 +673,7 @@ class BambuMQTTClient:
extruder_id: Extruder ID (0 or 1 for dual nozzle)
setting_id: Existing setting ID for updates, None for new
slot_id: Calibration index (cali_idx) for the profile
cali_idx: For H2D edits, the existing slot being edited (enables in-place edit)
Returns:
True if command was sent, False otherwise
@@ -649,43 +684,96 @@ class BambuMQTTClient:
self._sequence_id += 1
# Build the filament entry - printer uses cali_idx for profile identification
# For new profiles (slot_id=0), use cali_idx=-1 to tell printer to create new slot
cali_idx = -1 if slot_id == 0 else slot_id
# Detect printer type by serial number prefix
# X1C/P1/A1 series (single nozzle): serial starts with "00M", "00W", "01P", "01S", "03W", etc.
# H2D series (dual nozzle): serial starts with "094"
is_dual_nozzle = self.serial_number.startswith("094")
# Generate a setting_id for new profiles (required by printer)
# Format: "PF" + 17 random digits
import random
if not setting_id and slot_id == 0:
setting_id = f"PF{random.randint(10000000000000000, 99999999999999999)}"
# For H2D edits, use empty setting_id per OrcaSlicer sniff
# For new profiles, generate a setting_id
import secrets
if cali_idx is not None:
# Edit mode - use empty setting_id per OrcaSlicer sniff
setting_id = ""
elif not setting_id and slot_id == 0:
# New profile - generate setting_id
setting_id = f"PFUS{secrets.token_hex(7)}" # 7 bytes = 14 hex chars
filament_entry = {
"ams_id": 0,
"cali_idx": cali_idx,
"extruder_id": extruder_id,
"filament_id": filament_id,
"k_value": k_value,
"n_coef": "0.000000",
"name": name,
"nozzle_diameter": nozzle_diameter,
"nozzle_id": nozzle_id,
"setting_id": setting_id, # Always include setting_id
"tray_id": -1,
}
command = {
"print": {
"command": "extrusion_cali_set",
"filaments": [filament_entry],
if is_dual_nozzle:
# H2D format - exact OrcaSlicer format (captured via MQTT sniffing)
# For edits: include cali_idx (existing slot), slot_id=0, setting_id=""
# For new profiles: no cali_idx, slot_id=0, setting_id=generated
filament_entry = {
"ams_id": 0,
"extruder_id": extruder_id,
"filament_id": filament_id,
"k_value": k_value,
"n_coef": "0.000000",
"name": name,
"nozzle_diameter": nozzle_diameter,
"sequence_id": str(self._sequence_id),
"nozzle_id": nozzle_id,
"setting_id": setting_id if setting_id else "",
"slot_id": slot_id,
"tray_id": -1,
}
# For edits, add cali_idx field (position matters - alphabetical order)
if cali_idx is not None:
# Insert cali_idx in alphabetical position (after ams_id, before extruder_id)
# n_coef must be "0.000000" for H2D edits (matches OrcaSlicer sniff)
filament_entry = {
"ams_id": 0,
"cali_idx": cali_idx,
"extruder_id": extruder_id,
"filament_id": filament_id,
"k_value": k_value,
"n_coef": "0.000000",
"name": name,
"nozzle_diameter": nozzle_diameter,
"nozzle_id": nozzle_id,
"setting_id": "",
"slot_id": 0,
"tray_id": -1,
}
command = {
"print": {
"command": "extrusion_cali_set",
"filaments": [filament_entry],
"nozzle_diameter": nozzle_diameter,
"sequence_id": str(self._sequence_id),
}
}
else:
# X1C/P1/A1 format - based on actual X1C profile data:
# - n_coef: "1.000000" (NOT 0.000000 like H2D)
# - nozzle_id: "" (empty string, NOT the nozzle type)
# - tray_id: -1 (NOT 0)
filament_entry = {
"ams_id": 0,
"extruder_id": 0, # X1C is single nozzle
"filament_id": filament_id,
"k_value": k_value,
"n_coef": "1.000000", # X1C uses 1.0, not 0.0
"name": name,
"nozzle_diameter": nozzle_diameter,
"nozzle_id": "", # X1C uses empty string
"setting_id": setting_id,
"slot_id": slot_id,
"tray_id": -1, # X1C uses -1
}
command = {
"print": {
"command": "extrusion_cali_set",
"filaments": [filament_entry],
"nozzle_diameter": nozzle_diameter,
"sequence_id": str(self._sequence_id),
}
}
}
command_json = json.dumps(command)
logger.info(f"[{self.serial_number}] Setting K-profile: {name} = {k_value} (cali_idx={cali_idx}, new={slot_id==0})")
logger.debug(f"[{self.serial_number}] K-profile command: {command_json}")
self._client.publish(self.topic_publish, command_json)
logger.info(f"[{self.serial_number}] Setting K-profile: {name} = {k_value} (cali_idx={cali_idx}, new={slot_id==0}, dual={is_dual_nozzle})")
logger.info(f"[{self.serial_number}] K-profile SET command: {command_json}")
# Use QoS 1 for reliable delivery (at least once)
self._client.publish(self.topic_publish, command_json, qos=1)
return True
def delete_kprofile(
@@ -695,6 +783,7 @@ class BambuMQTTClient:
nozzle_id: str,
nozzle_diameter: str = "0.4",
extruder_id: int = 0,
setting_id: str | None = None,
) -> bool:
"""Delete a K-profile from the printer.
@@ -704,6 +793,7 @@ class BambuMQTTClient:
nozzle_id: Nozzle identifier (e.g., "HH00-0.4")
nozzle_diameter: Nozzle diameter (e.g., "0.4")
extruder_id: Extruder ID (0 or 1 for dual nozzle)
setting_id: Unique setting identifier (for X1C series)
Returns:
True if command was sent, False otherwise
@@ -714,20 +804,39 @@ class BambuMQTTClient:
self._sequence_id += 1
command = {
"print": {
"command": "extrusion_cali_del",
"sequence_id": str(self._sequence_id),
"extruder_id": extruder_id,
"nozzle_id": nozzle_id,
"filament_id": filament_id,
"cali_idx": cali_idx,
"nozzle_diameter": nozzle_diameter,
# Detect printer type by serial number prefix
# H2D series (dual nozzle): serial starts with "094"
is_dual_nozzle = self.serial_number.startswith("094")
if is_dual_nozzle:
# H2D format: uses extruder_id, nozzle_id, nozzle_diameter
command = {
"print": {
"command": "extrusion_cali_del",
"sequence_id": str(self._sequence_id),
"extruder_id": extruder_id,
"nozzle_id": nozzle_id,
"filament_id": filament_id,
"cali_idx": cali_idx,
"nozzle_diameter": nozzle_diameter,
}
}
else:
# X1C/P1/A1 format: uses setting_id, nozzle_diameter, no extruder/nozzle_id fields
command = {
"print": {
"command": "extrusion_cali_del",
"sequence_id": str(self._sequence_id),
"filament_id": filament_id,
"cali_idx": cali_idx,
"setting_id": setting_id,
"nozzle_diameter": nozzle_diameter,
}
}
}
command_json = json.dumps(command)
logger.info(f"[{self.serial_number}] Deleting K-profile: cali_idx={cali_idx}, filament={filament_id}")
logger.debug(f"[{self.serial_number}] K-profile delete command: {command_json}")
self._client.publish(self.topic_publish, command_json)
logger.info(f"[{self.serial_number}] Deleting K-profile: cali_idx={cali_idx}, filament={filament_id}, dual={is_dual_nozzle}")
logger.info(f"[{self.serial_number}] K-profile DELETE command: {command_json}")
# Use QoS 1 for reliable delivery (at least once)
self._client.publish(self.topic_publish, command_json, qos=1)
return True
+1
View File
@@ -351,6 +351,7 @@ export interface KProfileDelete {
nozzle_id: string; // e.g., "HH00-0.4"
nozzle_diameter: string; // e.g., "0.4"
filament_id: string; // Bambu filament identifier
setting_id?: string | null; // Setting ID (for X1C series)
}
export interface KProfilesResponse {
+161 -38
View File
@@ -128,7 +128,13 @@ function KProfileModal({
const [modalDiameter, setModalDiameter] = useState(
profile?.nozzle_diameter || nozzleDiameter
);
const [extruderId, setExtruderId] = useState(profile?.extruder_id || 0);
// For new profiles on dual-nozzle: allow selecting multiple extruders
// For editing: use single extruder from the profile
const [selectedExtruders, setSelectedExtruders] = useState<number[]>(
profile ? [profile.extruder_id] : isDualNozzle ? [0, 1] : [0] // Default: both extruders for new dual-nozzle profiles
);
const [isSyncing, setIsSyncing] = useState(false);
const [savingProgress, setSavingProgress] = useState({ current: 0, total: 0 });
// Extract unique filaments from existing K-profiles on the printer
// These have valid filament_ids that the printer recognizes
@@ -156,12 +162,20 @@ function KProfileModal({
onSuccess: (result) => {
console.log('[KProfile] Save success:', result);
showToast('K-profile saved');
queryClient.invalidateQueries({ queryKey: ['kprofiles', printerId] });
onSave();
// Show syncing indicator while printer processes the command
setIsSyncing(true);
// Add delay before refreshing to give printer time to process the save
// Bambu printers can be slow to apply K-profile changes
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['kprofiles', printerId] });
setIsSyncing(false);
onSave();
}, 2500);
},
onError: (error: Error) => {
console.error('[KProfile] Save error:', error);
showToast(error.message, 'error');
setIsSyncing(false);
},
});
@@ -173,12 +187,20 @@ function KProfileModal({
onSuccess: (result) => {
console.log('[KProfile] Delete success:', result);
showToast('K-profile deleted');
queryClient.invalidateQueries({ queryKey: ['kprofiles', printerId] });
onClose();
// Show syncing indicator while printer processes the command
setIsSyncing(true);
// Add delay before refreshing to give printer time to process the delete
// Bambu printers can be slow to apply K-profile changes
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['kprofiles', printerId] });
setIsSyncing(false);
onClose();
}, 2500);
},
onError: (error: Error) => {
console.error('[KProfile] Delete error:', error);
showToast(error.message, 'error');
setIsSyncing(false);
},
});
@@ -192,35 +214,101 @@ function KProfileModal({
nozzle_id: profile.nozzle_id,
nozzle_diameter: profile.nozzle_diameter,
filament_id: profile.filament_id,
setting_id: profile.setting_id,
});
};
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate at least one extruder is selected for dual-nozzle
if (isDualNozzle && !profile && selectedExtruders.length === 0) {
showToast('Please select at least one extruder', 'error');
return;
}
// Format k_value to 6 decimal places for Bambu protocol
const formattedKValue = parseFloat(kValue).toFixed(6);
// Combine nozzle type and diameter into nozzle_id (e.g., "HH00-0.4")
const nozzleId = `${nozzleType}-${modalDiameter}`;
// Use the name from the form - it's auto-populated when filament is selected
// but can be edited by the user
const payload = {
name: name,
k_value: formattedKValue,
filament_id: filamentId,
nozzle_id: nozzleId,
nozzle_diameter: modalDiameter,
extruder_id: extruderId,
setting_id: profile?.setting_id,
slot_id: profile?.slot_id ?? 0,
};
console.log('[KProfile] Saving profile:', payload);
saveMutation.mutate(payload);
// For editing or single extruder: just save one profile
if (profile || selectedExtruders.length === 1) {
const payload = {
name: name,
k_value: formattedKValue,
filament_id: filamentId,
nozzle_id: nozzleId,
nozzle_diameter: modalDiameter,
extruder_id: profile ? profile.extruder_id : selectedExtruders[0],
setting_id: profile?.setting_id,
slot_id: profile?.slot_id ?? 0,
};
console.log('[KProfile] Saving profile:', payload);
saveMutation.mutate(payload);
return;
}
// For new profiles with multiple extruders: save sequentially
setIsSyncing(true);
setSavingProgress({ current: 0, total: selectedExtruders.length });
for (let i = 0; i < selectedExtruders.length; i++) {
const extruderId = selectedExtruders[i];
const payload = {
name: name,
k_value: formattedKValue,
filament_id: filamentId,
nozzle_id: nozzleId,
nozzle_diameter: modalDiameter,
extruder_id: extruderId,
setting_id: undefined,
slot_id: 0,
};
setSavingProgress({ current: i + 1, total: selectedExtruders.length });
console.log(`[KProfile] Saving profile ${i + 1}/${selectedExtruders.length} for extruder ${extruderId}:`, payload);
try {
await api.setKProfile(printerId, payload);
// Wait between saves to let printer process
if (i < selectedExtruders.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1500));
}
} catch (error) {
console.error(`[KProfile] Failed to save for extruder ${extruderId}:`, error);
showToast(`Failed to save for ${extruderId === 1 ? 'Left' : 'Right'} extruder`, 'error');
setIsSyncing(false);
setSavingProgress({ current: 0, total: 0 });
return;
}
}
showToast(`K-profile saved to ${selectedExtruders.length} extruders`);
// Wait for final sync before closing
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['kprofiles', printerId] });
setIsSyncing(false);
setSavingProgress({ current: 0, total: 0 });
onSave();
}, 2500);
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<Card className="w-full max-w-md">
<Card className="w-full max-w-md relative">
{/* Syncing overlay */}
{isSyncing && (
<div className="absolute inset-0 bg-bambu-dark-secondary/90 flex flex-col items-center justify-center z-10 rounded-lg">
<Loader2 className="w-8 h-8 text-bambu-green animate-spin mb-3" />
<p className="text-white font-medium">
{savingProgress.total > 1
? `Saving to extruder ${savingProgress.current}/${savingProgress.total}...`
: 'Syncing with printer...'}
</p>
<p className="text-bambu-gray text-sm mt-1">Please wait</p>
</div>
)}
<CardContent className="p-0">
<div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
<h2 className="text-xl font-semibold text-white">
@@ -229,6 +317,7 @@ function KProfileModal({
<button
onClick={onClose}
className="text-bambu-gray hover:text-white transition-colors"
disabled={isSyncing}
>
<X className="w-5 h-5" />
</button>
@@ -245,7 +334,7 @@ function KProfileModal({
disabled={!!profile}
className={`w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none ${profile ? 'opacity-60 cursor-not-allowed' : ''}`}
placeholder="My PLA Profile"
required
required={!profile}
/>
</div>
@@ -299,13 +388,13 @@ function KProfileModal({
}}
disabled={!!profile}
className={`w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none ${profile ? 'opacity-60 cursor-not-allowed' : ''}`}
required
required={!profile}
>
<option value="">Select filament...</option>
{/* Show current filament when editing */}
{/* Show current filament when editing - look up from knownFilaments */}
{profile?.filament_id && (
<option key={profile.filament_id} value={profile.filament_id}>
{extractFilamentName(profile.name || profile.filament_id)}
{knownFilaments.find(f => f.id === profile.filament_id)?.name || profile.filament_id}
</option>
)}
{/* Show known filaments from existing K-profiles (for new profiles) */}
@@ -364,19 +453,52 @@ function KProfileModal({
</div>
</div>
{/* Extruder - only show for dual-nozzle printers, read-only when editing */}
{/* Extruder - only show for dual-nozzle printers */}
{isDualNozzle && (
<div>
<label className="block text-sm text-bambu-gray mb-1">Extruder</label>
<select
value={extruderId}
onChange={(e) => setExtruderId(parseInt(e.target.value))}
disabled={!!profile}
className={`w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none ${profile ? 'opacity-60 cursor-not-allowed' : ''}`}
>
<option value={1}>Left</option>
<option value={0}>Right</option>
</select>
<label className="block text-sm text-bambu-gray mb-1">
{profile ? 'Extruder' : 'Extruders'}
</label>
{profile ? (
// Read-only display for editing
<div className="px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white opacity-60">
{profile.extruder_id === 1 ? 'Left' : 'Right'}
</div>
) : (
// Checkboxes for new profile - can select both
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedExtruders.includes(1)}
onChange={(e) => {
if (e.target.checked) {
setSelectedExtruders([...selectedExtruders, 1]);
} else {
setSelectedExtruders(selectedExtruders.filter(id => id !== 1));
}
}}
className="w-4 h-4 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green focus:ring-offset-0 accent-bambu-green"
/>
<span className="text-white">Left</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedExtruders.includes(0)}
onChange={(e) => {
if (e.target.checked) {
setSelectedExtruders([...selectedExtruders, 0]);
} else {
setSelectedExtruders(selectedExtruders.filter(id => id !== 0));
}
}}
className="w-4 h-4 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green focus:ring-offset-0 accent-bambu-green"
/>
<span className="text-white">Right</span>
</label>
</div>
)}
</div>
)}
@@ -386,7 +508,7 @@ function KProfileModal({
type="button"
variant="secondary"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteMutation.isPending}
disabled={deleteMutation.isPending || isSyncing}
className="text-red-500 hover:bg-red-500/10"
>
{deleteMutation.isPending ? (
@@ -400,13 +522,14 @@ function KProfileModal({
type="button"
variant="secondary"
onClick={onClose}
disabled={isSyncing}
className="flex-1"
>
Cancel
</Button>
<Button
type="submit"
disabled={saveMutation.isPending}
disabled={saveMutation.isPending || isSyncing}
className="flex-1"
>
{saveMutation.isPending ? (
+302 -145
View File
@@ -8,10 +8,8 @@ import {
Clock,
Plus,
Trash2,
Settings2,
ChevronDown,
ChevronUp,
RotateCcw,
Droplet,
Flame,
Ruler,
@@ -19,6 +17,7 @@ import {
Square,
Cable,
Edit3,
RotateCcw,
} from 'lucide-react';
import { api } from '../api/client';
import type { MaintenanceStatus, PrinterMaintenanceOverview } from '../api/client';
@@ -334,134 +333,266 @@ function PrinterSection({
);
}
// Settings modal for managing custom types
function SettingsModal({
onClose,
// Settings section component - maintenance types and per-printer interval overrides
function SettingsSection({
overview,
types,
onUpdateInterval,
onAddType,
onDeleteType,
}: {
onClose: () => void;
overview: PrinterMaintenanceOverview[] | undefined;
types: Array<{ id: number; name: string; default_interval_hours: number; icon: string | null; is_system: boolean }>;
onUpdateInterval: (id: number, customInterval: number | null) => void;
onAddType: (data: { name: string; description?: string; default_interval_hours: number; icon?: string }) => void;
onDeleteType: (id: number) => void;
}) {
const [name, setName] = useState('');
const [interval, setInterval] = useState('100');
const [icon, setIcon] = useState('Wrench');
const [editingInterval, setEditingInterval] = useState<number | null>(null);
const [intervalInput, setIntervalInput] = useState('');
const [showAddType, setShowAddType] = useState(false);
const [newTypeName, setNewTypeName] = useState('');
const [newTypeInterval, setNewTypeInterval] = useState('100');
const [newTypeIcon, setNewTypeIcon] = useState('Wrench');
const handleSubmit = (e: React.FormEvent) => {
const handleSaveInterval = (itemId: number, defaultInterval: number) => {
const newInterval = parseFloat(intervalInput);
if (!isNaN(newInterval) && newInterval > 0) {
const customInterval = Math.abs(newInterval - defaultInterval) < 0.01 ? null : newInterval;
onUpdateInterval(itemId, customInterval);
}
setEditingInterval(null);
};
const handleAddType = (e: React.FormEvent) => {
e.preventDefault();
if (name.trim() && parseFloat(interval) > 0) {
if (newTypeName.trim() && parseFloat(newTypeInterval) > 0) {
onAddType({
name: name.trim(),
default_interval_hours: parseFloat(interval),
icon,
name: newTypeName.trim(),
default_interval_hours: parseFloat(newTypeInterval),
icon: newTypeIcon,
});
setName('');
setInterval('100');
setNewTypeName('');
setNewTypeInterval('100');
setShowAddType(false);
}
};
const printerItems = overview?.map(p => ({
printerId: p.printer_id,
printerName: p.printer_name,
items: p.maintenance_items.sort((a, b) => a.maintenance_type_id - b.maintenance_type_id),
})).sort((a, b) => a.printerName.localeCompare(b.printerName)) || [];
const systemTypes = types.filter(t => t.is_system);
const customTypes = types.filter(t => !t.is_system);
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
<div className="bg-bambu-dark-secondary rounded-lg p-6 w-full max-w-lg" onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-semibold text-white mb-4">Maintenance Settings</h3>
{/* Existing custom types */}
{customTypes.length > 0 && (
<div className="mb-6">
<h4 className="text-sm text-bambu-gray mb-2">Custom Maintenance Types</h4>
<div className="space-y-2">
{customTypes.map((type) => {
const Icon = getIcon(type.icon);
return (
<div key={type.id} className="flex items-center justify-between p-2 bg-bambu-dark rounded">
<div className="flex items-center gap-2">
<Icon className="w-4 h-4 text-bambu-gray" />
<span className="text-white text-sm">{type.name}</span>
<span className="text-bambu-gray text-xs">({type.default_interval_hours}h)</span>
</div>
<button
onClick={() => {
if (confirm(`Delete "${type.name}"?`)) {
onDeleteType(type.id);
}
}}
className="p-1 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-red-400"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
);
})}
</div>
</div>
)}
{/* Add new type */}
<form onSubmit={handleSubmit}>
<h4 className="text-sm text-bambu-gray mb-2">Add Custom Type</h4>
<div className="flex gap-2 mb-3">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="flex-1 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm"
placeholder="Name (e.g., Replace HEPA Filter)"
/>
<input
type="number"
value={interval}
onChange={(e) => setInterval(e.target.value)}
className="w-20 px-2 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm"
placeholder="Hours"
min="1"
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-1">
{Object.keys(iconMap).map((iconName) => {
const IconComp = iconMap[iconName];
return (
<button
key={iconName}
type="button"
onClick={() => setIcon(iconName)}
className={`p-1.5 rounded ${
icon === iconName
? 'bg-bambu-green text-white'
: 'bg-bambu-dark text-bambu-gray hover:text-white'
}`}
>
<IconComp className="w-4 h-4" />
</button>
);
})}
</div>
<Button type="submit" size="sm" disabled={!name.trim()}>
<Plus className="w-4 h-4" />
Add
</Button>
</div>
</form>
<div className="mt-6 pt-4 border-t border-bambu-dark-tertiary flex justify-end">
<Button variant="secondary" onClick={onClose}>
Close
<div className="space-y-8">
{/* Maintenance Types */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">Maintenance Types</h2>
<Button size="sm" variant="secondary" onClick={() => setShowAddType(!showAddType)}>
<Plus className="w-4 h-4" />
Add Custom Type
</Button>
</div>
{/* Add custom type form */}
{showAddType && (
<Card className="mb-4">
<div className="p-4">
<form onSubmit={handleAddType}>
<div className="flex gap-3 items-end">
<div className="flex-1">
<label className="block text-xs text-bambu-gray mb-1">Name</label>
<input
type="text"
value={newTypeName}
onChange={(e) => setNewTypeName(e.target.value)}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm"
placeholder="e.g., Replace HEPA Filter"
autoFocus
/>
</div>
<div className="w-28">
<label className="block text-xs text-bambu-gray mb-1">Interval (hours)</label>
<input
type="number"
value={newTypeInterval}
onChange={(e) => setNewTypeInterval(e.target.value)}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm"
min="1"
/>
</div>
<div>
<label className="block text-xs text-bambu-gray mb-1">Icon</label>
<div className="flex gap-1">
{Object.keys(iconMap).map((iconName) => {
const IconComp = iconMap[iconName];
return (
<button
key={iconName}
type="button"
onClick={() => setNewTypeIcon(iconName)}
className={`p-2 rounded ${
newTypeIcon === iconName
? 'bg-bambu-green text-white'
: 'bg-bambu-dark-tertiary text-bambu-gray hover:text-white'
}`}
>
<IconComp className="w-4 h-4" />
</button>
);
})}
</div>
</div>
<div className="flex gap-2">
<Button type="submit" size="sm" disabled={!newTypeName.trim()}>Add</Button>
<Button type="button" size="sm" variant="secondary" onClick={() => setShowAddType(false)}>Cancel</Button>
</div>
</div>
</form>
</div>
</Card>
)}
{/* Types grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{/* System types */}
{systemTypes.map((type) => {
const Icon = getIcon(type.icon);
return (
<div key={type.id} className="bg-bambu-dark-secondary rounded-lg p-4 border border-bambu-dark-tertiary">
<div className="flex items-center gap-3">
<div className="p-2 bg-bambu-dark rounded-lg">
<Icon className="w-5 h-5 text-bambu-gray" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-white truncate">{type.name}</div>
<div className="text-xs text-bambu-gray mt-0.5">{type.default_interval_hours}h interval</div>
</div>
</div>
</div>
);
})}
{/* Custom types */}
{customTypes.map((type) => {
const Icon = getIcon(type.icon);
return (
<div key={type.id} className="bg-bambu-dark-secondary rounded-lg p-4 border border-bambu-green/30">
<div className="flex items-center gap-3">
<div className="p-2 bg-bambu-green/20 rounded-lg">
<Icon className="w-5 h-5 text-bambu-green" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-white truncate">{type.name}</span>
<span className="px-1.5 py-0.5 bg-bambu-green/20 text-bambu-green text-[10px] font-medium rounded">Custom</span>
</div>
<div className="text-xs text-bambu-gray mt-0.5">{type.default_interval_hours}h interval</div>
</div>
<button
onClick={() => {
if (confirm(`Delete "${type.name}"?`)) {
onDeleteType(type.id);
}
}}
className="p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
</div>
{/* Per-printer interval overrides */}
{printerItems.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-white mb-2">Interval Overrides</h2>
<p className="text-sm text-bambu-gray mb-4">
Set custom intervals per printer.
</p>
<div className="space-y-3">
{printerItems.map((printer) => (
<Card key={printer.printerId}>
<div className="p-4">
<h3 className="text-sm font-medium text-white mb-3">{printer.printerName}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{printer.items.map((item) => {
const Icon = getIcon(item.maintenance_type_icon);
const typeInfo = types.find(t => t.id === item.maintenance_type_id);
const defaultInterval = typeInfo?.default_interval_hours || item.interval_hours;
const isEditing = editingInterval === item.id;
return (
<div key={item.id} className="flex items-center gap-2 p-2 bg-bambu-dark rounded-lg">
<Icon className="w-4 h-4 text-bambu-gray shrink-0" />
<span className="text-xs text-bambu-gray flex-1 truncate">{item.maintenance_type_name}</span>
{isEditing ? (
<div className="flex items-center gap-1">
<input
type="number"
value={intervalInput}
onChange={(e) => setIntervalInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveInterval(item.id, defaultInterval);
if (e.key === 'Escape') setEditingInterval(null);
}}
className="w-16 px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-white text-xs"
min="1"
autoFocus
/>
<Button size="sm" onClick={() => handleSaveInterval(item.id, defaultInterval)}>OK</Button>
</div>
) : (
<button
onClick={() => {
setEditingInterval(item.id);
setIntervalInput(item.interval_hours.toString());
}}
className="px-2 py-1 bg-bambu-dark-tertiary hover:bg-bambu-dark-secondary border border-bambu-dark-tertiary hover:border-bambu-green rounded text-xs font-medium text-white transition-colors"
>
{item.interval_hours}h
<Edit3 className="w-3 h-3 inline ml-1.5 text-bambu-gray" />
</button>
)}
</div>
);
})}
</div>
</div>
</Card>
))}
</div>
</div>
)}
{printerItems.length === 0 && (
<Card>
<CardContent className="text-center py-12">
<Clock className="w-12 h-12 mx-auto mb-4 text-bambu-gray/30" />
<p className="text-bambu-gray">No printers configured</p>
<p className="text-sm text-bambu-gray/70 mt-1">
Add printers to configure maintenance intervals
</p>
</CardContent>
</Card>
)}
</div>
);
}
type TabType = 'status' | 'settings';
export function MaintenancePage() {
const queryClient = useQueryClient();
const { showToast } = useToast();
const [showSettings, setShowSettings] = useState(false);
const [activeTab, setActiveTab] = useState<TabType>('status');
const { data: overview, isLoading } = useQuery({
queryKey: ['maintenanceOverview'],
@@ -561,52 +692,78 @@ export function MaintenancePage() {
return (
<div className="p-8">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Maintenance</h1>
<p className="text-bambu-gray text-sm">
{totalDue > 0 && <span className="text-red-400">{totalDue} tasks overdue</span>}
{totalDue > 0 && totalWarning > 0 && ' · '}
{totalWarning > 0 && <span className="text-yellow-400">{totalWarning} due soon</span>}
{totalDue === 0 && totalWarning === 0 && 'All maintenance up to date'}
</p>
</div>
<Button variant="secondary" onClick={() => setShowSettings(true)}>
<Settings2 className="w-4 h-4" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-white">Maintenance</h1>
<p className="text-bambu-gray text-sm">
{activeTab === 'status' ? (
<>
{totalDue > 0 && <span className="text-red-400">{totalDue} tasks overdue</span>}
{totalDue > 0 && totalWarning > 0 && ' · '}
{totalWarning > 0 && <span className="text-yellow-400">{totalWarning} due soon</span>}
{totalDue === 0 && totalWarning === 0 && 'All maintenance up to date'}
</>
) : (
'Configure maintenance types and intervals'
)}
</p>
</div>
{/* Tabs */}
<div className="flex gap-1 mb-6 border-b border-bambu-dark-tertiary">
<button
onClick={() => setActiveTab('status')}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
activeTab === 'status'
? 'text-bambu-green border-bambu-green'
: 'text-bambu-gray border-transparent hover:text-white'
}`}
>
Status
</button>
<button
onClick={() => setActiveTab('settings')}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
activeTab === 'settings'
? 'text-bambu-green border-bambu-green'
: 'text-bambu-gray border-transparent hover:text-white'
}`}
>
Settings
</Button>
</button>
</div>
{/* Printers - sorted alphabetically */}
<div className="space-y-4">
{overview && overview.length > 0 ? (
[...overview].sort((a, b) => a.printer_name.localeCompare(b.printer_name)).map((printerOverview) => (
<PrinterSection
key={printerOverview.printer_id}
overview={printerOverview}
onPerform={handlePerform}
onToggle={handleToggle}
onSetHours={handleSetHours}
/>
))
) : (
<Card>
<CardContent className="text-center py-12">
<Wrench className="w-12 h-12 mx-auto mb-4 text-bambu-gray/30" />
<p className="text-bambu-gray">No printers configured</p>
<p className="text-sm text-bambu-gray/70 mt-1">
Add printers to start tracking maintenance
</p>
</CardContent>
</Card>
)}
</div>
{/* Settings modal */}
{showSettings && types && (
<SettingsModal
onClose={() => setShowSettings(false)}
types={types}
{/* Tab content */}
{activeTab === 'status' ? (
<div className="space-y-4">
{overview && overview.length > 0 ? (
[...overview].sort((a, b) => a.printer_name.localeCompare(b.printer_name)).map((printerOverview) => (
<PrinterSection
key={printerOverview.printer_id}
overview={printerOverview}
onPerform={handlePerform}
onToggle={handleToggle}
onSetHours={handleSetHours}
/>
))
) : (
<Card>
<CardContent className="text-center py-12">
<Wrench className="w-12 h-12 mx-auto mb-4 text-bambu-gray/30" />
<p className="text-bambu-gray">No printers configured</p>
<p className="text-sm text-bambu-gray/70 mt-1">
Add printers to start tracking maintenance
</p>
</CardContent>
</Card>
)}
</div>
) : (
<SettingsSection
overview={overview}
types={types || []}
onUpdateInterval={(id, customInterval) =>
updateMutation.mutate({ id, data: { custom_interval_hours: customInterval } })
}
onAddType={(data) => addTypeMutation.mutate(data)}
onDeleteType={(id) => deleteTypeMutation.mutate(id)}
/>
+11 -1
View File
@@ -87,6 +87,7 @@ function CoverImage({ url, printName }: { url: string | null; printName?: string
interface PrinterMaintenanceInfo {
due_count: number;
warning_count: number;
total_print_hours: number;
}
function PrinterCard({
@@ -175,7 +176,15 @@ function PrinterCard({
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-white">{printer.name}</h3>
<p className="text-sm text-bambu-gray">{printer.model || 'Unknown Model'}</p>
<p className="text-sm text-bambu-gray">
{printer.model || 'Unknown Model'}
{maintenanceInfo && maintenanceInfo.total_print_hours > 0 && (
<span className="ml-2 text-bambu-gray">
<Clock className="w-3 h-3 inline-block mr-1" />
{Math.round(maintenanceInfo.total_print_hours)}h
</span>
)}
</p>
</div>
<div className="flex items-center gap-2">
<span
@@ -726,6 +735,7 @@ export function PrintersPage() {
acc[overview.printer_id] = {
due_count: overview.due_count,
warning_count: overview.warning_count,
total_print_hours: overview.total_print_hours,
};
return acc;
},
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""MQTT Sniffer for Bambu Lab printers.
Connects to a printer and logs all MQTT messages to capture the exact
command format used by OrcaSlicer or Bambu Studio.
Usage:
python mqtt_sniffer.py <printer_ip> <serial_number> <access_code>
Example:
python mqtt_sniffer.py 192.168.1.100 0948BB540200427 12345678
"""
import json
import ssl
import sys
import time
from datetime import datetime
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, rc):
"""Called when connected to the MQTT broker."""
if rc == 0:
print(f"[{datetime.now().strftime('%H:%M:%S')}] Connected to printer!")
serial = userdata["serial"]
# Subscribe to all topics for this printer
topic_report = f"device/{serial}/report"
client.subscribe(topic_report)
print(f"[{datetime.now().strftime('%H:%M:%S')}] Subscribed to: {topic_report}")
print("-" * 80)
print("Listening for MQTT messages... Press Ctrl+C to stop.")
print("Now use OrcaSlicer to add a K-profile and the command will be logged here.")
print("-" * 80)
else:
print(f"[{datetime.now().strftime('%H:%M:%S')}] Connection failed with code: {rc}")
def on_message(client, userdata, msg):
"""Called when a message is received."""
try:
payload = json.loads(msg.payload.decode("utf-8"))
# Check if this is an extrusion_cali related message
is_cali_msg = False
command = None
if "print" in payload:
command = payload["print"].get("command", "")
if "extrusion_cali" in command:
is_cali_msg = True
# Always log calibration messages with full detail
if is_cali_msg:
print(f"\n{'='*80}")
print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] *** CALIBRATION COMMAND: {command} ***")
print(f"Topic: {msg.topic}")
print(f"Full payload:")
print(json.dumps(payload, indent=2))
print(f"{'='*80}\n")
else:
# For other messages, just show a brief summary
if "print" in payload:
cmd = payload["print"].get("command", "unknown")
# Skip noisy status messages
if cmd not in ["push_status"]:
print(f"[{datetime.now().strftime('%H:%M:%S')}] Command: {cmd}")
except json.JSONDecodeError:
print(f"[{datetime.now().strftime('%H:%M:%S')}] Non-JSON message on {msg.topic}")
except Exception as e:
print(f"[{datetime.now().strftime('%H:%M:%S')}] Error processing message: {e}")
def on_disconnect(client, userdata, rc):
"""Called when disconnected from the MQTT broker."""
print(f"[{datetime.now().strftime('%H:%M:%S')}] Disconnected with code: {rc}")
def main():
if len(sys.argv) != 4:
print("Usage: python mqtt_sniffer.py <printer_ip> <serial_number> <access_code>")
print("\nExample:")
print(" python mqtt_sniffer.py 192.168.1.100 0948BB540200427 12345678")
sys.exit(1)
printer_ip = sys.argv[1]
serial_number = sys.argv[2]
access_code = sys.argv[3]
print(f"Connecting to printer at {printer_ip}...")
print(f"Serial: {serial_number}")
# Create MQTT client
client = mqtt.Client(userdata={"serial": serial_number})
client.username_pw_set("bblp", access_code)
# Configure TLS
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
client.tls_set_context(ssl_context)
# Set callbacks
client.on_connect = on_connect
client.on_message = on_message
client.on_disconnect = on_disconnect
try:
client.connect(printer_ip, 8883, 60)
client.loop_forever()
except KeyboardInterrupt:
print("\n\nStopping sniffer...")
client.disconnect()
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
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
View File
@@ -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-CKBzHHT0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-H_ymON9v.css">
<script type="module" crossorigin src="/assets/index-soeWpmRO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-4olH2ask.css">
</head>
<body>
<div id="root"></div>