- Add subnet scanning for printer discovery in Docker environments

- Detect Docker environment automatically via /.dockerenv and cgroup
  - Show subnet input field in Add Printer dialog when running in Docker
  - Scan IP range for Bambu printer ports (8883 MQTT, 990 FTPS)
  - Query SSDP to get printer name/serial/model from discovered IPs
  - Map raw SSDP model codes to friendly names (BL-P001→X1C, O1D→H2D)
This commit is contained in:
maziggy
2025-12-28 09:40:47 +01:00
parent 017e36a0a6
commit 8acc91d3cd
10 changed files with 673 additions and 119 deletions
+26
View File
@@ -2190,8 +2190,22 @@ export interface DiscoveryStatus {
running: boolean;
}
export interface DiscoveryInfo {
is_docker: boolean;
ssdp_running: boolean;
scan_running: boolean;
}
export interface SubnetScanStatus {
running: boolean;
scanned: number;
total: number;
}
// Discovery API
export const discoveryApi = {
getInfo: () => request<DiscoveryInfo>('/discovery/info'),
getStatus: () => request<DiscoveryStatus>('/discovery/status'),
startDiscovery: (duration: number = 10) =>
@@ -2202,4 +2216,16 @@ export const discoveryApi = {
getDiscoveredPrinters: () =>
request<DiscoveredPrinter[]>('/discovery/printers'),
// Subnet scanning (for Docker environments)
startSubnetScan: (subnet: string, timeout: number = 1.0) =>
request<SubnetScanStatus>('/discovery/scan', {
method: 'POST',
body: JSON.stringify({ subnet, timeout }),
}),
getScanStatus: () => request<SubnetScanStatus>('/discovery/scan/status'),
stopSubnetScan: () =>
request<SubnetScanStatus>('/discovery/scan/stop', { method: 'POST' }),
};
+94 -33
View File
@@ -1496,6 +1496,18 @@ function AddPrinterModal({
const [discovered, setDiscovered] = useState<DiscoveredPrinter[]>([]);
const [discoveryError, setDiscoveryError] = useState('');
const [hasScanned, setHasScanned] = useState(false);
const [isDocker, setIsDocker] = useState(false);
const [subnet, setSubnet] = useState('192.168.1.0/24');
const [scanProgress, setScanProgress] = useState({ scanned: 0, total: 0 });
// Fetch discovery info on mount
useEffect(() => {
discoveryApi.getInfo().then(info => {
setIsDocker(info.is_docker);
}).catch(() => {
// Ignore errors, assume not Docker
});
}, []);
// Filter out already-added printers
const newPrinters = discovered.filter(p => !existingSerials.includes(p.serial));
@@ -1505,38 +1517,64 @@ function AddPrinterModal({
setDiscovered([]);
setDiscovering(true);
setHasScanned(false);
setScanProgress({ scanned: 0, total: 0 });
try {
await discoveryApi.startDiscovery(10);
if (isDocker) {
// Use subnet scanning for Docker
await discoveryApi.startSubnetScan(subnet);
// Poll for discovered printers every second
const pollInterval = setInterval(async () => {
try {
const printers = await discoveryApi.getDiscoveredPrinters();
setDiscovered(printers);
} catch (e) {
console.error('Failed to get discovered printers:', e);
}
}, 1000);
// Poll for scan status and results
const pollInterval = setInterval(async () => {
try {
const status = await discoveryApi.getScanStatus();
setScanProgress({ scanned: status.scanned, total: status.total });
// Stop after 10 seconds
setTimeout(async () => {
clearInterval(pollInterval);
try {
await discoveryApi.stopDiscovery();
} catch (e) {
// Ignore stop errors
}
setDiscovering(false);
setHasScanned(true);
// Final fetch
try {
const printers = await discoveryApi.getDiscoveredPrinters();
setDiscovered(printers);
} catch (e) {
console.error('Failed to get final discovered printers:', e);
}
}, 10000);
const printers = await discoveryApi.getDiscoveredPrinters();
setDiscovered(printers);
if (!status.running) {
clearInterval(pollInterval);
setDiscovering(false);
setHasScanned(true);
}
} catch (e) {
console.error('Failed to get scan status:', e);
}
}, 500);
} else {
// Use SSDP discovery for native installs
await discoveryApi.startDiscovery(10);
// Poll for discovered printers every second
const pollInterval = setInterval(async () => {
try {
const printers = await discoveryApi.getDiscoveredPrinters();
setDiscovered(printers);
} catch (e) {
console.error('Failed to get discovered printers:', e);
}
}, 1000);
// Stop after 10 seconds
setTimeout(async () => {
clearInterval(pollInterval);
try {
await discoveryApi.stopDiscovery();
} catch (e) {
// Ignore stop errors
}
setDiscovering(false);
setHasScanned(true);
// Final fetch
try {
const printers = await discoveryApi.getDiscoveredPrinters();
setDiscovered(printers);
} catch (e) {
console.error('Failed to get final discovered printers:', e);
}
}, 10000);
}
} catch (e) {
console.error('Failed to start discovery:', e);
setDiscoveryError(e instanceof Error ? e.message : 'Failed to start discovery');
@@ -1596,6 +1634,7 @@ function AddPrinterModal({
useEffect(() => {
return () => {
discoveryApi.stopDiscovery().catch(() => {});
discoveryApi.stopSubnetScan().catch(() => {});
};
}, []);
@@ -1619,6 +1658,26 @@ function AddPrinterModal({
{/* Discovery Section */}
<div className="mb-4 pb-4 border-b border-bambu-dark-tertiary">
{isDocker && (
<div className="mb-3">
<label className="block text-sm text-bambu-gray mb-1">
Subnet to scan
</label>
<input
type="text"
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 text-sm"
value={subnet}
onChange={(e) => setSubnet(e.target.value)}
placeholder="192.168.1.0/24"
disabled={discovering}
/>
<p className="mt-1 text-xs text-bambu-gray">
Docker detected. Enter your printer's subnet in CIDR notation.
Requires <code className="text-bambu-green">network_mode: host</code> in docker-compose.yml.
</p>
</div>
)}
<Button
type="button"
variant="secondary"
@@ -1629,12 +1688,14 @@ function AddPrinterModal({
{discovering ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Scanning...
{isDocker && scanProgress.total > 0
? `Scanning... ${scanProgress.scanned}/${scanProgress.total}`
: 'Scanning...'}
</>
) : (
<>
<Search className="w-4 h-4" />
Discover Printers on Network
{isDocker ? 'Scan Subnet for Printers' : 'Discover Printers on Network'}
</>
)}
</Button>
@@ -1656,7 +1717,7 @@ function AddPrinterModal({
{printer.name || printer.serial}
</p>
<p className="text-xs text-bambu-gray truncate">
{printer.model || 'Unknown'} • {printer.ip_address}
{mapModelCode(printer.model) || 'Unknown'} • {printer.ip_address}
</p>
</div>
<ChevronDown className="w-4 h-4 text-bambu-gray -rotate-90 flex-shrink-0 ml-2" />
@@ -1667,13 +1728,13 @@ function AddPrinterModal({
{discovering && (
<p className="mt-2 text-sm text-bambu-gray text-center">
Scanning network...
{isDocker ? 'Scanning subnet for Bambu printers...' : 'Scanning network...'}
</p>
)}
{hasScanned && !discovering && discovered.length === 0 && (
<p className="mt-2 text-sm text-bambu-gray text-center">
No printers found on the network.
No printers found{isDocker ? ' in the specified subnet' : ' on the network'}.
</p>
)}