├── assets ├── logo.png ├── setup-icon.png ├── add-printer.png ├── dashboard-empty.png └── dashboard-printers.png ├── backend ├── assets │ └── logo.png ├── requirements.txt ├── src │ ├── __init__.py │ ├── app.js │ ├── services │ │ ├── __init__.py │ │ ├── notificationService.py │ │ ├── systemStats.py │ │ └── networkScanner.py │ ├── routes │ │ ├── __init__.py │ │ ├── stream.py │ │ └── system.py │ ├── printer_types.py │ ├── config.py │ └── app.py ├── Dockerfile.prod └── Dockerfile ├── frontend ├── public │ ├── logo.png │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── 3d-printer.png │ ├── background.png │ ├── notification.mp3 │ ├── manifest.json │ └── index.html ├── src │ ├── index.js │ ├── components │ │ ├── printer-info │ │ │ ├── GenericInfo.jsx │ │ │ ├── PrinterInfo.jsx │ │ │ ├── BambuLabInfo.jsx │ │ │ ├── CrealityInfo.jsx │ │ │ └── OctoPrintInfo.jsx │ │ ├── NotificationStatus.jsx │ │ ├── ControlsOverlay.jsx │ │ ├── CameraOverlay.jsx │ │ ├── AddPrinterButton.jsx │ │ ├── SystemStatsButton.jsx │ │ ├── CameraView.jsx │ │ ├── PrinterList.jsx │ │ ├── RTSPStream.jsx │ │ ├── printer-setup │ │ │ └── OctoPrintSetup.jsx │ │ ├── Header.jsx │ │ ├── EmergencyStopDialog.jsx │ │ ├── NotificationButton.jsx │ │ ├── SystemStatsDialog.jsx │ │ └── CloudLoginDialog.jsx │ ├── theme.js │ ├── hooks │ │ └── useVisibilityChange.js │ ├── config.js │ ├── services │ │ ├── api.js │ │ └── notificationService.js │ ├── App.jsx │ ├── api │ │ ├── notificationApi.js │ │ └── printerApi.js │ ├── styles │ │ ├── NeonSwitch.js │ │ └── NeonButton.css │ ├── utils │ │ └── logger.js │ └── App.js ├── Dockerfile ├── Dockerfile.prod ├── nginx │ └── nginx.conf └── package.json ├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── docker.yml ├── installer └── BambuCAM.Installer │ ├── Assets │ ├── setup-icon.ico │ ├── setup-icon.png │ └── installer │ │ └── BambuCAM.Installer │ │ └── Assets │ │ ├── setup-icon.ico │ │ └── setup-icon.png │ ├── Views │ ├── FinishView.xaml.cs │ ├── WelcomeView.xaml.cs │ ├── InstallationView.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── WelcomeView.xaml │ ├── FinishView.xaml │ └── InstallationView.xaml │ ├── Models │ ├── InstallationProgress.cs │ └── InstallationStatus.cs │ ├── Services │ ├── NavigationService.cs │ ├── ShortcutService.cs │ ├── UninstallService.cs │ ├── NetworkService.cs │ ├── DockerWslService.cs │ ├── WslPortForwardService.cs │ ├── DownloadService.cs │ ├── InstallationService.cs │ └── DockerService.cs │ ├── ViewModels │ ├── WelcomeViewModel.cs │ ├── ViewModelBase.cs │ ├── Commands │ │ └── RelayCommand.cs │ ├── FinishViewModel.cs │ ├── InstallationViewModel.cs │ └── MainViewModel.cs │ ├── app.manifest │ ├── Styles │ └── InstallerStyles.xaml │ ├── App.xaml │ ├── App.xaml.cs │ └── BambuCAM.Installer.csproj ├── LICENSE ├── .gitignore ├── docker-compose.dev.yml ├── docker-compose.yml ├── nginx └── nginx.conf └── README.md /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/setup-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/assets/setup-icon.png -------------------------------------------------------------------------------- /assets/add-printer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/assets/add-printer.png -------------------------------------------------------------------------------- /backend/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/backend/assets/logo.png -------------------------------------------------------------------------------- /frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/frontend/public/logo.png -------------------------------------------------------------------------------- /assets/dashboard-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/assets/dashboard-empty.png -------------------------------------------------------------------------------- /assets/dashboard-printers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/assets/dashboard-printers.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/3d-printer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/frontend/public/3d-printer.png -------------------------------------------------------------------------------- /frontend/public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/frontend/public/background.png -------------------------------------------------------------------------------- /frontend/public/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/frontend/public/notification.mp3 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ["https://www.paypal.com/donate/?hosted_button_id=FD26FHKRWS3US"] 4 | -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Assets/setup-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/installer/BambuCAM.Installer/Assets/setup-icon.ico -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Assets/setup-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/installer/BambuCAM.Installer/Assets/setup-icon.png -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Assets/installer/BambuCAM.Installer/Assets/setup-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/installer/BambuCAM.Installer/Assets/installer/BambuCAM.Installer/Assets/setup-icon.ico -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Assets/installer/BambuCAM.Installer/Assets/setup-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BangerTech/BambuCAM/HEAD/installer/BambuCAM.Installer/Assets/installer/BambuCAM.Installer/Assets/setup-icon.png -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); 11 | 12 | if (module.hot) { 13 | module.hot.accept(); 14 | } -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Dependencies installieren 6 | COPY package*.json ./ 7 | RUN npm install 8 | RUN npm install hls.js --save 9 | 10 | # Anwendungscode kopieren 11 | COPY . . 12 | 13 | ENV NODE_ENV=development 14 | ENV CHOKIDAR_USEPOLLING=true 15 | ENV WATCHPACK_POLLING=true 16 | 17 | EXPOSE 3000 18 | 19 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==2.3.3 2 | flask-cors==4.0.0 3 | python-dotenv==1.0.0 4 | requests==2.31.0 5 | websockets==12.0 6 | Werkzeug==2.3.7 7 | click==8.1.7 8 | itsdangerous==2.1.2 9 | Jinja2==3.1.2 10 | MarkupSafe==2.1.3 11 | opencv-python-headless==4.8.1.78 12 | numpy==1.24.3 13 | paho-mqtt>=2.0.0 14 | bambulabs-api==2.5.8 15 | psutil==5.9.0 16 | python-telegram-bot==13.7 17 | pyyaml==6.0.1 18 | -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Views/FinishView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using BambuCAM.Installer.ViewModels; 3 | 4 | namespace BambuCAM.Installer.Views 5 | { 6 | public partial class FinishView : UserControl 7 | { 8 | public FinishView() 9 | { 10 | InitializeComponent(); 11 | DataContext = new FinishViewModel(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Views/WelcomeView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using BambuCAM.Installer.ViewModels; 3 | 4 | namespace BambuCAM.Installer.Views 5 | { 6 | public partial class WelcomeView : UserControl 7 | { 8 | public WelcomeView() 9 | { 10 | InitializeComponent(); 11 | DataContext = new WelcomeViewModel(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Views/InstallationView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using BambuCAM.Installer.ViewModels; 3 | 4 | namespace BambuCAM.Installer.Views 5 | { 6 | public partial class InstallationView : UserControl 7 | { 8 | public InstallationView() 9 | { 10 | InitializeComponent(); 11 | DataContext = new InstallationViewModel(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /backend/src/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cors import CORS 3 | import logging 4 | 5 | # Standard Logger Setup 6 | logger = logging.getLogger(__name__) 7 | 8 | def create_app(): 9 | app = Flask(__name__) 10 | logger.info("Starting Flask application...") 11 | 12 | # CORS konfigurieren 13 | CORS(app) 14 | 15 | from src.routes import register_blueprints 16 | register_blueprints(app) 17 | 18 | return app -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Views/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Models/InstallationProgress.cs: -------------------------------------------------------------------------------- 1 | public class InstallationProgress 2 | { 3 | public bool Success { get; } 4 | public string ErrorMessage { get; } 5 | 6 | private InstallationProgress(bool success, string errorMessage = null) 7 | { 8 | Success = success; 9 | ErrorMessage = errorMessage; 10 | } 11 | 12 | public static InstallationProgress Successful => new(true); 13 | public static InstallationProgress Failure(string message) => new(false, message); 14 | } -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Views/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using BambuCAM.Installer.Services; 4 | using BambuCAM.Installer.Views; 5 | 6 | namespace BambuCAM.Installer.Views 7 | { 8 | public partial class MainWindow : Window 9 | { 10 | public MainWindow() 11 | { 12 | InitializeComponent(); 13 | NavigationService.Initialize(MainContent); 14 | NavigationService.Navigate(new WelcomeView()); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Services/NavigationService.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace BambuCAM.Installer.Services 4 | { 5 | public static class NavigationService 6 | { 7 | private static ContentControl _mainContent; 8 | 9 | public static void Initialize(ContentControl mainContent) 10 | { 11 | _mainContent = mainContent; 12 | } 13 | 14 | public static void Navigate(UserControl view) 15 | { 16 | _mainContent.Content = view; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /frontend/src/components/printer-info/GenericInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Typography } from '@mui/material'; 3 | 4 | const GenericInfo = ({ printer }) => { 5 | return ( 6 | 7 | 8 | Model: {printer.model} 9 | 10 | 11 | Type: {printer.type} 12 | 13 | {/* Basis-Informationen für andere Drucker */} 14 | 15 | ); 16 | }; 17 | 18 | export default GenericInfo; -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Models/InstallationStatus.cs: -------------------------------------------------------------------------------- 1 | namespace BambuCAM.Installer.Models 2 | { 3 | public class InstallationStatus 4 | { 5 | public int Progress { get; set; } 6 | public string Message { get; set; } 7 | public string DetailedMessage { get; set; } 8 | 9 | public InstallationStatus(int progress, string message, string detailedMessage = null) 10 | { 11 | Progress = progress; 12 | Message = message; 13 | DetailedMessage = detailedMessage; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /frontend/src/theme.js: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material'; 2 | 3 | export const lightTheme = createTheme({ 4 | palette: { 5 | mode: 'light', 6 | background: { 7 | default: '#f5f5f7', 8 | paper: '#ffffff' 9 | }, 10 | primary: { 11 | main: '#007AFF' 12 | } 13 | } 14 | }); 15 | 16 | export const darkTheme = createTheme({ 17 | palette: { 18 | mode: 'dark', 19 | background: { 20 | default: '#1a1a1a', 21 | paper: '#2d2d2d' 22 | }, 23 | primary: { 24 | main: '#0A84FF' 25 | } 26 | } 27 | }); -------------------------------------------------------------------------------- /backend/src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const printersRouter = require('./routes/printers'); 4 | 5 | const app = express(); 6 | 7 | // Middleware 8 | app.use(cors()); 9 | app.use(express.json()); 10 | 11 | // Routes 12 | app.use('/printers', printersRouter); 13 | 14 | // Error handling 15 | app.use((err, req, res, next) => { 16 | console.error(err.stack); 17 | res.status(500).json({ 18 | success: false, 19 | error: 'Server Error', 20 | details: err.message 21 | }); 22 | }); 23 | 24 | module.exports = app; -------------------------------------------------------------------------------- /backend/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | WORKDIR /app 4 | 5 | # Installiere Build-Dependencies 6 | RUN apt-get update && apt-get install -y --no-install-recommends \ 7 | gcc \ 8 | python3-dev \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | # Kopiere und installiere Requirements 12 | COPY requirements.txt . 13 | RUN pip install --no-cache-dir -r requirements.txt 14 | 15 | # Kopiere den Rest der Anwendung 16 | COPY . . 17 | 18 | ENV FLASK_ENV=production 19 | ENV FLASK_APP=src/app.py 20 | ENV PYTHONPATH=/app 21 | 22 | EXPOSE 4000 23 | 24 | CMD ["python", "src/app.py"] -------------------------------------------------------------------------------- /frontend/src/hooks/useVisibilityChange.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export const useVisibilityChange = (onVisibilityChange) => { 4 | useEffect(() => { 5 | // Temporär deaktiviert für Tests 6 | // const handleVisibilityChange = () => { 7 | // onVisibilityChange(!document.hidden); 8 | // }; 9 | // 10 | // document.addEventListener('visibilitychange', handleVisibilityChange); 11 | // 12 | // return () => { 13 | // document.removeEventListener('visibilitychange', handleVisibilityChange); 14 | // }; 15 | }, [onVisibilityChange]); 16 | }; -------------------------------------------------------------------------------- /frontend/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as builder 2 | 3 | WORKDIR /app 4 | 5 | # Dependencies installieren 6 | COPY package*.json ./ 7 | RUN npm install 8 | RUN npm install hls.js --save 9 | 10 | # Anwendungscode kopieren und bauen 11 | COPY . . 12 | RUN npm run build 13 | 14 | # Production image 15 | FROM nginx:alpine 16 | COPY --from=builder /app/build /usr/share/nginx/html 17 | COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf 18 | 19 | # Ändern der Nginx-Konfiguration, um auf Port 3000 zu lauschen 20 | RUN sed -i 's/listen\s*80/listen 3000/g' /etc/nginx/conf.d/default.conf 21 | 22 | EXPOSE 3000 -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "BambuCAM", 3 | "name": "BambuCAM - Camera Viewer for Bambulab Printers", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } -------------------------------------------------------------------------------- /frontend/src/config.js: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV === 'development'; 2 | const hostname = window.location.hostname; 3 | const port = process.env.NODE_ENV === 'development' ? ':80' : ''; // Optional Port 4 | 5 | export const config = { 6 | // Wir nutzen die Nginx-Proxy-URL (Port 80) 7 | API_URL: `http://${hostname}/api`, 8 | WS_URL: `ws://${hostname}/stream`, 9 | API_HOST: hostname, 10 | 11 | // Weitere Konfigurationen 12 | NOTIFICATION_REFRESH_INTERVAL: 30000, // 30 Sekunden 13 | PRINTER_REFRESH_INTERVAL: 5000, // 5 Sekunden 14 | MAX_RETRIES: 3 15 | }; 16 | 17 | export const { API_URL, WS_URL, API_HOST } = config; -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import { API_URL } from '../config'; 2 | 3 | export const api = { 4 | async getPrinters() { 5 | const response = await fetch(`${API_URL}/printers`); 6 | if (!response.ok) throw new Error('Failed to fetch printers'); 7 | return response.json(); 8 | }, 9 | 10 | async getCloudPrinters(token) { 11 | const response = await fetch(`${API_URL}/cloud/printers`, { 12 | headers: { Authorization: `Bearer ${token}` } 13 | }); 14 | if (!response.ok) throw new Error('Failed to fetch cloud printers'); 15 | return response.json(); 16 | }, 17 | 18 | // ... weitere API-Methoden 19 | }; -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/ViewModels/WelcomeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | using BambuCAM.Installer.Services; 3 | using BambuCAM.Installer.Views; 4 | using BambuCAM.Installer.ViewModels.Commands; 5 | 6 | namespace BambuCAM.Installer.ViewModels 7 | { 8 | public class WelcomeViewModel : ViewModelBase 9 | { 10 | public WelcomeViewModel() 11 | { 12 | StartInstallCommand = new RelayCommand(StartInstallation); 13 | } 14 | 15 | public ICommand StartInstallCommand { get; } 16 | 17 | private void StartInstallation() 18 | { 19 | // Wechsel zur InstallationView 20 | NavigationService.Navigate(new InstallationView()); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /frontend/src/components/NotificationStatus.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import logger from '../utils/logger'; 3 | 4 | const NotificationStatus = () => { 5 | const [status, setStatus] = useState(null); 6 | 7 | useEffect(() => { 8 | const fetchStatus = async () => { 9 | try { 10 | const response = await fetch(`${API_URL}/notifications/status`); 11 | const data = await response.json(); 12 | logger.notification('Received notification status:', data); 13 | setStatus(data); 14 | } catch (error) { 15 | logger.error('Error fetching notification status:', error); 16 | } 17 | }; 18 | fetchStatus(); 19 | }, []); 20 | 21 | // ... Rest des Codes 22 | }; 23 | 24 | export default NotificationStatus; -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /backend/src/services/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .printerService import ( 4 | printer_service, 5 | getPrinters, 6 | getPrinterById, 7 | addPrinter, 8 | removePrinter, 9 | startPrint, 10 | stopPrint, 11 | scanNetwork, 12 | getPrinterStatus 13 | ) 14 | 15 | from .streamService import ( 16 | stream_service, 17 | startStream, 18 | stopStream 19 | ) 20 | 21 | from .telegramService import telegram_service 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | __all__ = [ 26 | 'getPrinters', 27 | 'addPrinter', 28 | 'removePrinter', 29 | 'getPrinterById', 30 | 'scanNetwork', 31 | 'startStream', 32 | 'stopStream', 33 | 'printer_service', 34 | 'stream_service', 35 | 'telegram_service', 36 | 'startPrint', 37 | 'stopPrint', 38 | 'getPrinterStatus' 39 | ] -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | # Installiere FFmpeg und andere Abhängigkeiten 4 | RUN apt-get update && apt-get install -y \ 5 | ffmpeg \ 6 | gcc \ 7 | python3-dev \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | # Setze Arbeitsverzeichnis 11 | WORKDIR /app 12 | 13 | # Kopiere requirements.txt 14 | COPY requirements.txt . 15 | 16 | # Installiere Python-Abhängigkeiten 17 | RUN pip install --no-cache-dir -r requirements.txt \ 18 | && pip install --upgrade pip 19 | 20 | # Kopiere den Rest der Anwendung 21 | COPY . . 22 | 23 | # Erstelle notwendige Verzeichnisse und setze Berechtigungen 24 | RUN mkdir -p /app/data/printers /app/data/streams /app/logs /app/data/go2rtc \ 25 | && chmod -R 777 /app/data /app/logs 26 | 27 | ENV FLASK_APP=src.app 28 | ENV PYTHONPATH=/app 29 | 30 | CMD ["flask", "run", "--host=0.0.0.0", "--port=4000"] -------------------------------------------------------------------------------- /frontend/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | try_files $uri $uri/ /index.html; 8 | index index.html; 9 | } 10 | 11 | location /api { 12 | proxy_pass http://localhost:4000; 13 | proxy_http_version 1.1; 14 | proxy_set_header Upgrade $http_upgrade; 15 | proxy_set_header Connection 'upgrade'; 16 | proxy_set_header Host $host; 17 | proxy_cache_bypass $http_upgrade; 18 | } 19 | 20 | location /go2rtc { 21 | proxy_pass http://localhost:1984; 22 | proxy_http_version 1.1; 23 | proxy_set_header Upgrade $http_upgrade; 24 | proxy_set_header Connection 'upgrade'; 25 | proxy_set_header Host $host; 26 | proxy_cache_bypass $http_upgrade; 27 | } 28 | } -------------------------------------------------------------------------------- /frontend/src/components/ControlsOverlay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from '@mui/material'; 3 | import styled from '@emotion/styled'; 4 | 5 | const StyledControls = styled(Box)` 6 | position: absolute; 7 | bottom: 0; 8 | left: 0; 9 | right: 0; 10 | min-height: 64px; 11 | padding: 16px; 12 | background: linear-gradient( 13 | to top, 14 | rgba(0,0,0,0.85) 0%, 15 | rgba(0,0,0,0.6) 60%, 16 | rgba(0,0,0,0) 100% 17 | ); 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | z-index: 2; 22 | pointer-events: auto; 23 | transition: opacity 0.2s ease; 24 | 25 | &:hover { 26 | opacity: 1; 27 | } 28 | `; 29 | 30 | const ControlsOverlay = ({ children, ...props }) => { 31 | return {children}; 32 | }; 33 | 34 | export default ControlsOverlay; -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Styles/InstallerStyles.xaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace BambuCAM.Installer.ViewModels 5 | { 6 | public abstract class ViewModelBase : INotifyPropertyChanged 7 | { 8 | public event PropertyChangedEventHandler PropertyChanged; 9 | 10 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 11 | { 12 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 13 | } 14 | 15 | protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null) 16 | { 17 | if (Equals(field, value)) return false; 18 | field = value; 19 | OnPropertyChanged(propertyName); 20 | return true; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /frontend/src/components/CameraOverlay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from '@mui/material'; 3 | import styled from '@emotion/styled'; 4 | 5 | const StyledOverlay = styled(Box)` 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | min-height: 64px; 11 | padding: 16px; 12 | background: linear-gradient( 13 | to bottom, 14 | rgba(0,0,0,0.85) 0%, 15 | rgba(0,0,0,0.6) 60%, 16 | rgba(0,0,0,0) 100% 17 | ); 18 | color: white; 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | z-index: 2; 23 | pointer-events: auto; 24 | transition: opacity 0.2s ease; 25 | 26 | &:hover { 27 | opacity: 1; 28 | } 29 | `; 30 | 31 | const CameraOverlay = ({ children, ...props }) => { 32 | return {children}; 33 | }; 34 | 35 | export default CameraOverlay; -------------------------------------------------------------------------------- /backend/src/routes/__init__.py: -------------------------------------------------------------------------------- 1 | # Leere Datei zur Markierung als Python-Paket 2 | 3 | from .printers import printers_bp 4 | from .system import system_bp 5 | from .notifications import notifications_bp 6 | from .stream import stream_bp 7 | from .cloud import cloud_bp 8 | 9 | def register_blueprints(app): 10 | """Registriert alle Blueprints""" 11 | from .cloud import cloud_bp 12 | #from .system import system_bp 13 | from src.routes.system import system_bp 14 | from .notifications import notifications_bp 15 | from .stream import stream_bp 16 | from .printers import printers_bp 17 | 18 | app.register_blueprint(cloud_bp, url_prefix='') 19 | #app.register_blueprint(system_bp) 20 | app.register_blueprint(system_bp, url_prefix='/api/system') 21 | app.register_blueprint(notifications_bp) 22 | app.register_blueprint(stream_bp) 23 | app.register_blueprint(printers_bp) 24 | -------------------------------------------------------------------------------- /frontend/src/components/AddPrinterButton.jsx: -------------------------------------------------------------------------------- 1 | import { Fab, useTheme, useMediaQuery } from '@mui/material'; 2 | import AddIcon from '@mui/icons-material/Add'; 3 | 4 | const AddPrinterButton = () => { 5 | const theme = useTheme(); 6 | const isMobile = useMediaQuery(theme.breakpoints.down('sm')); 7 | 8 | return ( 9 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default AddPrinterButton; -------------------------------------------------------------------------------- /frontend/src/components/SystemStatsButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Fab, Tooltip } from '@mui/material'; 3 | import MemoryIcon from '@mui/icons-material/Memory'; 4 | 5 | const SystemStatsButton = ({ onClick }) => { 6 | return ( 7 | 8 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default SystemStatsButton; -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Services/ShortcutService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace BambuCAM.Installer.Services 6 | { 7 | public class ShortcutService 8 | { 9 | public void CreateDesktopShortcut(string targetUrl, string shortcutName) 10 | { 11 | var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); 12 | var shortcutPath = Path.Combine(desktopPath, $"{shortcutName}.url"); 13 | 14 | using (StreamWriter writer = new StreamWriter(shortcutPath)) 15 | { 16 | writer.WriteLine("[InternetShortcut]"); 17 | writer.WriteLine($"URL={targetUrl}"); 18 | writer.WriteLine("IconIndex=0"); 19 | writer.WriteLine($"IconFile={Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "setup-icon.ico")}"); 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/ViewModels/Commands/RelayCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Input; 3 | 4 | namespace BambuCAM.Installer.ViewModels.Commands 5 | { 6 | public class RelayCommand : ICommand 7 | { 8 | private readonly Action _execute; 9 | private readonly Func _canExecute; 10 | 11 | public RelayCommand(Action execute, Func canExecute = null) 12 | { 13 | _execute = execute ?? throw new ArgumentNullException(nameof(execute)); 14 | _canExecute = canExecute; 15 | } 16 | 17 | public event EventHandler CanExecuteChanged 18 | { 19 | add { CommandManager.RequerySuggested += value; } 20 | remove { CommandManager.RequerySuggested -= value; } 21 | } 22 | 23 | public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true; 24 | public void Execute(object parameter) => _execute(); 25 | } 26 | } -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Box, useMediaQuery, useTheme } from '@mui/material'; 2 | // ... andere imports ... 3 | 4 | const App = () => { 5 | const theme = useTheme(); 6 | const isMobile = useMediaQuery(theme.breakpoints.down('sm')); 7 | 8 | return ( 9 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bambulab-camera-viewer-frontend", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@emotion/react": "^11.7.1", 6 | "@emotion/styled": "^11.6.0", 7 | "@mui/icons-material": "^5.2.8", 8 | "@mui/material": "^5.2.8", 9 | "axios": "^1.6.2", 10 | "react-beautiful-dnd": "^13.1.1", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2" 13 | }, 14 | "scripts": { 15 | "start": "DANGEROUSLY_DISABLE_HOST_CHECK=true HOST=0.0.0.0 WATCHPACK_POLLING=true react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "devDependencies": { 21 | "react-scripts": "5.0.1" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | }, 35 | "proxy": "http://localhost:4000/" 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/api/notificationApi.js: -------------------------------------------------------------------------------- 1 | import logger from '../utils/logger'; 2 | import { API_URL } from '../config'; 3 | 4 | export const notificationApi = { 5 | getStatus: async () => { 6 | try { 7 | const response = await fetch(`${API_URL}/notifications/status`); 8 | const data = await response.json(); 9 | logger.notification('Notification status:', data); 10 | return data; 11 | } catch (error) { 12 | logger.error('Error fetching notification status:', error); 13 | throw error; 14 | } 15 | }, 16 | 17 | updateSettings: async (settings) => { 18 | try { 19 | const response = await fetch(`${API_URL}/notifications/settings`, { 20 | method: 'POST', 21 | headers: { 'Content-Type': 'application/json' }, 22 | body: JSON.stringify(settings) 23 | }); 24 | const data = await response.json(); 25 | logger.notification('Updated notification settings:', data); 26 | return data; 27 | } catch (error) { 28 | logger.error('Error updating notification settings:', error); 29 | throw error; 30 | } 31 | } 32 | }; -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/ViewModels/FinishViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Windows.Input; 3 | using System.Runtime.InteropServices; 4 | using BambuCAM.Installer.ViewModels.Commands; 5 | 6 | namespace BambuCAM.Installer.ViewModels 7 | { 8 | public class FinishViewModel : ViewModelBase 9 | { 10 | public FinishViewModel() 11 | { 12 | LaunchCommand = new RelayCommand(LaunchApplication); 13 | CloseCommand = new RelayCommand(CloseInstaller); 14 | } 15 | 16 | public ICommand LaunchCommand { get; } 17 | public ICommand CloseCommand { get; } 18 | 19 | private void LaunchApplication() 20 | { 21 | var url = "http://localhost"; 22 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 23 | { 24 | Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); 25 | } 26 | CloseInstaller(); 27 | } 28 | 29 | private void CloseInstaller() 30 | { 31 | App.Current.Shutdown(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /backend/src/printer_types.py: -------------------------------------------------------------------------------- 1 | PRINTER_CONFIGS = { 2 | 'BAMBULAB': { 3 | 'stream_type': 'rtsp', 4 | 'stream_url_template': 'rtsps://bblp:{access_code}@{ip}:322/streaming/live/1', 5 | 'ffmpeg_options': [ 6 | '-rtsp_transport', 'tcp', 7 | '-fflags', '+genpts+igndts', 8 | '-max_muxing_queue_size', '1024', 9 | '-tune', 'zerolatency', 10 | '-preset', 'ultrafast', 11 | '-reconnect', '1', 12 | '-reconnect_at_eof', '1', 13 | '-reconnect_streamed', '1' 14 | ] 15 | }, 16 | 'CREALITY': { 17 | 'stream_type': 'mjpeg', 18 | 'stream_url_template': 'http://{ip}:8080/?action=stream', 19 | 'ffmpeg_options': [ 20 | '-f', 'mjpeg', 21 | '-reconnect', '1', 22 | '-reconnect_at_eof', '1' 23 | ] 24 | }, 25 | 'CUSTOM': { 26 | 'stream_type': 'auto', 27 | 'stream_url_template': '{stream_url}', # Direkt die eingegebene URL verwenden 28 | 'ffmpeg_options': [] # Basis-Optionen, werden je nach URL angepasst 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 BangerTech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | BambuCAM 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | env/ 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | *.log 23 | 24 | # Node 25 | node_modules/ 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .env 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # IDE 36 | .idea/ 37 | .vscode/ 38 | *.swp 39 | *.swo 40 | 41 | # OS 42 | .DS_Store 43 | Thumbs.db 44 | 45 | # Project specific 46 | /backend/logs/* 47 | !/backend/logs/.gitkeep 48 | /frontend/build 49 | /frontend/dist 50 | 51 | # Sensitive data 52 | backend/config/bambu_cloud.json 53 | backend/config/*.json 54 | 55 | # Environment files 56 | .env 57 | .env.local 58 | .env.*.local 59 | 60 | # Log files 61 | backend/logs/ 62 | *.log 63 | 64 | # Data directories 65 | backend/data/ 66 | data/ 67 | 68 | # IDE files 69 | .idea/ 70 | .vscode/ 71 | *.swp 72 | *.swo 73 | 74 | # Dependencies 75 | __pycache__/ 76 | *.pyc 77 | 78 | # Build files 79 | frontend/build/ 80 | dist/ 81 | -------------------------------------------------------------------------------- /frontend/src/components/CameraView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paper } from '@mui/material'; 3 | import styled from '@emotion/styled'; 4 | 5 | const Container = styled.div` 6 | width: 100%; 7 | margin: 0; 8 | padding: 0; 9 | `; 10 | 11 | const StyledCameraView = styled(Paper)` 12 | position: relative; 13 | width: 100%; 14 | padding-top: 56.25%; // 16:9 Aspect Ratio 15 | background: #000; 16 | border-radius: 15px; 17 | overflow: hidden; 18 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 19 | cursor: pointer; 20 | 21 | &:hover { 22 | transform: scale(1.02); 23 | box-shadow: 0 8px 16px rgba(0,0,0,0.2); 24 | } 25 | `; 26 | 27 | const ContentContainer = styled.div` 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | right: 0; 32 | bottom: 0; 33 | display: flex; 34 | flex-direction: column; 35 | `; 36 | 37 | const CameraView = ({ children, ...props }) => { 38 | return ( 39 | 40 | 41 | 42 | {children} 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default CameraView; -------------------------------------------------------------------------------- /backend/src/services/notificationService.py: -------------------------------------------------------------------------------- 1 | from src.services.telegramService import telegram_service 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | def send_printer_notification(printer_name, status, message=None): 7 | """Sendet eine Drucker-spezifische Benachrichtigung""" 8 | try: 9 | # Status-spezifische Emojis 10 | status_emojis = { 11 | 'completed': '✅', 12 | 'failed': '❌', 13 | 'error': '⚠️', 14 | 'started': '🚀', 15 | 'paused': '⏸️' 16 | } 17 | 18 | emoji = status_emojis.get(status.lower(), '🖨️') 19 | 20 | # Basis-Nachricht 21 | notification = f"{emoji} *{printer_name}*: " 22 | 23 | # Status-spezifische Nachricht 24 | if message: 25 | notification += message 26 | else: 27 | notification += f"Status: {status}" 28 | 29 | # Sende über alle konfigurierten Kanäle 30 | if telegram_service.is_configured(): 31 | telegram_service.send_notification(notification) 32 | 33 | except Exception as e: 34 | logger.error(f"Error sending printer notification: {e}") -------------------------------------------------------------------------------- /frontend/src/components/printer-info/PrinterInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BambuLabInfo from './BambuLabInfo'; 3 | import CrealityInfo from './CrealityInfo'; 4 | import logger from '../../utils/logger'; 5 | 6 | const PrinterInfo = ({ printer, status, onEmergencyStop }) => { 7 | // Daten-Mapping für unterschiedliche Druckertypen 8 | const mappedStatus = printer?.type === 'CREALITY' ? { 9 | ...status, 10 | temperatures: status?.temps, // Für Creality: temps -> temperatures 11 | } : status; 12 | 13 | // Erweitertes Debug-Logging 14 | logger.debug('PrinterInfo render:', { 15 | printer_type: printer?.type, 16 | printer_id: printer?.id, 17 | status: mappedStatus, 18 | temperatures: mappedStatus?.temperatures, 19 | targets: mappedStatus?.targets, 20 | progress: mappedStatus?.progress 21 | }); 22 | 23 | // Wähle die richtige Info-Komponente basierend auf dem Druckertyp 24 | switch (printer?.type) { 25 | case 'BAMBULAB': 26 | return ; 27 | case 'CREALITY': 28 | return ; 29 | default: 30 | logger.warn(`Unknown printer type: ${printer?.type}`); 31 | return null; 32 | } 33 | }; 34 | 35 | export default PrinterInfo; -------------------------------------------------------------------------------- /frontend/src/components/PrinterList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from 'react'; 2 | import { Box } from '@mui/material'; 3 | import PrinterCard from './PrinterCard'; 4 | 5 | const PrinterList = () => { 6 | const [printers, setPrinters] = useState([]); 7 | 8 | const fetchPrinters = useCallback(async () => { 9 | try { 10 | // Hole LAN Drucker 11 | const lanResponse = await fetch(`${API_URL}/printers`); 12 | const lanPrinters = await lanResponse.json(); 13 | 14 | // Hole Cloud Drucker 15 | const cloudResponse = await fetch(`${API_URL}/api/cloud/printers`); 16 | const cloudPrinters = await cloudResponse.json(); 17 | 18 | // Kombiniere beide Listen 19 | setPrinters([...lanPrinters, ...cloudPrinters]); 20 | } catch (error) { 21 | console.error('Error fetching printers:', error); 22 | } 23 | }, []); 24 | 25 | useEffect(() => { 26 | fetchPrinters(); 27 | const interval = setInterval(fetchPrinters, 5000); 28 | return () => clearInterval(interval); 29 | }, [fetchPrinters]); 30 | 31 | return ( 32 | 33 | {printers.map(printer => ( 34 | 39 | ))} 40 | 41 | ); 42 | }; 43 | 44 | export default PrinterList; -------------------------------------------------------------------------------- /backend/src/services/systemStats.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | def bytes_to_gb(bytes_value): 7 | """Konvertiert Bytes in GB""" 8 | return bytes_value / (1024 * 1024 * 1024) 9 | 10 | def get_system_stats(): 11 | """Sammelt System-Statistiken""" 12 | try: 13 | # CPU Info 14 | cpu_percent = psutil.cpu_percent(interval=1) 15 | cpu_cores = psutil.cpu_count() 16 | 17 | # Memory Info 18 | memory = psutil.virtual_memory() 19 | memory_total = bytes_to_gb(memory.total) 20 | memory_used = bytes_to_gb(memory.used) 21 | 22 | # Disk Info 23 | disk = psutil.disk_usage('/') 24 | disk_total = bytes_to_gb(disk.total) 25 | disk_used = bytes_to_gb(disk.used) 26 | 27 | return { 28 | 'cpu': { 29 | 'percent': cpu_percent, 30 | 'cores': cpu_cores 31 | }, 32 | 'memory': { 33 | 'total': memory_total, 34 | 'used': memory_used, 35 | 'percent': memory.percent 36 | }, 37 | 'disk': { 38 | 'total': disk_total, 39 | 'used': disk_used, 40 | 'percent': disk.percent 41 | } 42 | } 43 | 44 | except Exception as e: 45 | logger.error(f"Error getting system stats: {e}") 46 | return None -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | #2196F3 16 | #00BCD4 17 | 18 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Security.Principal; 4 | using System.Windows; 5 | 6 | public partial class App : Application 7 | { 8 | protected override void OnStartup(StartupEventArgs e) 9 | { 10 | base.OnStartup(e); 11 | 12 | // Prüfe Admin-Rechte 13 | if (!IsRunAsAdministrator()) 14 | { 15 | RestartAsAdmin(); 16 | Shutdown(); 17 | return; 18 | } 19 | } 20 | 21 | private bool IsRunAsAdministrator() 22 | { 23 | var identity = WindowsIdentity.GetCurrent(); 24 | var principal = new WindowsPrincipal(identity); 25 | return principal.IsInRole(WindowsBuiltInRole.Administrator); 26 | } 27 | 28 | private void RestartAsAdmin() 29 | { 30 | var startInfo = new ProcessStartInfo 31 | { 32 | UseShellExecute = true, 33 | FileName = Process.GetCurrentProcess().MainModule.FileName, 34 | Verb = "runas" 35 | }; 36 | 37 | try 38 | { 39 | Process.Start(startInfo); 40 | } 41 | catch 42 | { 43 | MessageBox.Show("BambuCAM Setup requires administrator privileges.", 44 | "Administrator Rights Required", 45 | MessageBoxButton.OK, 46 | MessageBoxImage.Warning); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Views/WelcomeView.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 18 | 23 | 102 | 103 | 104 | ); 105 | }; 106 | 107 | export default Header; -------------------------------------------------------------------------------- /backend/src/routes/stream.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request, Response 2 | from src.services.streamService import stream_service 3 | from src.services.printerService import getPrinterById 4 | import logging 5 | import requests 6 | 7 | # Einfacher Logger statt des spezialisierten Loggers 8 | logger = logging.getLogger(__name__) 9 | 10 | stream_bp = Blueprint('stream', __name__) 11 | 12 | @stream_bp.route('//reset', methods=['POST']) 13 | def reset_stream(printer_id): 14 | try: 15 | logger.info(f"\n=== Stream reset requested for printer {printer_id} ===") 16 | 17 | if printer_id not in stream_service.active_streams: 18 | logger.error(f"Printer {printer_id} not found in active streams") 19 | return jsonify({ 20 | 'success': False, 21 | 'error': 'Printer not found' 22 | }), 404 23 | 24 | logger.info("Calling stream service restart_stream...") 25 | result = stream_service.restart_stream(printer_id) 26 | logger.info(f"Stream service result: {result}") 27 | 28 | if isinstance(result, dict) and 'new_port' in result: 29 | logger.info(f"Sending new port {result['new_port']} to frontend") 30 | return jsonify(result) 31 | 32 | logger.error("Stream reset failed - no valid result") 33 | return jsonify({ 34 | 'success': False, 35 | 'error': 'Reset failed' 36 | }), 400 37 | 38 | except Exception as e: 39 | logger.error(f"Error during stream reset: {e}", exc_info=True) 40 | return jsonify({ 41 | 'success': False, 42 | 'error': str(e) 43 | }), 500 44 | 45 | @stream_bp.route('/mjpeg/') 46 | def proxy_mjpeg_stream(printer_id): 47 | try: 48 | printer = getPrinterById(printer_id) 49 | if not printer: 50 | logger.error(f"Printer {printer_id} not found") 51 | return jsonify({'error': 'Printer not found'}), 404 52 | 53 | stream_url = f"http://{printer['ip']}:8080/?action=stream" 54 | logger.info(f"Proxying stream from: {stream_url}") 55 | 56 | def generate(): 57 | try: 58 | response = requests.get(stream_url, stream=True, timeout=5) 59 | if response.ok: 60 | headers = response.headers 61 | logger.debug(f"Original headers: {headers}") 62 | for chunk in response.iter_content(chunk_size=8192): 63 | yield chunk 64 | except Exception as e: 65 | logger.error(f"Error proxying MJPEG stream: {e}") 66 | 67 | return Response( 68 | generate(), 69 | mimetype='multipart/x-mixed-replace; boundary=boundarydonotcross', 70 | direct_passthrough=True, # Wichtig für Streaming 71 | headers={ 72 | 'Cache-Control': 'no-cache, no-store, must-revalidate', 73 | 'Pragma': 'no-cache', 74 | 'Expires': '0', 75 | 'Connection': 'close', 76 | } 77 | ) 78 | except Exception as e: 79 | logger.error(f"Error setting up MJPEG proxy: {e}", exc_info=True) 80 | return jsonify({'error': str(e)}), 500 81 | 82 | @stream_bp.route('/stream/') 83 | def start_stream(printer_id): 84 | url = request.args.get('url') 85 | if not url: 86 | return jsonify({'success': False, 'error': 'No URL provided'}), 400 87 | 88 | logger.info(f"Starting stream for printer {printer_id} with URL: {url}") 89 | result = stream_service.start_stream(printer_id, url) 90 | 91 | if result.get('success'): 92 | return jsonify(result) 93 | else: 94 | return jsonify(result), 500 95 | 96 | @stream_bp.route('//stop', methods=['POST']) 97 | def stop_stream(printer_id): 98 | """Stoppt einen laufenden Stream""" 99 | try: 100 | stream_service.stop_stream(printer_id) 101 | return jsonify({'success': True}) 102 | except Exception as e: 103 | return jsonify({'error': str(e)}), 500 -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include /etc/nginx/mime.types; 9 | default_type application/octet-stream; 10 | 11 | # Vereinfachtes Log-Format ohne User-Agent und Referer 12 | log_format simple '$remote_addr - - [$time_local] "$request" $status $body_bytes_sent'; 13 | access_log /var/log/nginx/access.log simple; 14 | 15 | server { 16 | listen 80; 17 | server_name _; 18 | 19 | # Frontend 20 | location / { 21 | proxy_pass http://127.0.0.1:3000; 22 | proxy_http_version 1.1; 23 | proxy_set_header Upgrade $http_upgrade; 24 | proxy_set_header Connection 'upgrade'; 25 | proxy_set_header Host $host; 26 | proxy_cache_bypass $http_upgrade; 27 | } 28 | 29 | # API Endpoints 30 | location /api/ { 31 | proxy_pass http://127.0.0.1:4000/api/; 32 | proxy_http_version 1.1; 33 | proxy_set_header Upgrade $http_upgrade; 34 | proxy_set_header Connection 'upgrade'; 35 | proxy_set_header Host $host; 36 | proxy_cache_bypass $http_upgrade; 37 | } 38 | 39 | # Optimierter Stream Endpoint 40 | location /stream/ { 41 | proxy_pass http://127.0.0.1:9000/; 42 | proxy_http_version 1.1; 43 | proxy_set_header Upgrade $http_upgrade; 44 | proxy_set_header Connection "upgrade"; 45 | 46 | # Verbesserte Buffer-Einstellungen 47 | proxy_buffering off; 48 | proxy_buffer_size 4k; 49 | proxy_read_timeout 3600s; 50 | proxy_send_timeout 3600s; 51 | 52 | # Erhöhte Timeouts für stabilere Verbindung 53 | keepalive_timeout 65; 54 | keepalive_requests 100; 55 | 56 | # Chunk-Einstellungen 57 | chunked_transfer_encoding on; 58 | proxy_request_buffering off; 59 | 60 | # WebSocket-spezifische Einstellungen 61 | proxy_set_header X-Real-IP $remote_addr; 62 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 63 | 64 | # Zusätzliche Optimierungen 65 | proxy_connect_timeout 60s; 66 | tcp_nodelay on; 67 | 68 | # CORS Headers 69 | add_header 'Access-Control-Allow-Origin' '*' always; 70 | add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; 71 | add_header 'Access-Control-Allow-Headers' '*' always; 72 | } 73 | 74 | # MJPEG Stream Endpoint 75 | location /stream/mjpeg/ { 76 | proxy_pass http://0.0.0.0:4000/stream/mjpeg/; 77 | proxy_http_version 1.1; 78 | proxy_set_header Upgrade $http_upgrade; 79 | proxy_set_header Connection "upgrade"; 80 | proxy_set_header Host $host; 81 | 82 | # CORS Headers 83 | add_header 'Access-Control-Allow-Origin' '*' always; 84 | add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; 85 | add_header 'Access-Control-Allow-Headers' '*' always; 86 | 87 | # Streaming-spezifische Headers 88 | proxy_buffering off; 89 | proxy_cache off; 90 | chunked_transfer_encoding off; 91 | 92 | # Debug-Logging 93 | error_log /var/log/nginx/stream_error.log debug; 94 | access_log /var/log/nginx/stream_access.log; 95 | } 96 | 97 | # go2rtc Web UI Root 98 | location = /go2rtc { 99 | return 301 $scheme://$host$uri/; 100 | } 101 | 102 | # go2rtc Web UI und API 103 | location /go2rtc/ { 104 | proxy_pass http://127.0.0.1:1984/; 105 | proxy_http_version 1.1; 106 | proxy_set_header Upgrade $http_upgrade; 107 | proxy_set_header Connection "upgrade"; 108 | proxy_set_header Host $host; 109 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 110 | proxy_set_header X-Real-IP $remote_addr; 111 | proxy_set_header X-Forwarded-Proto $scheme; 112 | proxy_buffering off; 113 | 114 | # Für MSE wichtig 115 | proxy_cache off; 116 | proxy_set_header Range $http_range; 117 | proxy_set_header If-Range $http_if_range; 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /frontend/src/components/EmergencyStopDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | Button, 8 | Typography, 9 | Box 10 | } from '@mui/material'; 11 | import { styled } from '@mui/material/styles'; 12 | import WarningIcon from '@mui/icons-material/Warning'; 13 | 14 | // Styled Components 15 | const NeonDialog = styled(Dialog)(({ theme }) => ({ 16 | '& .MuiPaper-root': { 17 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.95)', 18 | border: theme.palette.mode === 'dark' 19 | ? '1px solid #ff0000' 20 | : '1px solid #ff0000', 21 | boxShadow: theme.palette.mode === 'dark' 22 | ? '0 0 10px #ff0000' 23 | : '0 0 10px rgba(255, 0, 0, 0.3)', 24 | color: theme.palette.mode === 'dark' ? '#fff' : '#333' 25 | } 26 | })); 27 | 28 | const WarningText = styled(Typography)(({ theme }) => ({ 29 | color: '#ff0000', 30 | textAlign: 'center', 31 | marginBottom: '20px', 32 | fontWeight: 'bold', 33 | textShadow: theme.palette.mode === 'dark' ? '0 0 5px rgba(255, 0, 0, 0.7)' : 'none', 34 | })); 35 | 36 | const NeonButton = styled(Button)(({ theme, buttonType }) => ({ 37 | color: buttonType === 'warning' 38 | ? '#ff0000' 39 | : theme.palette.mode === 'dark' ? '#00ffff' : '#008080', 40 | borderColor: buttonType === 'warning' 41 | ? '#ff0000' 42 | : theme.palette.mode === 'dark' ? '#00ffff' : '#008080', 43 | '&:hover': { 44 | backgroundColor: buttonType === 'warning' 45 | ? 'rgba(255, 0, 0, 0.1)' 46 | : theme.palette.mode === 'dark' ? 'rgba(0, 255, 255, 0.1)' : 'rgba(0, 128, 128, 0.1)', 47 | borderColor: buttonType === 'warning' 48 | ? '#ff0000' 49 | : theme.palette.mode === 'dark' ? '#00ffff' : '#008080', 50 | } 51 | })); 52 | 53 | const WarningButton = styled(Button)(({ theme }) => ({ 54 | color: '#ffffff', 55 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 0, 0, 0.7)' : 'rgba(255, 0, 0, 0.6)', 56 | '&:hover': { 57 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 0, 0, 0.85)' : 'rgba(255, 0, 0, 0.75)', 58 | }, 59 | fontSize: '0.85rem', 60 | padding: '4px 12px' 61 | })); 62 | 63 | const CancelButton = styled(Button)(({ theme }) => ({ 64 | color: theme.palette.mode === 'dark' ? '#ffffff' : '#333333', 65 | borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#333333', 66 | '&:hover': { 67 | backgroundColor: theme.palette.mode === 'dark' 68 | ? 'rgba(255, 255, 255, 0.1)' 69 | : 'rgba(0, 0, 0, 0.05)', 70 | borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#333333', 71 | }, 72 | fontSize: '0.85rem', 73 | padding: '4px 12px' 74 | })); 75 | 76 | const EmergencyStopDialog = ({ open, onClose, onConfirm, printerName }) => { 77 | return ( 78 | 89 | theme.palette.mode === 'dark' ? '#ff0000' : '#ff0000', 91 | textAlign: 'center', 92 | fontSize: '1.5rem', 93 | fontWeight: 'bold', 94 | textShadow: theme => theme.palette.mode === 'dark' ? '0 0 10px #ff0000' : 'none', 95 | borderBottom: '1px solid rgba(255,0,0,0.3)', 96 | pb: 2 97 | }}> 98 | 99 | EMERGENCY STOP 100 | 101 | 102 | 103 | 104 | 105 | Are you sure you want to execute an emergency stop for printer "{printerName}"? 106 | 107 | 108 | This will immediately halt all printer operations and may require a restart of the printer. 109 | 110 | 111 | 112 | 113 | 114 | 115 | Cancel 116 | 117 | 118 | Execute Emergency Stop 119 | 120 | 121 | 122 | ); 123 | }; 124 | 125 | export default EmergencyStopDialog; -------------------------------------------------------------------------------- /backend/src/routes/system.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | import os 3 | import time 4 | import platform 5 | from flask import Blueprint, jsonify 6 | from flask_cors import cross_origin 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | system_bp = Blueprint('system', __name__, url_prefix='') 11 | 12 | # Cache für System-Informationen 13 | system_info_cache = { 14 | 'last_update': 0, 15 | 'data': None 16 | } 17 | CACHE_DURATION = 2 # Cache für 2 Sekunden 18 | 19 | def get_cpu_temp(): 20 | """Liest CPU Temperatur systemspezifisch""" 21 | try: 22 | if platform.system() == "Linux": 23 | # Raspberry Pi 24 | if os.path.exists('/sys/class/thermal/thermal_zone0/temp'): 25 | with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f: 26 | return float(f.read()) / 1000.0 27 | # Andere Linux Systeme 28 | if os.path.exists('/sys/class/hwmon'): 29 | for hwmon in os.listdir('/sys/class/hwmon'): 30 | name_path = f'/sys/class/hwmon/{hwmon}/name' 31 | if os.path.exists(name_path): 32 | with open(name_path, 'r') as f: 33 | if 'coretemp' in f.read(): 34 | temp_path = f'/sys/class/hwmon/{hwmon}/temp1_input' 35 | if os.path.exists(temp_path): 36 | with open(temp_path, 'r') as f: 37 | return float(f.read()) / 1000.0 38 | except Exception as e: 39 | logger.debug(f"Could not read CPU temperature: {e}") 40 | return 0 41 | 42 | def get_memory_info(): 43 | """Holt Speicherinformationen""" 44 | mem = psutil.virtual_memory() 45 | return { 46 | 'total': mem.total, 47 | 'used': mem.used, 48 | 'percent': mem.percent, 49 | 'available': mem.available 50 | } 51 | 52 | def get_disk_info(): 53 | """Holt Festplatteninformationen""" 54 | disk = psutil.disk_usage('/') 55 | return { 56 | 'total': disk.total, 57 | 'used': disk.used, 58 | 'percent': disk.percent, 59 | 'free': disk.free 60 | } 61 | 62 | def get_load_average(): 63 | """Holt Load Average (nur Linux/Unix)""" 64 | try: 65 | if platform.system() != "Windows": 66 | load1, load5, load15 = psutil.getloadavg() 67 | cpu_count = psutil.cpu_count() 68 | return [ 69 | (load1 / cpu_count) * 100, 70 | (load5 / cpu_count) * 100, 71 | (load15 / cpu_count) * 100 72 | ] 73 | except: 74 | pass 75 | return [0, 0, 0] 76 | 77 | @system_bp.route('/stats', methods=['GET']) 78 | @cross_origin() 79 | def get_system_stats(): 80 | try: 81 | cpu_percent = psutil.cpu_percent(interval=1) 82 | memory = psutil.virtual_memory() 83 | disk = psutil.disk_usage('/') 84 | 85 | return jsonify({ 86 | 'cpu': { 87 | 'percent': cpu_percent, 88 | 'cores': psutil.cpu_count() 89 | }, 90 | 'memory': { 91 | 'total': memory.total, 92 | 'used': memory.used, 93 | 'percent': memory.percent 94 | }, 95 | 'disk': { 96 | 'total': disk.total, 97 | 'used': disk.used, 98 | 'percent': disk.percent 99 | } 100 | }) 101 | except Exception as e: 102 | logger.error(f"Error getting system stats: {e}") 103 | return jsonify({'error': str(e)}), 500 104 | 105 | @system_bp.route('/system/shutdown', methods=['POST']) 106 | @cross_origin() 107 | def shutdown(): 108 | """System herunterfahren (nur Linux)""" 109 | if platform.system() == "Linux": 110 | try: 111 | os.system('sudo shutdown -h now') 112 | return jsonify({'status': 'ok'}) 113 | except Exception as e: 114 | logger.error(f"Error during shutdown: {e}") 115 | return jsonify({'error': str(e)}), 500 116 | return jsonify({'error': 'Shutdown only supported on Linux'}), 400 117 | 118 | @system_bp.route('/system/reboot', methods=['POST']) 119 | @cross_origin() 120 | def reboot(): 121 | """System neustarten (nur Linux)""" 122 | if platform.system() == "Linux": 123 | try: 124 | os.system('sudo reboot') 125 | return jsonify({'status': 'ok'}) 126 | except Exception as e: 127 | logger.error(f"Error during reboot: {e}") 128 | return jsonify({'error': str(e)}), 500 129 | return jsonify({'error': 'Reboot only supported on Linux'}), 400 -------------------------------------------------------------------------------- /frontend/src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const LOG_LEVELS = { 2 | DEBUG: 'DEBUG', 3 | INFO: 'INFO', 4 | WARN: 'WARN', 5 | ERROR: 'ERROR' 6 | }; 7 | 8 | const LOG_CATEGORIES = { 9 | PRINTER: 'PRINTER', 10 | STREAM: 'STREAM', 11 | NETWORK: 'NETWORK', 12 | API: 'API', 13 | SYSTEM: 'SYSTEM', 14 | NOTIFICATION: 'NOTIFICATION' 15 | }; 16 | 17 | class Logger { 18 | static lastLogs = new Map(); // Speichert die letzten Logs pro Kategorie/Message 19 | 20 | static formatLogKey(level, category, message, data) { 21 | // Erstellt einen eindeutigen Schlüssel für den Log 22 | return `${level}-${category}-${message}-${JSON.stringify(data)}`; 23 | } 24 | 25 | static formatPrinterStatus(printer, status) { 26 | const name = printer?.name || 'Unknown Printer'; 27 | const temps = []; 28 | 29 | // Vereinheitlichte Temperatur-Bezeichnungen 30 | if (status.temps) { 31 | if (status.temps.bed !== undefined) { 32 | temps.push(`Bed: ${status.temps.bed.toFixed(1)}°C`); 33 | } 34 | if (status.temps.nozzle !== undefined || status.temps.hotend !== undefined) { 35 | const temp = status.temps.nozzle || status.temps.hotend; 36 | temps.push(`Tool: ${temp.toFixed(1)}°C`); 37 | } 38 | if (status.temps.chamber !== undefined) { 39 | temps.push(`Chamber: ${status.temps.chamber.toFixed(1)}°C`); 40 | } 41 | } 42 | 43 | return { 44 | message: `Printer "${name}": ${status.status} | Progress: ${status.progress}% | Temps - ${temps.join(', ')}`, 45 | data: { printer, status } 46 | }; 47 | } 48 | 49 | // Spezielle Logger-Methoden 50 | static logPrinterStatus(printerId, status) { 51 | return this.debug( 52 | LOG_CATEGORIES.PRINTER, 53 | `Printer ${printerId} status:`, 54 | status 55 | ); 56 | } 57 | 58 | static logStream(message, data = null) { 59 | return this.debug(LOG_CATEGORIES.STREAM, message, data); 60 | } 61 | 62 | static logApi(message, data = null) { 63 | return this.debug(LOG_CATEGORIES.API, message, data); 64 | } 65 | 66 | static logApiResponse(endpoint, data = null) { 67 | return this.debug(LOG_CATEGORIES.API, `Response from ${endpoint}`, data); 68 | } 69 | 70 | static logPrinter(message, data = null) { 71 | return this.debug(LOG_CATEGORIES.PRINTER, message, data); 72 | } 73 | 74 | static notification(message, data = null) { 75 | return this.info(LOG_CATEGORIES.NOTIFICATION, message, data); 76 | } 77 | 78 | static printer(message, data = null) { 79 | return this.debug(LOG_CATEGORIES.PRINTER, message, data); 80 | } 81 | 82 | // Basis Logger-Methoden 83 | static log(level, category, message, data = null) { 84 | const timestamp = new Date().toISOString(); 85 | const logKey = this.formatLogKey(level, category, message, data); 86 | 87 | if (this.lastLogs.has(logKey)) { 88 | const lastLog = this.lastLogs.get(logKey); 89 | lastLog.count++; 90 | lastLog.lastTimestamp = timestamp; 91 | 92 | // Nur alle 10 Wiederholungen oder nach 30 Sekunden ausgeben 93 | if (lastLog.count % 10 === 0 || 94 | (new Date(timestamp) - new Date(lastLog.firstTimestamp)) > 30000) { 95 | console.log( 96 | `${lastLog.firstTimestamp} [${level}] [${category}] ${message} ` + 97 | `(${lastLog.count}x in ${Math.round((new Date(timestamp) - new Date(lastLog.firstTimestamp))/1000)}s)`, 98 | data || '' 99 | ); 100 | } 101 | } else { 102 | // Neuer Log-Eintrag 103 | this.lastLogs.set(logKey, { 104 | firstTimestamp: timestamp, 105 | lastTimestamp: timestamp, 106 | count: 1 107 | }); 108 | 109 | console.log( 110 | `${timestamp} [${level}] [${category}] ${message}`, 111 | data || '' 112 | ); 113 | } 114 | 115 | return { 116 | timestamp, 117 | level, 118 | category, 119 | message, 120 | data 121 | }; 122 | } 123 | 124 | static debug(category, message, data = null) { 125 | return this.log(LOG_LEVELS.DEBUG, category, message, data); 126 | } 127 | 128 | static info(category, message, data = null) { 129 | return this.log(LOG_LEVELS.INFO, category, message, data); 130 | } 131 | 132 | static warn(category, message, data = null) { 133 | return this.log(LOG_LEVELS.WARN, category, message, data); 134 | } 135 | 136 | static error(category, message, data = null) { 137 | return this.log(LOG_LEVELS.ERROR, category, message, data); 138 | } 139 | } 140 | 141 | export { Logger, LOG_LEVELS, LOG_CATEGORIES }; -------------------------------------------------------------------------------- /backend/src/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | from flask_cors import CORS 3 | from src.services import ( 4 | scanNetwork, 5 | startStream, 6 | addPrinter, 7 | getPrinters, 8 | getPrinterById, 9 | removePrinter, 10 | stream_service 11 | ) 12 | from src.routes.system import system_bp 13 | from src.routes.notifications import notifications_bp 14 | from src.routes.stream import stream_bp 15 | from src.routes.printers import printers_bp 16 | from src.routes.cloud import cloud_bp 17 | import logging # Standard Python logging 18 | from src.routes import register_blueprints 19 | import os 20 | from pathlib import Path 21 | import yaml 22 | import socket 23 | from src.config import Config 24 | from src.services.octoprintService import octoprint_service 25 | from src.services.bambuCloudService import bambu_cloud_service 26 | 27 | def get_host_ip(): 28 | """Ermittelt die Host-IP""" 29 | try: 30 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 31 | s.connect(('8.8.8.8', 80)) 32 | ip = s.getsockname()[0] 33 | s.close() 34 | return ip 35 | except Exception: 36 | return '0.0.0.0' 37 | 38 | # Logging-Konfiguration 39 | logging.basicConfig( 40 | level=logging.DEBUG, 41 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 42 | ) 43 | 44 | # Stelle sicher, dass auch die requests-Bibliothek Debug-Logs ausgibt 45 | logging.getLogger('urllib3').setLevel(logging.DEBUG) 46 | 47 | logger = logging.getLogger(__name__) 48 | 49 | app = Flask(__name__) 50 | logger.info("Starting Flask application...") 51 | 52 | # Definiere Basis-Verzeichnis 53 | BASE_DIR = Path(os.path.dirname(os.path.dirname(__file__))) 54 | DATA_DIR = BASE_DIR / 'data' 55 | PRINTERS_DIR = DATA_DIR / 'printers' 56 | STREAMS_DIR = DATA_DIR / 'streams' 57 | 58 | # Stelle sicher, dass die Verzeichnisse existieren 59 | os.makedirs(PRINTERS_DIR, exist_ok=True) 60 | os.makedirs(STREAMS_DIR, exist_ok=True) 61 | 62 | # Stelle sicher, dass das go2rtc Verzeichnis existiert 63 | Config.init_directories() # Erstellt alle benötigten Verzeichnisse 64 | 65 | # Erstelle initiale go2rtc Konfiguration wenn sie nicht existiert 66 | if not os.path.exists(Config.GO2RTC_CONFIG): 67 | logger.info("Creating initial go2rtc configuration") 68 | initial_config = { 69 | 'api': { 70 | 'listen': ':1984', 71 | 'base_path': 'go2rtc', 72 | 'origin': '*' 73 | }, 74 | 'webrtc': { 75 | 'listen': ':8555', 76 | 'candidates': [f"{get_host_ip()}:8555"] 77 | }, 78 | 'streams': {} 79 | } 80 | with open(Config.GO2RTC_CONFIG, 'w') as f: 81 | yaml.safe_dump(initial_config, f) 82 | logger.info(f"Created initial go2rtc config at {Config.GO2RTC_CONFIG}") 83 | 84 | # CORS mit erweiterten Optionen konfigurieren 85 | CORS(app, resources={ 86 | r"/api/*": { 87 | "origins": "*", 88 | "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], 89 | "allow_headers": ["Content-Type", "Authorization"] 90 | }, 91 | r"/stream/*": {"origins": "*"} 92 | }) 93 | 94 | # Blueprints nur EINMAL registrieren 95 | register_blueprints(app) 96 | 97 | # Initialisiere OctoPrint-Drucker 98 | logger.info("Initializing OctoPrint printers from stored configurations") 99 | octoprint_service.initialize_from_stored_printers() 100 | 101 | # Initialisiere Bambu Cloud-Drucker 102 | logger.info("Initializing Bambu Cloud printers from stored configurations") 103 | bambu_cloud_service.initialize_from_stored_printers() 104 | 105 | @app.before_request 106 | def log_request_info(): 107 | if request.path.startswith('/api/'): # Nur API-Anfragen loggen 108 | logger.info(f"API Request: {request.method} {request.url}") 109 | 110 | @app.route('/stream//stop', methods=['POST']) 111 | def stop_stream(printer_id): 112 | """Stoppt einen laufenden Stream""" 113 | try: 114 | stream_service.stop_stream(printer_id) 115 | return jsonify({'success': True}) 116 | except Exception as e: 117 | return jsonify({'error': str(e)}), 500 118 | 119 | @app.errorhandler(404) 120 | def not_found(e): 121 | """Handler für 404 Fehler - gibt JSON statt HTML zurück""" 122 | return jsonify({ 123 | 'success': False, 124 | 'error': 'Route not found' 125 | }), 404 126 | 127 | @app.route('/debug/routes') 128 | def list_routes(): 129 | routes = [] 130 | for rule in app.url_map.iter_rules(): 131 | routes.append({ 132 | 'endpoint': rule.endpoint, 133 | 'methods': list(rule.methods), 134 | 'path': str(rule) 135 | }) 136 | return jsonify(routes) 137 | 138 | @app.route('/api/test', methods=['GET']) 139 | def test_route(): 140 | return jsonify({ 141 | 'status': 'ok', 142 | 'message': 'Backend is running' 143 | }) 144 | 145 | @app.route('/api/stream/', methods=['GET']) 146 | def start_stream(printer_id): 147 | """Startet einen neuen Stream""" 148 | try: 149 | url = request.args.get('url') 150 | if not url: 151 | return jsonify({'error': 'No stream URL provided'}), 400 152 | 153 | # Starte den Stream 154 | result = stream_service.start_stream(printer_id, url) 155 | 156 | if result.get('success'): 157 | return jsonify(result) 158 | else: 159 | return jsonify({'error': result.get('error', 'Unknown error')}), 500 160 | 161 | except Exception as e: 162 | logger.error(f"Error starting stream: {e}") 163 | return jsonify({'error': str(e)}), 500 164 | 165 | @app.route('/api/debug/go2rtc/config') 166 | def debug_go2rtc_config(): 167 | try: 168 | with open(Config.GO2RTC_CONFIG, 'r') as f: 169 | content = f.read() 170 | logger.info(f"Current go2rtc config:\n{content}") 171 | config = yaml.safe_load(content) 172 | return jsonify(config) 173 | except Exception as e: 174 | logger.error(f"Error reading go2rtc config: {e}") 175 | return jsonify({'error': str(e)}), 500 176 | 177 | if __name__ == '__main__': 178 | app.run(host='0.0.0.0', port=4000, debug=True) -------------------------------------------------------------------------------- /frontend/src/components/NotificationButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { Fab, Tooltip, IconButton, Badge, Snackbar } from '@mui/material'; 3 | import NotificationsIcon from '@mui/icons-material/Notifications'; 4 | import NotificationsOffIcon from '@mui/icons-material/NotificationsOff'; 5 | import NotificationDialog from './NotificationDialog'; 6 | import { API_URL } from '../config'; 7 | import { Logger, LOG_CATEGORIES } from '../utils/logger'; 8 | 9 | const NotificationButton = () => { 10 | const [notificationsEnabled, setNotificationsEnabled] = useState(false); 11 | const [dialogOpen, setDialogOpen] = useState(false); 12 | const [pressTimer, setPressTimer] = useState(null); 13 | const [tooltipText, setTooltipText] = useState(''); 14 | const [snackbarOpen, setSnackbarOpen] = useState(false); 15 | 16 | const checkStatus = async () => { 17 | try { 18 | const response = await fetch(`${API_URL}/notifications/telegram/status`); 19 | const data = await response.json(); 20 | 21 | if (response.ok) { 22 | setNotificationsEnabled(data.enabled); 23 | Logger.notification('Checking notification status:', data); 24 | } 25 | } catch (error) { 26 | Logger.error('Error checking notification status:', error); 27 | } 28 | }; 29 | 30 | useEffect(() => { 31 | checkStatus(); 32 | const interval = setInterval(checkStatus, 5000); 33 | return () => clearInterval(interval); 34 | }, []); 35 | 36 | useEffect(() => { 37 | if (notificationsEnabled) { 38 | setTooltipText('Click to disable notifications\nLong press to reset configuration'); 39 | } else { 40 | setTooltipText('Click to enable notifications\nLong press to reset configuration'); 41 | } 42 | }, [notificationsEnabled]); 43 | 44 | const handlePressStart = useCallback(() => { 45 | const timer = setTimeout(async () => { 46 | try { 47 | const response = await fetch(`${API_URL}/notifications/telegram/reset`, { 48 | method: 'POST' 49 | }); 50 | const data = await response.json(); 51 | if (data.success) { 52 | Logger.notification('Notification configuration reset'); 53 | setNotificationsEnabled(false); 54 | setSnackbarOpen(true); 55 | } 56 | } catch (error) { 57 | Logger.error('Error resetting notification config:', error); 58 | } 59 | }, 1000); 60 | setPressTimer(timer); 61 | }, []); 62 | 63 | const handlePressEnd = useCallback(() => { 64 | if (pressTimer) { 65 | clearTimeout(pressTimer); 66 | setPressTimer(null); 67 | } 68 | }, [pressTimer]); 69 | 70 | const handleToggle = async () => { 71 | if (notificationsEnabled) { 72 | try { 73 | const response = await fetch(`${API_URL}/notifications/telegram/disable`, { 74 | method: 'POST' 75 | }); 76 | const data = await response.json(); 77 | if (data.success) { 78 | setNotificationsEnabled(false); 79 | Logger.notification('Notifications disabled'); 80 | } 81 | } catch (error) { 82 | Logger.error('Error disabling notifications:', error); 83 | } 84 | } else { 85 | try { 86 | // Prüfe erst, ob der Bot bereits konfiguriert ist 87 | const response = await fetch(`${API_URL}/notifications/telegram/status`); 88 | const data = await response.json(); 89 | 90 | if (data.configured) { 91 | // Bot ist bereits konfiguriert, also nur aktivieren 92 | const enableResponse = await fetch(`${API_URL}/notifications/telegram/enable`, { 93 | method: 'POST' 94 | }); 95 | const enableData = await enableResponse.json(); 96 | if (enableData.success) { 97 | setNotificationsEnabled(true); 98 | Logger.notification('Notifications enabled'); 99 | } 100 | } else { 101 | // Bot muss erst eingerichtet werden 102 | setDialogOpen(true); 103 | } 104 | } catch (error) { 105 | Logger.error('Error checking telegram status:', error); 106 | setDialogOpen(true); 107 | } 108 | } 109 | }; 110 | 111 | const handleDialogClose = (success = false) => { 112 | setDialogOpen(false); 113 | if (success) { 114 | checkStatus(); 115 | } 116 | }; 117 | 118 | return ( 119 | <> 120 | 124 | 150 | {notificationsEnabled ? : } 151 | 152 | 153 | 154 | 158 | 159 | setSnackbarOpen(false)} 163 | message="Notification settings have been reset" 164 | sx={{ 165 | '& .MuiSnackbarContent-root': { 166 | backgroundColor: 'rgba(0, 0, 0, 0.9)', 167 | color: '#00ffff', 168 | border: '1px solid #00ffff', 169 | boxShadow: '0 0 10px #00ffff' 170 | } 171 | }} 172 | /> 173 | 174 | ); 175 | }; 176 | 177 | export default NotificationButton; -------------------------------------------------------------------------------- /frontend/src/components/printer-info/OctoPrintInfo.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Box, Typography, LinearProgress, IconButton } from '@mui/material'; 3 | import StopCircleIcon from '@mui/icons-material/StopCircle'; 4 | import { styled } from '@mui/material/styles'; 5 | import { API_URL } from '../../config'; 6 | 7 | const InfoContainer = styled(Box)(({ theme }) => ({ 8 | padding: '1rem', 9 | color: '#00ffff' 10 | })); 11 | 12 | const ProgressBar = styled(LinearProgress)(({ theme }) => ({ 13 | height: 10, 14 | borderRadius: 5, 15 | backgroundColor: 'rgba(0, 255, 255, 0.1)', 16 | '& .MuiLinearProgress-bar': { 17 | backgroundColor: '#00ffff' 18 | } 19 | })); 20 | 21 | const statusMap = { 22 | 'ready': 'Ready', 23 | 'printing': 'Printing', 24 | 'paused': 'Paused', 25 | 'error': 'Error', 26 | 'offline': 'Offline', 27 | 'connecting': 'Connecting...', 28 | 'completed': 'Print Completed', 29 | 'failed': 'Print Failed' 30 | }; 31 | 32 | const OctoPrintInfo = ({ printer, onEmergencyStop }) => { 33 | const [printerStatus, setPrinterStatus] = useState({ 34 | id: printer.id, 35 | name: printer.name, 36 | temps: { 37 | hotend: 0, 38 | nozzle: 0, 39 | bed: 0, 40 | chamber: 0 41 | }, 42 | temperatures: { 43 | hotend: 0, 44 | nozzle: 0, 45 | bed: 0, 46 | chamber: 0 47 | }, 48 | status: 'connecting', 49 | progress: 0 50 | }); 51 | 52 | // Fetch status periodically 53 | useEffect(() => { 54 | const fetchStatus = async () => { 55 | try { 56 | const response = await fetch(`${API_URL}/printers/${printer.id}/status`); 57 | if (response.ok) { 58 | const data = await response.json(); 59 | console.log('OctoPrint status response:', data); 60 | 61 | // Get current temps to preserve non-zero values 62 | const currentTemps = printerStatus.temps || {}; 63 | const currentTemperatures = printerStatus.temperatures || {}; 64 | 65 | // Only update temperatures if they are non-zero in the new data 66 | const newTemps = data.temps || {}; 67 | const newTemperatures = data.temperatures || {}; 68 | 69 | const mergedTemps = { 70 | hotend: newTemps.hotend > 0 ? newTemps.hotend : (currentTemps.hotend || 0), 71 | nozzle: newTemps.nozzle > 0 ? newTemps.nozzle : (currentTemps.nozzle || 0), 72 | bed: newTemps.bed > 0 ? newTemps.bed : (currentTemps.bed || 0), 73 | chamber: newTemps.chamber > 0 ? newTemps.chamber : (currentTemps.chamber || 0) 74 | }; 75 | 76 | const mergedTemperatures = { 77 | hotend: newTemperatures.hotend > 0 ? newTemperatures.hotend : (currentTemperatures.hotend || 0), 78 | nozzle: newTemperatures.nozzle > 0 ? newTemperatures.nozzle : (currentTemperatures.nozzle || 0), 79 | bed: newTemperatures.bed > 0 ? newTemperatures.bed : (currentTemperatures.bed || 0), 80 | chamber: newTemperatures.chamber > 0 ? newTemperatures.chamber : (currentTemperatures.chamber || 0) 81 | }; 82 | 83 | // Merge the API response with existing printer data 84 | setPrinterStatus(prevStatus => ({ 85 | ...prevStatus, 86 | id: printer.id, 87 | name: printer.name, 88 | temps: mergedTemps, 89 | temperatures: mergedTemperatures, 90 | status: data.status || prevStatus.status, 91 | progress: data.progress || prevStatus.progress 92 | })); 93 | } 94 | } catch (error) { 95 | console.error('Error fetching OctoPrint status:', error); 96 | } 97 | }; 98 | 99 | // Initial fetch 100 | fetchStatus(); 101 | 102 | // Set up polling 103 | const interval = setInterval(fetchStatus, 5000); 104 | return () => clearInterval(interval); 105 | }, [printer.id, printer.name]); 106 | 107 | // Use the status from state 108 | const { 109 | temps = { hotend: 0, nozzle: 0, bed: 0, chamber: 0 }, 110 | temperatures = { hotend: 0, nozzle: 0, bed: 0, chamber: 0 }, 111 | status = 'connecting', 112 | progress = 0 113 | } = printerStatus; 114 | 115 | // Get hotend temperature, supporting both 'hotend' and 'nozzle' property names 116 | const hotendTemp = temps.hotend ?? temps.nozzle ?? temperatures.hotend ?? temperatures.nozzle ?? 0; 117 | const bedTemp = temps.bed ?? temperatures.bed ?? 0; 118 | const chamberTemp = temps.chamber ?? temperatures.chamber ?? 0; 119 | 120 | useEffect(() => { 121 | if (printer.streamUrl) { 122 | const video = document.getElementById(`video-${printer.id}`); 123 | if (video) { 124 | video.src = printer.streamUrl; 125 | } 126 | } 127 | }, [printer.streamUrl, printer.id]); 128 | 129 | return ( 130 | 131 | 132 | 133 | Status: {statusMap[status] || status} 134 | 135 | onEmergencyStop && onEmergencyStop(printer.id)} 137 | disabled={status === 'offline' || status === 'connecting'} 138 | sx={{ 139 | color: '#ff5555', 140 | padding: '2px', 141 | height: '24px', 142 | width: '24px', 143 | '&:hover': { 144 | backgroundColor: 'rgba(255, 0, 0, 0.1)' 145 | }, 146 | '&.Mui-disabled': { 147 | color: 'rgba(255, 85, 85, 0.3)' 148 | } 149 | }} 150 | > 151 | 152 | 153 | 154 | 155 | Hotend: {hotendTemp?.toFixed(1)}°C 156 | 157 | 158 | Bed: {bedTemp?.toFixed(1)}°C 159 | 160 | 161 | Chamber: {chamberTemp?.toFixed(1)}°C 162 | 163 | {status === 'printing' && ( 164 | 165 | 166 | 167 | {progress?.toFixed(1)}% 168 | 169 | 170 | )} 171 | 172 | ); 173 | }; 174 | 175 | export default OctoPrintInfo; -------------------------------------------------------------------------------- /frontend/src/components/SystemStatsDialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | Typography, 7 | Box, 8 | LinearProgress, 9 | IconButton 10 | } from '@mui/material'; 11 | import CloseIcon from '@mui/icons-material/Close'; 12 | import { API_URL } from '../config'; 13 | import { styled } from '@mui/material/styles'; 14 | import { Logger, LOG_CATEGORIES } from '../utils/logger'; 15 | 16 | // Styled Components 17 | const NeonDialog = styled(Dialog)(({ theme }) => ({ 18 | '& .MuiPaper-root': { 19 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.95)', 20 | border: theme.palette.mode === 'dark' 21 | ? '1px solid #00ffff' 22 | : '1px solid #008080', 23 | boxShadow: theme.palette.mode === 'dark' 24 | ? '0 0 10px #00ffff' 25 | : '0 0 10px rgba(0, 128, 128, 0.3)', 26 | color: theme.palette.mode === 'dark' ? '#fff' : '#333' 27 | } 28 | })); 29 | 30 | const NeonProgress = styled(LinearProgress)(({ theme }) => ({ 31 | height: 10, 32 | borderRadius: 5, 33 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(0, 255, 255, 0.2)' : 'rgba(0, 128, 128, 0.1)', 34 | '& .MuiLinearProgress-bar': { 35 | backgroundColor: theme.palette.mode === 'dark' ? '#00ffff' : '#008080', 36 | borderRadius: 5, 37 | }, 38 | '&:hover': { 39 | boxShadow: theme.palette.mode === 'dark' ? '0 0 5px #00ffff' : '0 0 5px rgba(0, 128, 128, 0.3)', 40 | } 41 | })); 42 | 43 | const StatBox = styled(Box)(({ theme }) => ({ 44 | border: theme.palette.mode === 'dark' ? '1px solid rgba(0, 255, 255, 0.3)' : '1px solid rgba(0, 128, 128, 0.3)', 45 | borderRadius: '8px', 46 | padding: '10px', 47 | margin: '5px 0', 48 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.5)' : 'rgba(255, 255, 255, 0.5)', 49 | color: theme.palette.mode === 'dark' ? '#fff' : '#333', 50 | '&:hover': { 51 | boxShadow: theme.palette.mode === 'dark' ? '0 0 5px rgba(0, 255, 255, 0.5)' : '0 0 5px rgba(0, 128, 128, 0.3)', 52 | backgroundColor: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)', 53 | } 54 | })); 55 | 56 | const SystemStatsDialog = ({ open, onClose }) => { 57 | const [stats, setStats] = useState(null); 58 | const [error, setError] = useState(null); 59 | 60 | const fetchSystemStats = async () => { 61 | try { 62 | const response = await fetch(`${API_URL}/system/stats`); 63 | const data = await response.json(); 64 | Logger.info('System stats:', data); 65 | setStats(data); 66 | setError(null); 67 | } catch (error) { 68 | Logger.error('Error fetching system stats:', error); 69 | setError('Failed to load system statistics'); 70 | } 71 | }; 72 | 73 | useEffect(() => { 74 | if (open) { 75 | fetchSystemStats(); 76 | const interval = setInterval(fetchSystemStats, 2000); 77 | return () => clearInterval(interval); 78 | } 79 | }, [open]); 80 | 81 | // Funktion zum Formatieren der Bytes in GB 82 | const formatBytes = (bytes) => { 83 | const gb = bytes / (1024 * 1024 * 1024); 84 | return gb.toFixed(1); 85 | }; 86 | 87 | return ( 88 | 94 | theme.palette.mode === 'dark' ? '#00ffff' : '#008080', 96 | textAlign: 'center', 97 | fontSize: '1.5rem', 98 | fontWeight: 'bold', 99 | textShadow: theme => theme.palette.mode === 'dark' ? '0 0 10px #00ffff' : 'none', 100 | pr: 6 101 | }}> 102 | System Statistics 103 | theme.palette.mode === 'dark' ? '#00ffff' : '#008080' 110 | }} 111 | > 112 | 113 | 114 | 115 | 116 | 117 | {error ? ( 118 | {error} 119 | ) : stats ? ( 120 | <> 121 | 122 | 123 | theme.palette.mode === 'dark' ? '#00ffff' : '#008080' }}>CPU Usage 124 | {stats.cpu.percent}% 125 | 126 | 127 | 128 | {stats.cpu.cores} Cores 129 | 130 | 131 | 132 | 133 | 134 | theme.palette.mode === 'dark' ? '#00ffff' : '#008080' }}>Memory Usage 135 | {stats.memory.percent}% 136 | 137 | 138 | 139 | {formatBytes(stats.memory.used)} GB / {formatBytes(stats.memory.total)} GB 140 | 141 | 142 | 143 | 144 | 145 | theme.palette.mode === 'dark' ? '#00ffff' : '#008080' }}>Disk Usage 146 | {stats.disk.percent}% 147 | 148 | 149 | 150 | {formatBytes(stats.disk.used)} GB / {formatBytes(stats.disk.total)} GB 151 | 152 | 153 | 154 | ) : ( 155 | 156 | 157 | 158 | )} 159 | 160 | 161 | ); 162 | }; 163 | 164 | export default SystemStatsDialog; -------------------------------------------------------------------------------- /frontend/src/components/CloudLoginDialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | TextField, 8 | Button, 9 | Alert, 10 | Box 11 | } from '@mui/material'; 12 | import styled from '@emotion/styled'; 13 | 14 | const GlassDialog = styled(Dialog)(({ theme }) => ({ 15 | '& .MuiDialog-paper': { 16 | background: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.95)', 17 | backdropFilter: 'blur(10px)', 18 | border: theme.palette.mode === 'dark' 19 | ? '1px solid rgba(0, 255, 255, 0.2)' 20 | : '1px solid rgba(0, 100, 200, 0.2)', 21 | borderRadius: '15px', 22 | boxShadow: theme.palette.mode === 'dark' 23 | ? '0 0 30px rgba(0, 255, 255, 0.2)' 24 | : '0 0 30px rgba(0, 100, 200, 0.1)', 25 | color: theme.palette.mode === 'dark' ? '#00ffff' : '#333333', 26 | '& .MuiDialogTitle-root': { 27 | color: theme.palette.mode === 'dark' ? '#00ffff' : '#008080' 28 | } 29 | } 30 | })); 31 | 32 | const NeonTextField = styled(TextField)(({ theme }) => ({ 33 | '& .MuiOutlinedInput-root': { 34 | color: theme.palette.mode === 'dark' ? '#00ffff' : '#333333', 35 | '& fieldset': { 36 | borderColor: theme.palette.mode === 'dark' ? 'rgba(0, 255, 255, 0.5)' : 'rgba(0, 100, 200, 0.3)', 37 | }, 38 | '&:hover fieldset': { 39 | borderColor: theme.palette.mode === 'dark' ? 'rgba(0, 255, 255, 0.5)' : 'rgba(0, 100, 200, 0.5)', 40 | }, 41 | '&.Mui-focused fieldset': { 42 | borderColor: theme.palette.mode === 'dark' ? '#00ffff' : '#008080', 43 | } 44 | }, 45 | '& .MuiInputLabel-root': { 46 | color: theme.palette.mode === 'dark' ? '#00ffff' : 'rgba(0, 0, 0, 0.6)', 47 | '&.Mui-focused': { 48 | color: theme.palette.mode === 'dark' ? '#00ffff' : '#008080' 49 | } 50 | }, 51 | '& .MuiInputBase-input': { 52 | color: theme.palette.mode === 'dark' ? '#00ffff' : '#333333' 53 | } 54 | })); 55 | 56 | const NeonButton = styled(Button)(({ theme }) => ({ 57 | background: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)', 58 | color: theme.palette.mode === 'dark' ? '#00ffff' : '#008080', 59 | border: theme.palette.mode === 'dark' 60 | ? '1px solid rgba(0, 255, 255, 0.3)' 61 | : '1px solid rgba(0, 128, 128, 0.3)', 62 | '&:hover': { 63 | background: theme.palette.mode === 'dark' 64 | ? 'rgba(0, 255, 255, 0.2)' 65 | : 'rgba(0, 128, 128, 0.1)', 66 | boxShadow: theme.palette.mode === 'dark' 67 | ? '0 0 20px rgba(0, 255, 255, 0.3)' 68 | : '0 0 20px rgba(0, 128, 128, 0.2)' 69 | } 70 | })); 71 | 72 | // API URL definieren 73 | const API_URL = `http://${window.location.hostname}:4000`; 74 | 75 | const CloudLoginDialog = ({ open, onClose, onLogin }) => { 76 | const [credentials, setCredentials] = useState({ 77 | email: '', 78 | password: '' 79 | }); 80 | const [error, setError] = useState(''); 81 | const [verificationCode, setVerificationCode] = useState(''); 82 | const [needsVerification, setNeedsVerification] = useState(false); 83 | const [isLoading, setIsLoading] = useState(false); 84 | 85 | const handleLogin = async () => { 86 | if (isLoading) return; // Verhindert doppelte Ausführung 87 | 88 | try { 89 | setIsLoading(true); 90 | setError(''); 91 | 92 | const response = await fetch(`${API_URL}/api/cloud/login`, { 93 | method: 'POST', 94 | headers: { 95 | 'Content-Type': 'application/json' 96 | }, 97 | body: JSON.stringify({ 98 | email: credentials.email, 99 | password: credentials.password, 100 | verification_code: verificationCode || undefined 101 | }) 102 | }); 103 | 104 | const data = await response.json(); 105 | console.log('Login response:', data); 106 | 107 | if (data.needs_verification) { 108 | setNeedsVerification(true); 109 | setError(data.error); 110 | return; 111 | } 112 | 113 | if (data.success) { 114 | await onLogin(data); 115 | onClose(); 116 | } else { 117 | setError(data.error || 'Login fehlgeschlagen'); 118 | } 119 | } catch (error) { 120 | console.error('Login error:', error); 121 | setError('Login fehlgeschlagen: ' + error.message); 122 | } finally { 123 | setIsLoading(false); 124 | } 125 | }; 126 | 127 | return ( 128 | 129 | Cloud Login 130 | 131 | {error && ( 132 | 144 | {error} 145 | 146 | )} 147 | 148 | setCredentials(prev => ({...prev, email: e.target.value}))} 152 | fullWidth 153 | margin="normal" 154 | /> 155 | setCredentials(prev => ({...prev, password: e.target.value}))} 160 | fullWidth 161 | margin="normal" 162 | /> 163 | {needsVerification && ( 164 | setVerificationCode(e.target.value)} 168 | fullWidth 169 | margin="normal" 170 | /> 171 | )} 172 | 173 | 174 | 175 | 180 | Cancel 181 | 182 | 187 | {isLoading ? 'Loading...' : 'Login'} 188 | 189 | 190 | 191 | ); 192 | }; 193 | 194 | export default CloudLoginDialog; -------------------------------------------------------------------------------- /backend/src/services/networkScanner.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import queue 4 | import json 5 | import requests 6 | import logging 7 | import uuid 8 | import time 9 | from urllib3.exceptions import InsecureRequestWarning 10 | import ipaddress 11 | import paho.mqtt.client as mqtt 12 | 13 | # Warnungen für selbst-signierte Zertifikate unterdrücken 14 | requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | def test_lan_mode(ip): 19 | """Tests if printer is in LAN mode by checking RTSP port""" 20 | try: 21 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 22 | sock.settimeout(2) 23 | result = sock.connect_ex((ip, 322)) 24 | sock.close() 25 | 26 | return result == 0 27 | except Exception as e: 28 | logger.error(f"Error testing LAN mode for {ip}: {e}") 29 | return False 30 | 31 | def parse_printer_info(response, ip): 32 | """Extrahiert detaillierte Drucker-Informationen aus der SSDP-Antwort""" 33 | printer_info = { 34 | 'id': str(uuid.uuid4()), 35 | 'ip': ip, 36 | 'type': 'BAMBULAB', 37 | 'status': 'online', 38 | 'mode': 'unknown', 39 | 'serial': '', 40 | 'name': f'Printer {ip}', 41 | 'model': 'X1C', 42 | 'version': '', 43 | 'lan_mode_enabled': False, 44 | 'access_code': '' 45 | } 46 | 47 | try: 48 | # Check LAN mode using RTSP port 49 | lan_mode_available = test_lan_mode(ip) 50 | printer_info['lan_mode_enabled'] = lan_mode_available 51 | 52 | # Parse SSDP response for additional info 53 | for line in response.split('\r\n'): 54 | if 'DevName.bambu.com:' in line: 55 | printer_info['name'] = line.split(':', 1)[1].strip() 56 | elif 'DevModel.bambu.com:' in line: 57 | printer_info['model'] = line.split(':', 1)[1].strip() 58 | elif 'DevVersion.bambu.com:' in line: 59 | printer_info['version'] = line.split(':', 1)[1].strip() 60 | elif 'DevConnect.bambu.com:' in line: 61 | connect_mode = line.split(':', 1)[1].strip().lower() 62 | printer_info['mode'] = connect_mode # Use the actual mode from the header 63 | elif 'USN:' in line: 64 | printer_info['serial'] = line.split(':', 1)[1].strip() 65 | 66 | # If DevConnect header wasn't found, fall back to the port test 67 | if printer_info['mode'] == 'unknown': 68 | printer_info['mode'] = 'lan' if lan_mode_available else 'cloud' 69 | 70 | logger.debug(f"Found printer: {printer_info}") 71 | return printer_info 72 | 73 | except Exception as e: 74 | logger.error(f"Error parsing printer info for {ip}: {e}") 75 | return printer_info 76 | 77 | def scanNetwork(network_range=None): 78 | """Scannt das Netzwerk nach Bambu Lab Druckern""" 79 | try: 80 | # Get local IP and network if not provided 81 | if not network_range: 82 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 83 | try: 84 | s.connect(('8.8.8.8', 80)) 85 | local_ip = s.getsockname()[0] 86 | logger.info(f"Local IP: {local_ip}") 87 | network_range = f"{'.'.join(local_ip.split('.')[:3])}.0/24" 88 | except Exception as e: 89 | logger.error(f"Error getting local IP: {e}") 90 | network_range = "192.168.1.0/24" # Fallback 91 | finally: 92 | s.close() 93 | 94 | logger.info(f"Starting network scan on {network_range}") 95 | found_printers = [] 96 | 97 | # SSDP Discovery 98 | ssdp_request = ( 99 | 'M-SEARCH * HTTP/1.1\r\n' 100 | 'HOST: 239.255.255.250:1990\r\n' 101 | 'MAN: "ssdp:discover"\r\n' 102 | 'MX: 3\r\n' 103 | 'ST: urn:bambulab-com:device:3dprinter:1\r\n' 104 | '\r\n' 105 | ).encode() 106 | 107 | # UDP Socket für SSDP mit Broadcast 108 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 109 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 110 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 111 | sock.settimeout(2) 112 | 113 | try: 114 | # Bind to all interfaces 115 | sock.bind(('0.0.0.0', 0)) 116 | logger.info(f"SSDP socket bound to port {sock.getsockname()[1]}") 117 | 118 | # Sende SSDP Anfragen auf verschiedenen Ports 119 | discovery_ports = [1990, 2021, 1982] # Added 1982 120 | for port in discovery_ports: 121 | try: 122 | logger.info(f"Sending SSDP discovery to port {port}") 123 | for _ in range(3): 124 | sock.sendto(ssdp_request, ('239.255.255.250', port)) 125 | sock.sendto(ssdp_request, ('255.255.255.255', port)) 126 | time.sleep(0.2) 127 | except Exception as e: 128 | logger.error(f"Error sending to port {port}: {e}") 129 | 130 | # Sammle SSDP Antworten 131 | start_time = time.time() 132 | while time.time() - start_time < 5: 133 | try: 134 | data, addr = sock.recvfrom(4096) 135 | response = data.decode() 136 | logger.debug(f"Received from {addr}: {response}") 137 | 138 | if 'bambulab' in response.lower(): 139 | printer_info = parse_printer_info(response, addr[0]) 140 | if not any(p['ip'] == addr[0] for p in found_printers): 141 | found_printers.append(printer_info) 142 | logger.info(f"Found printer via SSDP: {printer_info}") 143 | 144 | except socket.timeout: 145 | continue 146 | except Exception as e: 147 | logger.error(f"Error receiving SSDP response: {e}") 148 | 149 | finally: 150 | sock.close() 151 | 152 | # HTTP Scan als Backup 153 | if not found_printers: 154 | logger.info("No printers found via SSDP, trying HTTP scan...") 155 | result_queue = queue.Queue() 156 | threads = [] 157 | 158 | network = ipaddress.ip_network(network_range) 159 | logger.info(f"Scanning {len(list(network.hosts()))} hosts...") 160 | 161 | for ip in network.hosts(): 162 | ip_str = str(ip) 163 | thread = threading.Thread(target=scan_printer, args=(ip_str, 8989, result_queue)) 164 | thread.daemon = True 165 | threads.append(thread) 166 | thread.start() 167 | 168 | # Wait for all threads with timeout 169 | for thread in threads: 170 | thread.join(timeout=0.1) 171 | 172 | while not result_queue.empty(): 173 | found_printers.append(result_queue.get()) 174 | 175 | logger.info(f"Scan complete. Found {len(found_printers)} printers") 176 | return found_printers 177 | 178 | except Exception as e: 179 | logger.error(f"Error during network scan: {e}") 180 | return [] -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { ThemeProvider, CssBaseline } from '@mui/material'; 3 | import PrinterGrid from './components/PrinterGrid'; 4 | import { lightTheme, darkTheme } from './theme'; 5 | import { Box } from '@mui/material'; 6 | import styled from '@emotion/styled'; 7 | import CloudLoginDialog from './components/CloudLoginDialog'; 8 | import CloudPrinterDialog from './components/CloudPrinterDialog'; 9 | import { API_URL } from './config'; 10 | 11 | const PageBackground = styled(Box)(({ theme }) => ({ 12 | position: 'relative', 13 | minHeight: '100vh', 14 | backgroundColor: theme.palette.background.default 15 | })); 16 | 17 | const BackgroundImage = styled(Box)(({ theme }) => ({ 18 | position: 'absolute', 19 | top: 0, 20 | left: 0, 21 | right: 0, 22 | bottom: 0, 23 | background: `url('${process.env.PUBLIC_URL}/background.png') center center fixed`, 24 | backgroundSize: 'contain', 25 | backgroundRepeat: 'no-repeat', 26 | opacity: theme.palette.mode === 'dark' ? 0.03 : 0.05, 27 | zIndex: 0 28 | })); 29 | 30 | const ContentWrapper = styled(Box)({ 31 | position: 'relative', 32 | zIndex: 1, 33 | padding: 24 34 | }); 35 | 36 | function App() { 37 | // Theme aus localStorage laden 38 | const [isDarkMode, setIsDarkMode] = useState(() => { 39 | const savedTheme = localStorage.getItem('theme'); 40 | return savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches); 41 | }); 42 | 43 | // Mode aus localStorage laden 44 | const [mode, setMode] = useState(() => { 45 | const savedMode = localStorage.getItem('mode'); 46 | const hasToken = localStorage.getItem('cloudToken'); 47 | // Wenn Cloud-Mode gespeichert ist und ein Token existiert 48 | if (savedMode === 'cloud' && hasToken) { 49 | return 'cloud'; 50 | } 51 | return 'lan'; 52 | }); 53 | 54 | const [isLoginOpen, setLoginOpen] = useState(false); 55 | const [cloudPrinters, setCloudPrinters] = useState([]); 56 | const [lanPrinters, setLanPrinters] = useState([]); 57 | const [addPrinterDialogOpen, setAddPrinterDialogOpen] = useState(false); 58 | const [cloudPrinterDialogOpen, setCloudPrinterDialogOpen] = useState(false); 59 | const [availableCloudPrinters, setAvailableCloudPrinters] = useState([]); 60 | const [selectedCloudPrinters, setSelectedCloudPrinters] = useState([]); 61 | 62 | // Lade LAN Drucker 63 | useEffect(() => { 64 | let isMounted = true; 65 | 66 | if (mode === 'lan') { 67 | fetch(`${API_URL}/printers`) 68 | .then(res => res.json()) 69 | .then(data => { 70 | if (isMounted) { 71 | setLanPrinters(data); 72 | } 73 | }) 74 | .catch(err => console.error('Error loading LAN printers:', err)); 75 | } 76 | 77 | return () => { 78 | isMounted = false; 79 | }; 80 | }, [mode]); 81 | 82 | const toggleTheme = () => { 83 | setIsDarkMode(!isDarkMode); 84 | localStorage.setItem('theme', !isDarkMode ? 'dark' : 'light'); 85 | }; 86 | 87 | // Mode-Wechsel Handler 88 | const handleModeChange = async (newMode) => { 89 | try { 90 | if (newMode === 'cloud') { 91 | // Check cloud status first 92 | const response = await fetch(`${API_URL}/cloud/status?mode=${newMode}`); 93 | const status = await response.json(); 94 | 95 | if (status.connected && status.token) { 96 | // Already logged in, just switch mode 97 | setMode('cloud'); 98 | localStorage.setItem('mode', 'cloud'); 99 | loadCloudPrinters(status.token); 100 | } else { 101 | // Need to login 102 | setLoginOpen(true); 103 | } 104 | } else { 105 | // Switching to LAN mode - tell backend to disconnect cloud 106 | await fetch(`${API_URL}/cloud/status?mode=lan`); 107 | setMode(newMode); 108 | localStorage.setItem('mode', newMode); 109 | } 110 | } catch (error) { 111 | console.error('Error changing mode:', error); 112 | setMode('lan'); 113 | localStorage.setItem('mode', 'lan'); 114 | } 115 | }; 116 | 117 | // Initial mode check 118 | useEffect(() => { 119 | const checkInitialMode = async () => { 120 | const savedMode = localStorage.getItem('mode') || 'lan'; 121 | 122 | if (savedMode === 'cloud') { 123 | try { 124 | const response = await fetch(`${API_URL}/cloud/status?mode=cloud`); 125 | const status = await response.json(); 126 | 127 | if (status.connected && status.token) { 128 | setMode('cloud'); 129 | loadCloudPrinters(status.token); 130 | } else { 131 | // Token invalid/expired, switch back to LAN 132 | await fetch(`${API_URL}/cloud/status?mode=lan`); // Disconnect cloud 133 | setMode('lan'); 134 | localStorage.setItem('mode', 'lan'); 135 | } 136 | } catch (error) { 137 | console.error('Error checking cloud status:', error); 138 | setMode('lan'); 139 | localStorage.setItem('mode', 'lan'); 140 | } 141 | } else { 142 | setMode(savedMode); 143 | } 144 | }; 145 | 146 | checkInitialMode(); 147 | }, []); 148 | 149 | // Cloud-Drucker laden 150 | const loadCloudPrinters = async (token) => { 151 | try { 152 | const response = await fetch(`${API_URL}/cloud/printers`, { 153 | headers: { 154 | 'Authorization': `Bearer ${token}` 155 | } 156 | }); 157 | if (response.ok) { 158 | const printers = await response.json(); 159 | setCloudPrinters(printers); 160 | } 161 | } catch (error) { 162 | console.error('Error loading cloud printers:', error); 163 | // Bei Fehler zurück zu LAN 164 | setMode('lan'); 165 | localStorage.setItem('mode', 'lan'); 166 | localStorage.removeItem('cloudToken'); 167 | } 168 | }; 169 | 170 | // Cloud Login Handler 171 | const handleCloudLogin = async (loginData) => { 172 | try { 173 | if (loginData.success && loginData.token) { 174 | localStorage.setItem('cloudToken', loginData.token); 175 | 176 | const response = await fetch(`${API_URL}/cloud/printers`, { 177 | headers: { 178 | 'Authorization': `Bearer ${loginData.token}` 179 | } 180 | }); 181 | 182 | if (response.ok) { 183 | const printers = await response.json(); 184 | setAvailableCloudPrinters(printers); 185 | localStorage.setItem('mode', 'cloud'); 186 | setLoginOpen(false); 187 | setCloudPrinterDialogOpen(true); 188 | // Login erfolgreich Event 189 | window.dispatchEvent(new Event('cloud-login-success')); 190 | } else { 191 | throw new Error('Fehler beim Laden der Cloud-Drucker'); 192 | } 193 | } 194 | } catch (error) { 195 | console.error('Cloud login error:', error); 196 | // Login fehlgeschlagen Event 197 | window.dispatchEvent(new Event('cloud-login-failed')); 198 | throw error; 199 | } 200 | }; 201 | 202 | const handleAddPrinter = () => { 203 | // Öffne den AddPrinterDialog 204 | setAddPrinterDialogOpen(true); 205 | }; 206 | 207 | const handleAddCloudPrinter = (printer) => { 208 | setSelectedCloudPrinters(prev => [...prev, printer]); 209 | setCloudPrinters(prev => [...prev, printer]); 210 | }; 211 | 212 | return ( 213 | 214 | 215 | 216 | 217 | 218 | 226 | 227 | 228 | 229 | setLoginOpen(false)} 232 | onLogin={handleCloudLogin} 233 | /> 234 | 235 | {/* Cloud Drucker Auswahl Dialog */} 236 | setCloudPrinterDialogOpen(false)} 239 | printers={availableCloudPrinters} 240 | onAddPrinter={(printer) => { 241 | handleAddCloudPrinter(printer); 242 | setCloudPrinterDialogOpen(false); 243 | }} 244 | /> 245 | 246 | ); 247 | } 248 | 249 | export default App; -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Services/DownloadService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using System.IO.Compression; 6 | using BambuCAM.Installer.Models; 7 | using System.Net.Http.Json; 8 | using System.Text.Json.Serialization; 9 | using System.Linq; 10 | 11 | namespace BambuCAM.Installer.Services 12 | { 13 | public class DownloadService 14 | { 15 | private readonly string _installDir; 16 | private readonly HttpClient _client; 17 | 18 | public DownloadService() 19 | { 20 | _installDir = Path.Combine( 21 | Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 22 | "BambuCAM" 23 | ); 24 | _client = new HttpClient(); 25 | // GitHub API Headers 26 | _client.DefaultRequestHeaders.Add("User-Agent", "BambuCAM-Installer"); 27 | // Optional: Füge einen GitHub Token hinzu für höhere Rate Limits 28 | // _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "YOUR_TOKEN"); 29 | } 30 | 31 | public string InstallDir => _installDir; 32 | 33 | public async Task DownloadAndExtract(IProgress progress) 34 | { 35 | try 36 | { 37 | // Get latest release info 38 | progress.Report(new InstallationStatus(0, "Checking latest version...")); 39 | var releaseInfo = await GetLatestRelease(); 40 | 41 | // Download ZIP 42 | var zipPath = Path.Combine(Path.GetTempPath(), "BambuCAM.zip"); 43 | await DownloadFile(releaseInfo.DownloadUrl, zipPath, progress); 44 | 45 | // Extract 46 | progress.Report(new InstallationStatus(95, "Extracting files...", "This might take a minute")); 47 | Directory.CreateDirectory(_installDir); 48 | Directory.CreateDirectory(Path.Combine(_installDir, "backend", "data", "go2rtc")); 49 | 50 | // Extrahiere das ZIP 51 | using (var archive = ZipFile.OpenRead(zipPath)) 52 | { 53 | // Debug: Zeige alle Dateien im ZIP 54 | Console.WriteLine("Files in ZIP:"); 55 | foreach (var entry in archive.Entries) 56 | { 57 | Console.WriteLine($"- {entry.FullName}"); 58 | } 59 | 60 | // Der erste Eintrag ist normalerweise der Hauptordner 61 | var rootFolder = archive.Entries[0].FullName; 62 | Console.WriteLine($"Root folder: {rootFolder}"); 63 | 64 | // Suche nach docker-compose.yml (nicht mehr .prod) 65 | var composeEntry = archive.Entries.FirstOrDefault(e => 66 | e.FullName.EndsWith("docker-compose.yml", StringComparison.OrdinalIgnoreCase)); 67 | 68 | if (composeEntry == null) 69 | { 70 | throw new Exception("docker-compose.yml not found in downloaded ZIP"); 71 | } 72 | 73 | // Extrahiere docker-compose.yml direkt ins Zielverzeichnis 74 | composeEntry.ExtractToFile(Path.Combine(_installDir, "docker-compose.yml"), true); 75 | 76 | // Erstelle benötigte Verzeichnisse 77 | Directory.CreateDirectory(Path.Combine(_installDir, "backend", "data")); 78 | Directory.CreateDirectory(Path.Combine(_installDir, "backend", "data", "go2rtc")); 79 | } 80 | 81 | // Cleanup 82 | File.Delete(zipPath); 83 | 84 | // Debug: Zeige extrahierte Dateien 85 | Console.WriteLine("\nFiles in install directory:"); 86 | foreach (var file in Directory.GetFiles(_installDir, "*.*", SearchOption.AllDirectories)) 87 | { 88 | Console.WriteLine($"- {file}"); 89 | } 90 | 91 | // Nach dem Extrahieren der docker-compose.yml 92 | var prodComposeContent = @"version: '3' 93 | 94 | services: 95 | frontend: 96 | image: bangertech/bambucam-frontend:latest 97 | restart: unless-stopped 98 | network_mode: 'host' 99 | 100 | backend: 101 | image: bangertech/bambucam-backend:latest 102 | restart: unless-stopped 103 | volumes: 104 | - bambucam_data:/app/data 105 | - bambucam_logs:/app/logs 106 | network_mode: 'host' 107 | 108 | go2rtc: 109 | image: alexxit/go2rtc 110 | container_name: go2rtc 111 | restart: unless-stopped 112 | network_mode: host 113 | volumes: 114 | - bambucam_go2rtc:/config 115 | environment: 116 | - GO2RTC_CONFIG=/config/go2rtc.yaml 117 | - GO2RTC_API=listen=:1984 118 | - GO2RTC_API_BASE=/go2rtc 119 | 120 | volumes: 121 | bambucam_logs: 122 | bambucam_data: 123 | bambucam_go2rtc:"; 124 | 125 | // Schreibe die Production docker-compose.yml 126 | await File.WriteAllTextAsync(Path.Combine(_installDir, "docker-compose.yml"), prodComposeContent); 127 | } 128 | catch (Exception ex) 129 | { 130 | throw new Exception($"Download failed: {ex.Message}", ex); 131 | } 132 | } 133 | 134 | private async Task GetLatestRelease() 135 | { 136 | try 137 | { 138 | // Versuche zuerst den neuesten Release zu bekommen 139 | try 140 | { 141 | var response = await _client.GetFromJsonAsync( 142 | "https://api.github.com/repos/BangerTech/BambuCAM/releases/latest" 143 | ); 144 | 145 | if (response != null) 146 | { 147 | return new ReleaseInfo 148 | { 149 | TagName = response.TagName, 150 | DownloadUrl = response.ZipballUrl 151 | }; 152 | } 153 | } 154 | catch 155 | { 156 | // Wenn kein Release gefunden wurde, ignoriere den Fehler und nutze main.zip 157 | } 158 | 159 | // Fallback: Wenn kein Release existiert, nutze den main branch 160 | return new ReleaseInfo 161 | { 162 | TagName = "development", 163 | DownloadUrl = "https://github.com/BangerTech/BambuCAM/archive/refs/heads/main.zip" 164 | }; 165 | } 166 | catch (Exception ex) 167 | { 168 | throw new Exception("Failed to get release info. Please check your internet connection.", ex); 169 | } 170 | } 171 | 172 | private async Task DownloadFile(string url, string path, IProgress progress) 173 | { 174 | using var response = await _client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); 175 | response.EnsureSuccessStatusCode(); 176 | 177 | var totalBytes = response.Content.Headers.ContentLength ?? -1L; 178 | var totalMB = totalBytes / 1024.0 / 1024.0; 179 | 180 | using var fileStream = File.Create(path); 181 | using var downloadStream = await response.Content.ReadAsStreamAsync(); 182 | 183 | var buffer = new byte[81920]; // Größerer Buffer für schnelleres Downloaden 184 | var bytesRead = 0L; 185 | var startTime = DateTime.Now; 186 | 187 | while (true) 188 | { 189 | var read = await downloadStream.ReadAsync(buffer); 190 | if (read == 0) break; 191 | 192 | await fileStream.WriteAsync(buffer.AsMemory(0, read)); 193 | bytesRead += read; 194 | 195 | if (totalBytes != -1) 196 | { 197 | var percent = (int)((bytesRead * 100) / totalBytes); 198 | var downloadedMB = bytesRead / 1024.0 / 1024.0; 199 | var elapsed = DateTime.Now - startTime; 200 | var speed = downloadedMB / elapsed.TotalSeconds; 201 | var remaining = TimeSpan.FromSeconds((totalMB - downloadedMB) / speed); 202 | 203 | progress.Report(new InstallationStatus( 204 | percent, 205 | $"Downloading BambuCAM... {percent}%", 206 | $"Downloaded: {downloadedMB:F1} MB of {totalMB:F1} MB\n" + 207 | $"Speed: {speed:F1} MB/s\n" + 208 | $"Time remaining: {remaining:mm\\:ss}" 209 | )); 210 | } 211 | } 212 | } 213 | } 214 | 215 | public class ReleaseInfo 216 | { 217 | public string TagName { get; set; } 218 | public string DownloadUrl { get; set; } 219 | } 220 | 221 | // Klasse für die GitHub API Response (private entfernt) 222 | internal class GitHubRelease 223 | { 224 | [JsonPropertyName("tag_name")] 225 | public string TagName { get; set; } 226 | 227 | [JsonPropertyName("zipball_url")] 228 | public string ZipballUrl { get; set; } 229 | } 230 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BambuCAM 2 | 3 | ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) 4 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 5 | ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) 6 | [![License](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE) 7 | 8 |
9 | BambuCAM Logo 10 |
11 | 12 | > 🎥 A modern web application for monitoring multiple 3D Printer (BambuLab X1C, Creality / Fluidd / Moonraker, OctoPrint) through their camera feeds 13 | 14 | ## Screenshots 15 | 16 | 17 | 18 | _Left to right: Home screen, Add printer, Monitoring view with multiple printers_ 19 | 20 | ## Table of Contents 21 | - [What is BambuCAM?](#what-is-bambucam) 22 | - [Features](#features) 23 | - [Installation](#installation) 24 | - [Requirements](#requirements) 25 | - [Troubleshooting](#troubleshooting) 26 | - [Support](#support) 27 | 28 | ## What is BambuCAM? 29 | BambuCAM is a user-friendly web application for monitoring different 3D printers (Bambulab X1C, Creality / Moonraker, OctoPrint). It supports both local and cloud printers, offering enhanced video streaming through go2rtc integration and a clean, modern interface. 30 | 31 | ### Features 32 | - 🎥 Enhanced live camera feeds with [go2rtc](https://github.com/AlexxIT/go2rtc) integration 33 | - 🖱️ Drag & drop interface for camera arrangement 34 | - 🖥️ Fullscreen mode for each printer 35 | - ➕ Easy adding and removing of printers 36 | - 🔄 Improved stream stability and auto-reconnection 37 | - 🌐 Support for Bambu Cloud printers ( no videostreaming on Cloud Mode yet ) 38 | - 🔔 Status notifications and monitoring 39 | - 🚀 Optimized performance with nginx 40 | - 📱 Responsive design for all devices 41 | 42 | ## Installation 43 | 44 | ### 🐳 Quick Docker Installation 45 | 46 | The fastest way to get started is using our pre-built Docker images: 47 | 48 | 1. Create a Folder: `sudo mkdir BambuCAM` 49 | 2. Jump into the folder: `cd BambuCAM` 50 | 3. Create a `docker-compose.yml`: 51 | ```yaml 52 | 53 | services: 54 | frontend: 55 | image: bangertech/bambucam-frontend:latest 56 | restart: unless-stopped 57 | network_mode: "host" 58 | 59 | backend: 60 | image: bangertech/bambucam-backend:latest 61 | restart: unless-stopped 62 | volumes: 63 | - type: bind 64 | source: ./data 65 | target: /app/data 66 | bind: 67 | create_host_path: true 68 | - type: bind 69 | source: ./logs 70 | target: /app/logs 71 | bind: 72 | create_host_path: true 73 | - type: bind 74 | source: ./data/go2rtc 75 | target: /app/data/go2rtc 76 | bind: 77 | create_host_path: true 78 | environment: 79 | - LOG_LEVEL=DEBUG 80 | network_mode: "host" 81 | 82 | nginx: 83 | image: nginx:alpine 84 | network_mode: "host" 85 | restart: unless-stopped 86 | command: > 87 | /bin/sh -c "echo 'worker_processes auto; 88 | events { 89 | worker_connections 1024; 90 | } 91 | http { 92 | include /etc/nginx/mime.types; 93 | default_type application/octet-stream; 94 | sendfile on; 95 | keepalive_timeout 65; 96 | 97 | server { 98 | listen 80; 99 | 100 | location / { 101 | proxy_pass http://localhost:3000; 102 | proxy_http_version 1.1; 103 | proxy_set_header Upgrade $$http_upgrade; 104 | proxy_set_header Connection \"upgrade\"; 105 | proxy_set_header Host $$host; 106 | } 107 | 108 | location /api { 109 | proxy_pass http://localhost:4000; 110 | proxy_http_version 1.1; 111 | proxy_set_header Host $$host; 112 | } 113 | 114 | location /go2rtc/ { 115 | proxy_pass http://localhost:1984/; 116 | proxy_http_version 1.1; 117 | proxy_set_header Upgrade $$http_upgrade; 118 | proxy_set_header Connection \"upgrade\"; 119 | proxy_set_header Host $$host; 120 | } 121 | } 122 | }' > /etc/nginx/nginx.conf && nginx -g 'daemon off;'" 123 | depends_on: 124 | - frontend 125 | - backend 126 | - go2rtc 127 | 128 | go2rtc: 129 | image: alexxit/go2rtc 130 | container_name: go2rtc 131 | restart: unless-stopped 132 | network_mode: host 133 | volumes: 134 | - type: bind 135 | source: ./data/go2rtc 136 | target: /config 137 | bind: 138 | create_host_path: true 139 | environment: 140 | - GO2RTC_CONFIG=/config/go2rtc.yaml 141 | - GO2RTC_API=listen=:1984 142 | - GO2RTC_API_BASE=/go2rtc 143 | - GO2RTC_LOG_LEVEL=debug 144 | command: > 145 | /bin/sh -c " 146 | mkdir -p /config && 147 | touch /config/go2rtc.yaml && 148 | chmod 777 /config/go2rtc.yaml && 149 | echo 'api:' > /config/go2rtc.yaml && 150 | echo ' listen: :1984' >> /config/go2rtc.yaml && 151 | echo ' base: /go2rtc' >> /config/go2rtc.yaml && 152 | echo 'webrtc:' >> /config/go2rtc.yaml && 153 | echo ' listen: :8555' >> /config/go2rtc.yaml && 154 | echo 'rtsp:' >> /config/go2rtc.yaml && 155 | echo ' listen: :8554' >> /config/go2rtc.yaml && 156 | go2rtc 157 | " 158 | depends_on: 159 | - backend 160 | ``` 161 | 162 | 4. Start BambuCAM: 163 | ```bash 164 | docker compose up -d 165 | ``` 166 | 167 | That's it! The application will be available at http://localhost. Your data will be stored in the `./data` and `./logs` directories, making it easy to access and backup. 168 | 169 | ### Windows Users 170 | 171 | For Windows users who prefer a guided installation: 172 | 173 | #### 🚀 Quick Start 174 | 175 | 1. Download the [BambuCAM Installer](https://github.com/BangerTech/BambuCAM/releases/latest) 176 | 2. Run the installer as administrator 177 | 3. Open BambuCAM via desktop shortcut or at http://localhost 178 | 179 | The installer will automatically: 180 | - Install Docker Desktop if needed 181 | - Configure WSL2 and port forwarding 182 | - Pull the latest Docker images 183 | - Create a desktop shortcut 184 | 185 | #### 📋 System Requirements 186 | 187 | - Windows 10/11 188 | - 4 GB RAM 189 | - 2 GB free disk space 190 | - Available Ports: 191 | - 80 (Web Interface) 192 | - 1984 (go2rtc) 193 | - 4000 (Backend API) 194 | 195 | 196 | #### 🔧 Uninstallation 197 | 198 | 1. Run `docker compose down` in the installation directory 199 | 2. Delete the folder `%LOCALAPPDATA%\BambuCAM` 200 | 3. Remove the desktop shortcut 201 | 202 | #### ❓ Troubleshooting 203 | 204 | If you encounter issues: 205 | 1. Make sure Docker Desktop is running 206 | 2. Check if port 3000 is not in use by another application 207 | 3. Open an issue on GitHub 208 | 209 | ### Method 3: Manual Installation (Linux) 210 | 211 | For users who want to build from source: 212 | 213 | #### Quick Start Installation 214 | 1. Clone repository: 215 | ```bash 216 | git clone https://github.com/BangerTech/BambuCAM.git 217 | cd BambuCAM 218 | ``` 219 | 220 | 2. Start Docker Compose: 221 | ```bash 222 | docker compose -f docker-compose.dev.yml up --build 223 | ``` 224 | 225 | 3. Open in browser: 226 | ```bash 227 | http://localhost:3000 228 | ``` 229 | 230 | ## Printer Setup 231 | 232 | ### Requirements 233 | - BambuLab printer (local network or cloud) 234 | - Camera enabled in printer settings 235 | 236 | ### Adding a Printer 237 | #### Local Printer 238 | 1. Click "Add Printer" in the app 239 | 2. Enter a name for the printer 240 | 3. Enter the printer's IP address 241 | 4. Enter Access Code (found in printer settings under "Network") 242 | 5. Click "Add" 243 | 244 | #### Cloud Printer 245 | 1. Click "Add Printer" and select "Cloud Printer" 246 | 2. Log in with your Bambu Lab account 247 | 3. Select your printer from the list 248 | 4. Click "Add" 249 | 250 | ## Technologies 251 | - React.js Frontend 252 | - Node.js Backend 253 | - Docker & Docker Compose 254 | - go2rtc Stream Processing 255 | - nginx Reverse Proxy 256 | 257 | ## Troubleshooting 258 | 259 | If you cannot connect to the printer: 260 | 1. Check if the printer is powered on and connected to the network 261 | 2. Verify you are using the correct IP address 262 | 3. Verify the Access Code is correct 263 | 4. Check if "LAN Only Mode" is enabled 264 | 5. Restart the printer 265 | 266 | ## Support 267 | 268 | For issues or questions, please create a [GitHub Issue](https://github.com/BangerTech/BambuCAM/issues). 269 | 270 | 271 | ## Sponsorship 272 | 273 | SUPPORT 274 | 275 | ## Keywords 276 | `bambulab` `3d-printer` `camera-viewer` `monitoring` `docker` `react` `rtsp-stream` 277 | `printer-management` `web-interface` `live-stream` `temperature-monitoring` 278 | `print-progress` `open-source` 279 | 280 | ## Configuration 281 | 282 | ### Stream Settings 283 | The stream quality is automatically optimized. For manual adjustments, you can edit the go2rtc configuration in `backend/data/go2rtc/go2rtc.yaml`. 284 | 285 | ### Network Configuration 286 | Make sure the required ports (80, 1984, 4000) are available and not blocked by your firewall. -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Services/InstallationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using System.IO; 7 | using BambuCAM.Installer.Models; 8 | using System.Diagnostics; 9 | 10 | namespace BambuCAM.Installer.Services 11 | { 12 | public class InstallationService 13 | { 14 | private readonly DockerService _dockerService; 15 | private readonly NetworkService _networkService; 16 | private readonly DownloadService _downloadService; 17 | private readonly ShortcutService _shortcutService; 18 | private readonly string _serverIp; 19 | 20 | public InstallationService() 21 | { 22 | _dockerService = new DockerService(); 23 | _networkService = new NetworkService(); 24 | _downloadService = new DownloadService(); 25 | _shortcutService = new ShortcutService(); 26 | _serverIp = NetworkService.GetLocalIPAddress(); 27 | } 28 | 29 | public async Task Install( 30 | IProgress progress, 31 | bool createShortcut = true) 32 | { 33 | try 34 | { 35 | // Check Docker (20%) 36 | progress.Report(new InstallationStatus(0, "Checking Docker installation...")); 37 | if (!_dockerService.IsDockerInstalled()) 38 | { 39 | progress.Report(new InstallationStatus(10, "Installing Docker Desktop...")); 40 | await _dockerService.InstallDocker(); 41 | } 42 | 43 | // Configure WSL (40%) 44 | progress.Report(new InstallationStatus(20, "Configuring WSL...")); 45 | await ConfigureWsl(); 46 | 47 | // Start Docker if needed 48 | var dockerRunning = await _dockerService.CheckDockerRunning(); 49 | if (!dockerRunning) 50 | { 51 | progress.Report(new InstallationStatus(40, "Starting Docker Desktop...")); 52 | await _dockerService.StartDockerDesktop(); 53 | } 54 | 55 | // Check ports (35%) 56 | progress.Report(new InstallationStatus(35, "Checking port availability...")); 57 | await _networkService.CheckRequiredPorts(); 58 | 59 | // Download and extract (70%) 60 | progress.Report(new InstallationStatus(35, "Downloading BambuCAM...")); 61 | await _downloadService.DownloadAndExtract(progress); 62 | 63 | // Start containers (85%) 64 | progress.Report(new InstallationStatus(70, "Starting Docker containers...")); 65 | await _dockerService.StartContainers(); 66 | 67 | // Wait for services (95%) 68 | progress.Report(new InstallationStatus(85, "Waiting for services to start...", "This may take a few minutes")); 69 | await WaitForServices(); 70 | 71 | // Create shortcut if requested 72 | if (createShortcut) 73 | { 74 | progress.Report(new InstallationStatus(95, "Creating desktop shortcut...")); 75 | _shortcutService.CreateDesktopShortcut( 76 | $"http://{_serverIp}", 77 | "BambuCAM" 78 | ); 79 | } 80 | 81 | // Nach Docker Installation, vor Container-Start 82 | var dockerWslService = new DockerWslService(); 83 | await dockerWslService.ConfigureWsl(); 84 | 85 | // Ports freigeben 86 | await _networkService.KillProcessOnPort(80); 87 | await _networkService.KillProcessOnPort(4000); 88 | await _networkService.KillProcessOnPort(1984); 89 | 90 | // Complete 91 | progress.Report(new InstallationStatus(100, "Installation completed successfully!")); 92 | return InstallationProgress.Successful; 93 | } 94 | catch (Exception ex) 95 | { 96 | string errorMessage = ex.Message; 97 | if (errorMessage.Contains("Connection refused")) 98 | { 99 | errorMessage = $"Could not connect to BambuCAM services at {_serverIp}. Please check if your firewall is blocking the connection."; 100 | } 101 | return InstallationProgress.Failure(errorMessage); 102 | } 103 | } 104 | 105 | private async Task WaitForServices() 106 | { 107 | using var client = new HttpClient(); 108 | var endpoints = new[] 109 | { 110 | $"http://{_serverIp}", 111 | $"http://{_serverIp}:4000/api/health", 112 | $"http://{_serverIp}:1984/api/status" 113 | }; 114 | 115 | foreach (var endpoint in endpoints) 116 | { 117 | var retries = 30; 118 | while (retries-- > 0) 119 | { 120 | try 121 | { 122 | var response = await client.GetAsync(endpoint); 123 | if (response.IsSuccessStatusCode) break; 124 | } 125 | catch 126 | { 127 | if (retries == 0) 128 | throw new Exception($"Service at {endpoint} did not respond. Installation may have failed."); 129 | await Task.Delay(1000); 130 | } 131 | } 132 | } 133 | } 134 | 135 | private async Task ConfigureWsl() 136 | { 137 | try 138 | { 139 | // Prüfe ob Docker Desktop läuft und WSL2 aktiv ist 140 | var process = new Process 141 | { 142 | StartInfo = new ProcessStartInfo 143 | { 144 | FileName = "wsl", 145 | Arguments = "--status", 146 | UseShellExecute = false, 147 | RedirectStandardOutput = true, 148 | CreateNoWindow = true // Verstecke das Fenster 149 | } 150 | }; 151 | process.Start(); 152 | await process.WaitForExitAsync(); 153 | 154 | // Erstelle .wslconfig 155 | var wslConfigPath = Path.Combine( 156 | Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), 157 | ".wslconfig" 158 | ); 159 | 160 | var wslConfig = @"[wsl2] 161 | localhostForwarding=true"; // Nur diese Option behalten 162 | 163 | await File.WriteAllTextAsync(wslConfigPath, wslConfig); 164 | 165 | // Führe alle Befehle in einer einzigen elevated PowerShell aus 166 | var script = @" 167 | # Port-Forwarding 168 | $wslIp = (wsl hostname -I).Trim().Split(' ')[0] 169 | $ports = @(80, 4000, 1984) 170 | foreach ($port in $ports) { 171 | netsh interface portproxy delete v4tov4 listenport=$port 172 | netsh interface portproxy add v4tov4 listenport=$port listenaddress=0.0.0.0 connectport=$port connectaddress=$wslIp 173 | New-NetFirewallRule -DisplayName ""BambuCAM Port $port"" -Direction Inbound -Action Allow -Protocol TCP -LocalPort $port -ErrorAction SilentlyContinue 174 | } 175 | 176 | # WSL neu starten 177 | wsl --shutdown 178 | Start-Sleep -Seconds 5 179 | wsl --status"; 180 | 181 | var scriptPath = Path.Combine(Path.GetTempPath(), "configure-wsl.ps1"); 182 | await File.WriteAllTextAsync(scriptPath, script); 183 | 184 | var psProcess = new Process 185 | { 186 | StartInfo = new ProcessStartInfo 187 | { 188 | FileName = "powershell", 189 | Arguments = $"-ExecutionPolicy Bypass -File {scriptPath}", 190 | UseShellExecute = true, 191 | Verb = "runas", 192 | CreateNoWindow = true 193 | } 194 | }; 195 | psProcess.Start(); 196 | await psProcess.WaitForExitAsync(); 197 | 198 | // Cleanup 199 | File.Delete(scriptPath); 200 | } 201 | catch (Exception ex) 202 | { 203 | throw new Exception($"Failed to configure WSL: {ex.Message}"); 204 | } 205 | } 206 | 207 | private async Task GetWslIp() 208 | { 209 | var process = new Process 210 | { 211 | StartInfo = new ProcessStartInfo 212 | { 213 | FileName = "wsl", 214 | Arguments = "hostname -I", 215 | UseShellExecute = false, 216 | RedirectStandardOutput = true 217 | } 218 | }; 219 | process.Start(); 220 | var output = await process.StandardOutput.ReadToEndAsync(); 221 | await process.WaitForExitAsync(); 222 | return output.Trim().Split(' ')[0]; 223 | } 224 | } 225 | } -------------------------------------------------------------------------------- /installer/BambuCAM.Installer/Services/DockerService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Diagnostics; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace BambuCAM.Installer.Services 9 | { 10 | public class DockerService 11 | { 12 | public bool IsDockerInstalled() 13 | { 14 | try 15 | { 16 | var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); 17 | var dockerPath = Path.Combine(programFiles, "Docker", "Docker", "Docker Desktop.exe"); 18 | return File.Exists(dockerPath); 19 | } 20 | catch 21 | { 22 | return false; 23 | } 24 | } 25 | 26 | public async Task StartDockerDesktop() 27 | { 28 | var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); 29 | var dockerPath = Path.Combine(programFiles, "Docker", "Docker", "Docker Desktop.exe"); 30 | 31 | if (File.Exists(dockerPath)) 32 | { 33 | var process = new Process 34 | { 35 | StartInfo = new ProcessStartInfo 36 | { 37 | FileName = dockerPath, 38 | UseShellExecute = true 39 | } 40 | }; 41 | 42 | process.Start(); 43 | // Warte bis Docker bereit ist 44 | await WaitForDockerReady(); 45 | } 46 | } 47 | 48 | private async Task WaitForDockerReady() 49 | { 50 | var retries = 60; // 2 Minuten Timeout 51 | while (retries-- > 0) 52 | { 53 | try 54 | { 55 | var process = new Process 56 | { 57 | StartInfo = new ProcessStartInfo 58 | { 59 | FileName = "docker", 60 | Arguments = "info", 61 | RedirectStandardOutput = true, 62 | RedirectStandardError = true, 63 | UseShellExecute = false, 64 | CreateNoWindow = true 65 | } 66 | }; 67 | 68 | process.Start(); 69 | await process.WaitForExitAsync(); 70 | if (process.ExitCode == 0) return; 71 | } 72 | catch 73 | { 74 | // Ignoriere Fehler und versuche es weiter 75 | } 76 | await Task.Delay(2000); // Warte 2 Sekunden zwischen Versuchen 77 | } 78 | throw new Exception("Docker Desktop did not start properly. Please try starting it manually."); 79 | } 80 | 81 | public async Task InstallDocker() 82 | { 83 | var installerPath = Path.Combine(Path.GetTempPath(), "DockerDesktopInstaller.exe"); 84 | using (var client = new HttpClient()) 85 | { 86 | var response = await client.GetAsync("https://desktop.docker.com/win/stable/Docker%20Desktop%20Installer.exe"); 87 | using (var fs = new FileStream(installerPath, FileMode.Create)) 88 | { 89 | await response.Content.CopyToAsync(fs); 90 | } 91 | } 92 | 93 | var process = new Process 94 | { 95 | StartInfo = new ProcessStartInfo 96 | { 97 | FileName = installerPath, 98 | Arguments = "install --quiet", 99 | UseShellExecute = true, 100 | Verb = "runas" // Run as administrator 101 | } 102 | }; 103 | 104 | process.Start(); 105 | await process.WaitForExitAsync(); 106 | 107 | // Starte Docker Desktop 108 | await StartDockerDesktop(); 109 | } 110 | 111 | public async Task StartContainers() 112 | { 113 | // Stelle sicher, dass Docker installiert ist 114 | if (!IsDockerInstalled()) 115 | { 116 | throw new Exception("Docker is not installed. Please install Docker Desktop first."); 117 | } 118 | 119 | // Starte Docker Desktop falls nicht aktiv 120 | var dockerRunning = await CheckDockerRunning(); 121 | if (!dockerRunning) 122 | { 123 | await StartDockerDesktop(); 124 | } 125 | 126 | var installDir = Path.Combine( 127 | Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 128 | "BambuCAM" 129 | ); 130 | 131 | // Prüfe ob docker-compose.yml existiert 132 | var composeFile = Path.Combine(installDir, "docker-compose.yml"); 133 | if (!File.Exists(composeFile)) 134 | { 135 | throw new Exception("docker-compose.yml not found. Please ensure BambuCAM was downloaded correctly."); 136 | } 137 | 138 | // Mehrere Versuche für das Starten der Container 139 | var retries = 5; 140 | var delay = 5000; 141 | 142 | while (retries-- > 0) 143 | { 144 | try 145 | { 146 | var process = new Process 147 | { 148 | StartInfo = new ProcessStartInfo 149 | { 150 | FileName = "docker", 151 | Arguments = "compose up -d", 152 | UseShellExecute = false, 153 | RedirectStandardOutput = true, 154 | RedirectStandardError = true, 155 | CreateNoWindow = true, 156 | WorkingDirectory = installDir 157 | } 158 | }; 159 | 160 | var output = new System.Text.StringBuilder(); 161 | var error = new System.Text.StringBuilder(); 162 | 163 | process.OutputDataReceived += (s, e) => { if (e.Data != null) output.AppendLine(e.Data); }; 164 | process.ErrorDataReceived += (s, e) => { if (e.Data != null) error.AppendLine(e.Data); }; 165 | 166 | process.Start(); 167 | process.BeginOutputReadLine(); 168 | process.BeginErrorReadLine(); 169 | await process.WaitForExitAsync(); 170 | 171 | if (process.ExitCode == 0) 172 | { 173 | return; // Erfolgreich! 174 | } 175 | 176 | if (retries == 0) 177 | { 178 | throw new Exception($"Failed to start Docker containers.\nError: {error}\nOutput: {output}"); 179 | } 180 | 181 | await Task.Delay(delay); 182 | } 183 | catch (Exception ex) 184 | { 185 | if (retries == 0) 186 | { 187 | throw new Exception($"Failed to start Docker containers: {ex.Message}"); 188 | } 189 | await Task.Delay(delay); 190 | } 191 | } 192 | 193 | // In StartContainers(): 194 | if (File.Exists(composeFile)) 195 | { 196 | // Debug: Zeige Inhalt der Datei 197 | var content = await File.ReadAllTextAsync(composeFile); 198 | Console.WriteLine($"docker-compose.yml content:\n{content}"); 199 | } 200 | } 201 | 202 | public async Task CheckDockerRunning() 203 | { 204 | try 205 | { 206 | var process = new Process 207 | { 208 | StartInfo = new ProcessStartInfo 209 | { 210 | FileName = "docker", 211 | Arguments = "info", 212 | RedirectStandardOutput = true, 213 | RedirectStandardError = true, 214 | UseShellExecute = false, 215 | CreateNoWindow = true 216 | } 217 | }; 218 | 219 | process.Start(); 220 | await process.WaitForExitAsync(); 221 | return process.ExitCode == 0; 222 | } 223 | catch 224 | { 225 | return false; 226 | } 227 | } 228 | 229 | public async Task StopContainers() 230 | { 231 | var process = new Process 232 | { 233 | StartInfo = new ProcessStartInfo 234 | { 235 | FileName = "docker", 236 | Arguments = "compose down", 237 | UseShellExecute = false, 238 | RedirectStandardOutput = true, 239 | CreateNoWindow = true, 240 | WorkingDirectory = Path.Combine( 241 | Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 242 | "BambuCAM" 243 | ) 244 | } 245 | }; 246 | 247 | process.Start(); 248 | await process.WaitForExitAsync(); 249 | } 250 | } 251 | } --------------------------------------------------------------------------------