mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 15:35:39 +02:00
Add virtual printer model selection
Features:
- Configurable printer model for virtual printer emulation
- Supports X1 series (X1C, X1, X1E), P series (P1S, P1P, P2S),
A1 series (A1, A1 Mini), and H2 series (H2D, H2C, H2S)
- Dropdown in Settings > Virtual Printer to select model
- Model affects SSDP discovery and slicer compatibility
- Model change restarts virtual printer services automatically
Backend:
- Added VIRTUAL_PRINTER_MODELS mapping in manager.py
- Added virtual_printer_model setting in database
- New GET /api/v1/settings/virtual-printer/models endpoint
- Updated PUT /api/v1/settings/virtual-printer to accept model
Frontend:
- Added model dropdown to VirtualPrinterSettings component
- Status display shows selected model name
- Model change disabled while virtual printer is running
Tests:
- Added 3 unit tests for model configuration
- Updated frontend test mocks for getModels API
This commit is contained in:
@@ -16,6 +16,11 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- Play button to manually release staged prints to the queue
|
||||
- Edit queue items to switch between ASAP, Scheduled, and Queue Only modes
|
||||
- Useful for preparing print batches before activating
|
||||
- **Virtual printer model selection** - Choose which Bambu printer model to emulate:
|
||||
- Dropdown in Settings > Virtual Printer to select model
|
||||
- Supports X1 series (X1C, X1, X1E), P series (P1S, P1P, P2S), A1 series (A1, A1 Mini), and H2 series (H2D, H2C, H2S)
|
||||
- Affects how slicers detect and interact with the virtual printer
|
||||
- Model change requires disabling/re-enabling the virtual printer
|
||||
|
||||
### Fixed
|
||||
- **Camera stream reconnection** - Improved detection of stuck camera streams with automatic reconnection
|
||||
@@ -23,6 +28,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
### Tests
|
||||
- Added integration tests for print queue API endpoints (16 new tests)
|
||||
- Tests cover queue CRUD, manual_start flag, and start/cancel endpoints
|
||||
- Added unit tests for virtual printer model configuration (3 new tests)
|
||||
|
||||
## [0.1.6b5] - 2026-01-02
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
### 🖨️ Virtual Printer
|
||||
- Emulates a Bambu Lab printer on your network
|
||||
- Send prints directly from Bambu Studio/Orca Slicer
|
||||
- Configurable printer model (X1C, P1S, A1, H2D, etc.)
|
||||
- Queue mode or auto-start mode
|
||||
- SSDP discovery (appears in slicer automatically)
|
||||
- Secure TLS/MQTT communication
|
||||
|
||||
@@ -1592,22 +1592,26 @@ async def import_backup(
|
||||
vp_enabled = await get_setting(db, "virtual_printer_enabled")
|
||||
vp_access_code = await get_setting(db, "virtual_printer_access_code")
|
||||
vp_mode = await get_setting(db, "virtual_printer_mode")
|
||||
vp_model = await get_setting(db, "virtual_printer_model")
|
||||
|
||||
enabled = vp_enabled and vp_enabled.lower() == "true"
|
||||
access_code = vp_access_code or ""
|
||||
mode = vp_mode or "immediate"
|
||||
model = vp_model or ""
|
||||
|
||||
if enabled and access_code:
|
||||
await virtual_printer_manager.configure(
|
||||
enabled=True,
|
||||
access_code=access_code,
|
||||
mode=mode,
|
||||
model=model,
|
||||
)
|
||||
elif not enabled and virtual_printer_manager.is_enabled:
|
||||
await virtual_printer_manager.configure(
|
||||
enabled=False,
|
||||
access_code=access_code,
|
||||
mode=mode,
|
||||
model=model,
|
||||
)
|
||||
except Exception:
|
||||
pass # Virtual printer config failed, but don't fail the restore
|
||||
@@ -1649,19 +1653,38 @@ async def import_backup(
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/virtual-printer/models")
|
||||
async def get_virtual_printer_models():
|
||||
"""Get available virtual printer models."""
|
||||
from backend.app.services.virtual_printer import (
|
||||
DEFAULT_VIRTUAL_PRINTER_MODEL,
|
||||
VIRTUAL_PRINTER_MODELS,
|
||||
)
|
||||
|
||||
return {
|
||||
"models": VIRTUAL_PRINTER_MODELS,
|
||||
"default": DEFAULT_VIRTUAL_PRINTER_MODEL,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/virtual-printer")
|
||||
async def get_virtual_printer_settings(db: AsyncSession = Depends(get_db)):
|
||||
"""Get virtual printer settings and status."""
|
||||
from backend.app.services.virtual_printer import virtual_printer_manager
|
||||
from backend.app.services.virtual_printer import (
|
||||
DEFAULT_VIRTUAL_PRINTER_MODEL,
|
||||
virtual_printer_manager,
|
||||
)
|
||||
|
||||
enabled = await get_setting(db, "virtual_printer_enabled")
|
||||
access_code = await get_setting(db, "virtual_printer_access_code")
|
||||
mode = await get_setting(db, "virtual_printer_mode")
|
||||
model = await get_setting(db, "virtual_printer_model")
|
||||
|
||||
return {
|
||||
"enabled": enabled == "true" if enabled else False,
|
||||
"access_code_set": bool(access_code),
|
||||
"mode": mode or "immediate",
|
||||
"model": model or DEFAULT_VIRTUAL_PRINTER_MODEL,
|
||||
"status": virtual_printer_manager.get_status(),
|
||||
}
|
||||
|
||||
@@ -1671,20 +1694,27 @@ async def update_virtual_printer_settings(
|
||||
enabled: bool = None,
|
||||
access_code: str = None,
|
||||
mode: str = None,
|
||||
model: str = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update virtual printer settings and restart services if needed."""
|
||||
from backend.app.services.virtual_printer import virtual_printer_manager
|
||||
from backend.app.services.virtual_printer import (
|
||||
DEFAULT_VIRTUAL_PRINTER_MODEL,
|
||||
VIRTUAL_PRINTER_MODELS,
|
||||
virtual_printer_manager,
|
||||
)
|
||||
|
||||
# Get current values
|
||||
current_enabled = await get_setting(db, "virtual_printer_enabled") == "true"
|
||||
current_access_code = await get_setting(db, "virtual_printer_access_code") or ""
|
||||
current_mode = await get_setting(db, "virtual_printer_mode") or "immediate"
|
||||
current_model = await get_setting(db, "virtual_printer_model") or DEFAULT_VIRTUAL_PRINTER_MODEL
|
||||
|
||||
# Apply updates
|
||||
new_enabled = enabled if enabled is not None else current_enabled
|
||||
new_access_code = access_code if access_code is not None else current_access_code
|
||||
new_mode = mode if mode is not None else current_mode
|
||||
new_model = model if model is not None else current_model
|
||||
|
||||
# Validate mode
|
||||
if new_mode not in ("immediate", "queue"):
|
||||
@@ -1693,6 +1723,13 @@ async def update_virtual_printer_settings(
|
||||
content={"detail": "Mode must be 'immediate' or 'queue'"},
|
||||
)
|
||||
|
||||
# Validate model
|
||||
if model is not None and model not in VIRTUAL_PRINTER_MODELS:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"detail": f"Invalid model. Must be one of: {', '.join(VIRTUAL_PRINTER_MODELS.keys())}"},
|
||||
)
|
||||
|
||||
# Validate access code when enabling
|
||||
if new_enabled and not new_access_code:
|
||||
return JSONResponse(
|
||||
@@ -1712,6 +1749,8 @@ async def update_virtual_printer_settings(
|
||||
if access_code is not None:
|
||||
await set_setting(db, "virtual_printer_access_code", access_code)
|
||||
await set_setting(db, "virtual_printer_mode", new_mode)
|
||||
if model is not None:
|
||||
await set_setting(db, "virtual_printer_model", model)
|
||||
await db.commit()
|
||||
|
||||
# Reconfigure virtual printer
|
||||
@@ -1720,6 +1759,7 @@ async def update_virtual_printer_settings(
|
||||
enabled=new_enabled,
|
||||
access_code=new_access_code,
|
||||
mode=new_mode,
|
||||
model=new_model,
|
||||
)
|
||||
except ValueError as e:
|
||||
return JSONResponse(
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
"""Virtual printer services for slicer integration."""
|
||||
|
||||
from backend.app.services.virtual_printer.manager import virtual_printer_manager
|
||||
from backend.app.services.virtual_printer.manager import (
|
||||
DEFAULT_VIRTUAL_PRINTER_MODEL,
|
||||
VIRTUAL_PRINTER_MODELS,
|
||||
virtual_printer_manager,
|
||||
)
|
||||
|
||||
__all__ = ["virtual_printer_manager"]
|
||||
__all__ = [
|
||||
"virtual_printer_manager",
|
||||
"VIRTUAL_PRINTER_MODELS",
|
||||
"DEFAULT_VIRTUAL_PRINTER_MODEL",
|
||||
]
|
||||
|
||||
@@ -15,13 +15,36 @@ from backend.app.services.virtual_printer.ssdp_server import VirtualPrinterSSDPS
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Mapping of SSDP model codes to display names
|
||||
# These are the codes that slicers expect during discovery
|
||||
VIRTUAL_PRINTER_MODELS = {
|
||||
# X1 Series
|
||||
"BL-P001": "X1C", # X1 Carbon
|
||||
"BL-P002": "X1", # X1
|
||||
"BL-P003": "X1E", # X1E
|
||||
# P Series
|
||||
"C11": "P1S", # P1S
|
||||
"C12": "P1P", # P1P
|
||||
"C13": "P2S", # P2S
|
||||
# A1 Series
|
||||
"N2S": "A1", # A1
|
||||
"N1": "A1 Mini", # A1 Mini
|
||||
# H2 Series
|
||||
"O1D": "H2D", # H2D
|
||||
"O1C": "H2C", # H2C
|
||||
"O1S": "H2S", # H2S
|
||||
}
|
||||
|
||||
# Default model
|
||||
DEFAULT_VIRTUAL_PRINTER_MODEL = "BL-P001" # X1C
|
||||
|
||||
|
||||
class VirtualPrinterManager:
|
||||
"""Manages the virtual printer lifecycle and coordinates all services."""
|
||||
|
||||
# Fixed configuration
|
||||
PRINTER_NAME = "Bambuddy"
|
||||
PRINTER_SERIAL = "00M09A391800001" # X1C serial format
|
||||
PRINTER_MODEL = "3DPrinter-X1-Carbon" # Full model name for slicer compatibility
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the virtual printer manager."""
|
||||
@@ -29,6 +52,7 @@ class VirtualPrinterManager:
|
||||
self._enabled = False
|
||||
self._access_code = ""
|
||||
self._mode = "immediate"
|
||||
self._model = DEFAULT_VIRTUAL_PRINTER_MODEL
|
||||
|
||||
# Service instances
|
||||
self._ssdp: VirtualPrinterSSDPServer | None = None
|
||||
@@ -72,6 +96,7 @@ class VirtualPrinterManager:
|
||||
enabled: bool,
|
||||
access_code: str = "",
|
||||
mode: str = "immediate",
|
||||
model: str = "",
|
||||
) -> None:
|
||||
"""Configure and start/stop virtual printer.
|
||||
|
||||
@@ -79,17 +104,27 @@ class VirtualPrinterManager:
|
||||
enabled: Whether to enable the virtual printer
|
||||
access_code: Authentication password for slicer connections
|
||||
mode: Archive mode - 'immediate' or 'queue'
|
||||
model: SSDP model code (e.g., 'BL-P001' for X1C)
|
||||
"""
|
||||
if enabled and not access_code:
|
||||
raise ValueError("Access code is required when enabling virtual printer")
|
||||
|
||||
# Validate model if provided
|
||||
new_model = model if model and model in VIRTUAL_PRINTER_MODELS else self._model
|
||||
model_changed = new_model != self._model
|
||||
|
||||
self._access_code = access_code
|
||||
self._mode = mode
|
||||
self._model = new_model
|
||||
|
||||
if enabled and not self._enabled:
|
||||
await self._start()
|
||||
elif not enabled and self._enabled:
|
||||
await self._stop()
|
||||
elif enabled and self._enabled and model_changed:
|
||||
# Model changed while running - restart services
|
||||
await self._stop()
|
||||
await self._start()
|
||||
|
||||
self._enabled = enabled
|
||||
|
||||
@@ -108,7 +143,7 @@ class VirtualPrinterManager:
|
||||
self._ssdp = VirtualPrinterSSDPServer(
|
||||
name=self.PRINTER_NAME,
|
||||
serial=self.PRINTER_SERIAL,
|
||||
model=self.PRINTER_MODEL,
|
||||
model=self._model,
|
||||
)
|
||||
|
||||
self._ftp = VirtualPrinterFTPServer(
|
||||
@@ -314,7 +349,8 @@ class VirtualPrinterManager:
|
||||
"mode": self._mode,
|
||||
"name": self.PRINTER_NAME,
|
||||
"serial": self.PRINTER_SERIAL,
|
||||
"model": self.PRINTER_MODEL,
|
||||
"model": self._model,
|
||||
"model_name": VIRTUAL_PRINTER_MODELS.get(self._model, self._model),
|
||||
"pending_files": len(self._pending_files),
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,54 @@ class TestVirtualPrinterManager:
|
||||
with pytest.raises(ValueError, match="Access code is required"):
|
||||
await manager.configure(enabled=True)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_configure_sets_model(self, manager):
|
||||
"""Verify configure stores model correctly."""
|
||||
manager._start = AsyncMock()
|
||||
|
||||
await manager.configure(
|
||||
enabled=True,
|
||||
access_code="12345678",
|
||||
mode="immediate",
|
||||
model="C11", # P1S model code
|
||||
)
|
||||
|
||||
assert manager._model == "C11"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_configure_ignores_invalid_model(self, manager):
|
||||
"""Verify configure ignores invalid model codes."""
|
||||
manager._start = AsyncMock()
|
||||
|
||||
await manager.configure(
|
||||
enabled=True,
|
||||
access_code="12345678",
|
||||
model="INVALID",
|
||||
)
|
||||
|
||||
# Should keep default model
|
||||
assert manager._model == "BL-P001"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_configure_restarts_on_model_change(self, manager):
|
||||
"""Verify model change restarts services when running."""
|
||||
# Simulate running state
|
||||
manager._enabled = True
|
||||
manager._model = "BL-P001"
|
||||
manager._tasks = [MagicMock(done=MagicMock(return_value=False))]
|
||||
manager._stop = AsyncMock()
|
||||
manager._start = AsyncMock()
|
||||
|
||||
await manager.configure(
|
||||
enabled=True,
|
||||
access_code="12345678",
|
||||
model="C11",
|
||||
)
|
||||
|
||||
# Should have stopped and started
|
||||
manager._stop.assert_called_once()
|
||||
manager._start.assert_called_once()
|
||||
|
||||
# ========================================================================
|
||||
# Tests for status
|
||||
# ========================================================================
|
||||
@@ -67,6 +115,7 @@ class TestVirtualPrinterManager:
|
||||
"""Verify get_status returns expected fields."""
|
||||
manager._enabled = True
|
||||
manager._mode = "immediate"
|
||||
manager._model = "C11"
|
||||
manager._pending_files = {"file1.3mf": Path("/tmp/file1.3mf")}
|
||||
# Simulate running tasks
|
||||
manager._tasks = [MagicMock(done=MagicMock(return_value=False))]
|
||||
@@ -78,6 +127,8 @@ class TestVirtualPrinterManager:
|
||||
assert status["mode"] == "immediate"
|
||||
assert status["name"] == "Bambuddy"
|
||||
assert status["serial"] == "00M09A391800001"
|
||||
assert status["model"] == "C11"
|
||||
assert status["model_name"] == "P1S"
|
||||
assert status["pending_files"] == 1
|
||||
|
||||
def test_get_status_when_stopped(self, manager):
|
||||
|
||||
@@ -23,6 +23,7 @@ vi.mock('../../api/client', () => ({
|
||||
virtualPrinterApi: {
|
||||
getSettings: vi.fn(),
|
||||
updateSettings: vi.fn(),
|
||||
getModels: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -34,23 +35,38 @@ const createMockSettings = (overrides = {}) => ({
|
||||
enabled: false,
|
||||
access_code_set: false,
|
||||
mode: 'immediate' as const,
|
||||
model: 'BL-P001',
|
||||
status: {
|
||||
enabled: false,
|
||||
running: false,
|
||||
mode: 'immediate',
|
||||
name: 'Bambuddy',
|
||||
serial: '00M09A391800001',
|
||||
model: 'BL-P001',
|
||||
model_name: 'X1C',
|
||||
pending_files: 0,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const mockModelsData = {
|
||||
models: {
|
||||
'BL-P001': 'X1C',
|
||||
'BL-P002': 'X1',
|
||||
'BL-P003': 'X1E',
|
||||
'C11': 'P1S',
|
||||
'C12': 'P1P',
|
||||
},
|
||||
default: 'BL-P001',
|
||||
};
|
||||
|
||||
describe('VirtualPrinterSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Default mock implementation
|
||||
vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(createMockSettings());
|
||||
vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(createMockSettings());
|
||||
vi.mocked(virtualPrinterApi.getModels).mockResolvedValue(mockModelsData);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
|
||||
@@ -2450,6 +2450,7 @@ export interface VirtualPrinterStatus {
|
||||
name: string;
|
||||
serial: string;
|
||||
model: string;
|
||||
model_name: string;
|
||||
pending_files: number;
|
||||
}
|
||||
|
||||
@@ -2457,9 +2458,15 @@ export interface VirtualPrinterSettings {
|
||||
enabled: boolean;
|
||||
access_code_set: boolean;
|
||||
mode: 'immediate' | 'queue';
|
||||
model: string;
|
||||
status: VirtualPrinterStatus;
|
||||
}
|
||||
|
||||
export interface VirtualPrinterModels {
|
||||
models: Record<string, string>; // SSDP code -> display name
|
||||
default: string;
|
||||
}
|
||||
|
||||
export interface PendingUpload {
|
||||
id: number;
|
||||
filename: string;
|
||||
@@ -2476,15 +2483,19 @@ export interface PendingUpload {
|
||||
export const virtualPrinterApi = {
|
||||
getSettings: () => request<VirtualPrinterSettings>('/settings/virtual-printer'),
|
||||
|
||||
getModels: () => request<VirtualPrinterModels>('/settings/virtual-printer/models'),
|
||||
|
||||
updateSettings: (data: {
|
||||
enabled?: boolean;
|
||||
access_code?: string;
|
||||
mode?: 'immediate' | 'queue';
|
||||
model?: string;
|
||||
}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (data.enabled !== undefined) params.set('enabled', String(data.enabled));
|
||||
if (data.access_code !== undefined) params.set('access_code', data.access_code);
|
||||
if (data.mode !== undefined) params.set('mode', data.mode);
|
||||
if (data.model !== undefined) params.set('model', data.model);
|
||||
|
||||
return request<VirtualPrinterSettings>(`/settings/virtual-printer?${params.toString()}`, {
|
||||
method: 'PUT',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Loader2, Check, AlertTriangle, Printer, Eye, EyeOff, Info } from 'lucide-react';
|
||||
import { Loader2, Check, AlertTriangle, Printer, Eye, EyeOff, Info, ChevronDown } from 'lucide-react';
|
||||
import { virtualPrinterApi } from '../api/client';
|
||||
import { Card, CardContent, CardHeader } from './Card';
|
||||
import { Button } from './Button';
|
||||
@@ -13,6 +13,7 @@ export function VirtualPrinterSettings() {
|
||||
const [localEnabled, setLocalEnabled] = useState(false);
|
||||
const [localAccessCode, setLocalAccessCode] = useState('');
|
||||
const [localMode, setLocalMode] = useState<'immediate' | 'queue'>('immediate');
|
||||
const [localModel, setLocalModel] = useState('BL-P001');
|
||||
const [showAccessCode, setShowAccessCode] = useState(false);
|
||||
|
||||
// Fetch current settings
|
||||
@@ -22,17 +23,24 @@ export function VirtualPrinterSettings() {
|
||||
refetchInterval: 10000, // Refresh every 10 seconds for status updates
|
||||
});
|
||||
|
||||
// Fetch available models
|
||||
const { data: modelsData } = useQuery({
|
||||
queryKey: ['virtual-printer-models'],
|
||||
queryFn: virtualPrinterApi.getModels,
|
||||
});
|
||||
|
||||
// Initialize local state from settings
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setLocalEnabled(settings.enabled);
|
||||
setLocalMode(settings.mode);
|
||||
setLocalModel(settings.model);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
// Update mutation
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: { enabled?: boolean; access_code?: string; mode?: 'immediate' | 'queue' }) =>
|
||||
mutationFn: (data: { enabled?: boolean; access_code?: string; mode?: 'immediate' | 'queue'; model?: string }) =>
|
||||
virtualPrinterApi.updateSettings(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['virtual-printer-settings'] });
|
||||
@@ -44,6 +52,7 @@ export function VirtualPrinterSettings() {
|
||||
if (settings) {
|
||||
setLocalEnabled(settings.enabled);
|
||||
setLocalMode(settings.mode);
|
||||
setLocalModel(settings.model);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -87,6 +96,11 @@ export function VirtualPrinterSettings() {
|
||||
updateMutation.mutate({ mode });
|
||||
};
|
||||
|
||||
const handleModelChange = (model: string) => {
|
||||
setLocalModel(model);
|
||||
updateMutation.mutate({ model });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
@@ -148,6 +162,35 @@ export function VirtualPrinterSettings() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Printer Model */}
|
||||
<div className="py-3 border-t border-bambu-dark-tertiary">
|
||||
<div className="text-white font-medium mb-2">Printer Model</div>
|
||||
<div className="text-sm text-bambu-gray mb-3">
|
||||
Select which printer model to emulate.
|
||||
</div>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={localModel}
|
||||
onChange={(e) => handleModelChange(e.target.value)}
|
||||
disabled={updateMutation.isPending || isRunning}
|
||||
className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white appearance-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed pr-10"
|
||||
>
|
||||
{modelsData?.models && Object.entries(modelsData.models).map(([code, name]) => (
|
||||
<option key={code} value={code}>
|
||||
{name} ({code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
|
||||
</div>
|
||||
{isRunning && (
|
||||
<p className="text-xs text-yellow-400 mt-2">
|
||||
<AlertTriangle className="w-3 h-3 inline mr-1" />
|
||||
Disable the virtual printer to change the model
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Access Code */}
|
||||
<div className="py-3 border-t border-bambu-dark-tertiary">
|
||||
<div className="text-white font-medium mb-2">Access Code</div>
|
||||
@@ -264,6 +307,20 @@ export function VirtualPrinterSettings() {
|
||||
sudo iptables -t nat -A OUTPUT -o lo -p tcp --dport 990 -j REDIRECT --to-port 9990
|
||||
</code>
|
||||
</div>
|
||||
<div className="mt-3 p-2 bg-blue-500/10 border border-blue-500/30 rounded text-xs">
|
||||
<strong className="text-blue-400">Docker users:</strong>{' '}
|
||||
<span className="text-bambu-gray">
|
||||
Host network mode is required for SSDP discovery.{' '}
|
||||
<a
|
||||
href="https://wiki.bambuddy.cool/features/virtual-printer/#docker-configuration"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:underline"
|
||||
>
|
||||
See Docker configuration guide →
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -283,7 +340,7 @@ export function VirtualPrinterSettings() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-bambu-gray">Model</div>
|
||||
<div className="text-white">{status.model?.replace('3DPrinter-', '').replace('-', ' ') || 'X1 Carbon'}</div>
|
||||
<div className="text-white">{status.model_name || status.model}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-bambu-gray">Serial Number</div>
|
||||
|
||||
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
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DXIzheUf.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-YKsbWJQ9.css">
|
||||
<script type="module" crossorigin src="/assets/index-vV_B6YVc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B3w4bCH5.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user